├── .gitignore ├── CHANGE.log ├── README.rst ├── TODO.txt ├── TwitterAPI ├── BearerAuth.py ├── BearerAuthUser.py ├── TwitterAPI.py ├── TwitterError.py ├── TwitterOAuth.py ├── TwitterPager.py ├── __init__.py ├── constants.py └── credentials.txt ├── docs ├── Makefile ├── README.rst ├── authentication.rst ├── conf.py ├── errors.rst ├── examples.rst ├── faulttolerance.rst ├── index.rst ├── paging.rst ├── twitterapi.rst ├── twittererror.rst ├── twitteroauth.rst └── twitterpager.rst ├── examples ├── cli │ ├── README.rst │ ├── Unicode_win32.py │ └── cli.py ├── v1.1 │ ├── connect_using_login.py │ ├── connect_using_oauth.py │ ├── delete_last_tweet.py │ ├── direct_message.py │ ├── dump_timeline.py │ ├── logging.py │ ├── lookup_tweet.py │ ├── lookup_user.py │ ├── page_tweets.py │ ├── post_tweet.py │ ├── premium_search.py │ ├── sample_frequency.py │ ├── search_tweets.py │ ├── stream_tweets.py │ ├── unfollow_friends.py │ ├── upload_image.py │ ├── upload_tweet.py │ └── upload_video.py └── v2 │ ├── conversation_tree.py │ ├── expansions_media.py │ ├── expansions_place.py │ ├── expansions_poll.py │ ├── expansions_tweet.py │ ├── expansions_user.py │ ├── lookup_tweet.py │ ├── lookup_tweet_hydrate.py │ ├── lookup_tweets.py │ ├── lookup_user.py │ ├── page_tweets.py │ ├── page_tweets_hydrate.py │ ├── rules_add.py │ ├── rules_del.py │ ├── rules_del_all.py │ ├── rules_get.py │ ├── search_tweets.py │ ├── search_tweets_hydrate.py │ ├── stream_forever.py │ ├── stream_tweets.py │ ├── stream_tweets_hydrate.py │ ├── user_bookmarks.py │ ├── user_followers_following.py │ └── user_tweets.py ├── logo.png ├── requirements.txt ├── setup.py └── tests ├── test_iterators.py └── test_oauth.py /.gitignore: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | 3 | .DS_Store 4 | .vscode/* 5 | 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | dist 15 | build 16 | eggs 17 | parts 18 | bin 19 | var 20 | sdist 21 | develop-eggs 22 | .installed.cfg 23 | lib 24 | lib64 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | .idea/* 42 | env/* 43 | -------------------------------------------------------------------------------- /CHANGE.log: -------------------------------------------------------------------------------- 1 | v0.1.0, 24 Nov 2012 -- Initial release. 2 | 3 | v0.1.1, 26 Nov 2012 -- Added twitterapi.test. New error handling. 4 | 5 | v0.1.2, 28 Nov 2012 -- Imporved error and quota notification. 6 | 7 | v0.1.3, 02 Dec 2012 -- Added TwSearch.past_results() for paging old tweets. 8 | 9 | v0.1.4, 02 Dec 2012 -- Added wait argument to TwSearch.past_results(). 10 | 11 | v0.1.6, 03 Dec 2012 -- Added code comments. 12 | 13 | v0.1.7, 10 Dec 2012 -- Added TwTrends. 14 | 15 | v0.1.9, 14 Dec 2012 -- Added read_file to TwCredentials. 16 | Moved OAuth credentials out of test scripts and into credentials.txt. 17 | 18 | v0.1.10, 15 Dec 2012 -- Added socket timeout to TwStream. 19 | Added new_results to TwSearch to search new tweets. 20 | Added tools sub-folder containing a few useful scripts. 21 | 22 | v1.0.0, 30 Jan 2013 -- Uploaded to github. 23 | 24 | v2.0.0, 07 Jun 2013 -- Switched to requests module. 25 | 26 | v2.0.1, 10 Jun 2013 -- Updated documentation. 27 | 28 | v2.0.4, 16 Jun 2013 -- Works with python 3. 29 | 30 | v2.0.6, 25 Jun 2013 -- Fixed printing. 31 | 32 | v2.0.7, 06 Aug 2013 -- TwitterRestPager does not exit when no tweets. 33 | 34 | v2.0.8, 22 Aug 2013 -- Renamed TwitterOauth.py to TwitterOAuth.py. 35 | 36 | v2.0.9, 22 Aug 2013 -- Added flush to cli.py for python3 buffered stdout. 37 | 38 | v2.1.0, 16 Sep 2013 -- New class TwitterResponse. TwitterAPI no longer saves state. 39 | 40 | v2.1.1, 23 Sep 2013 -- Added sphinx-generated docs; added tags. 41 | 42 | v2.1.3, 07 Oct 2013 -- Fix circular imports in setup.py. 43 | 44 | v2.1.4, 07 Oct 2013 -- Fix endpoints with params in path. 45 | 46 | v2.1.5, 07 Oct 2013 -- Better timing in TwitterRestPager. 47 | 48 | v2.1.6, 07 Oct 2013 -- cli upports UTF-8 printing to console. 49 | 50 | v2.1.7, 07 Oct 2013 -- Fix endpoints with params in path. 51 | 52 | v2.1.8, 27 Dec 2013 -- Added OAuth 2.0 support. 53 | 54 | v2.1.9, 30 Jan 2014 -- Added image posting support. 55 | 56 | v2.1.10, 31 Jan 2014 -- Added proxy server support. 57 | 58 | v2.1.11, 22 Mar 2014 -- Fixed datetime import and oauth_test.py. 59 | 60 | v2.1.12, 22 Mar 2014 -- Ran code against autopep8. 61 | 62 | v2.1.13, 24 Mar 2014 -- Decreased streaming buffer size to 1. 63 | 64 | v2.2.0, 22 Jun 2014 -- Added indent option to cli.py. 65 | 66 | v2.2.3, 26 Aug 2014 -- Fix POST request for query string. 67 | 68 | v2.2.4, 25 Sep 2014 -- Merged tangentmonger-alternative branch with cleaner iterator syntax. 69 | 70 | v2.2.5, 25 Sep 2014 -- Added sphinx docs builder. 71 | 72 | v2.2.6, 12 Nov 2014 -- Silenced ValueError for streaming endpoints. 73 | Fixed cli.py field search for streaming endpoints. 74 | Updated constants.py with new endpoints - media/upload has new example. 75 | Streaming always uses 'delimited=length'. 76 | 77 | v2.2.7, 12 Nov 2014 -- Fix streaming delimited. 78 | 79 | v2.2.9, 12 Nov 2014 -- Merged dilmerv fix in constants.py. 80 | 81 | v2.2.8, 12 Nov 2014 -- Fix error in BearerAuth.py for OAuth 2. 82 | 83 | v2.3.0, 10 Feb 2015 -- Added logging, error classes, stall warning for streams, 84 | _RestIterable can iterate 'users' and 'ids'. 85 | 86 | v2.3.1, 18 Feb 2015 -- Changed 'is' to '==' for string comparison. 87 | 88 | v2.3.3, 21 Mar 2015 -- Merged revinewaters fix in TwitterError.py. 89 | 90 | v2.3.5, 28 Oct 2015 -- Removed deprecated classes RestIterator, StreamingIterator. 91 | 92 | v2.3.6, 20 Dec 2015 -- Added support in TwitterRestPager for endpoints with cursors. 93 | 94 | v2.4.0, 23 Jan 2016 -- Added support for Curator API and Collections API. 95 | 96 | v2.4.2, 14 Aug 2016 -- Merged karlicoss. Added method_override to request. 97 | 98 | v2.4.3, 29 Dec 2016 -- Create request session inside a with block. 99 | 100 | v2.4.4, 29 Dec 2016 -- Merged Socialery patch. Added optional arguments to json() method. 101 | 102 | v2.4.5, 25 Feb 2017 -- Merged NeilCTurner patch. Added statuses/unretweet API endpoint. 103 | 104 | v2.4.6, 16 Jul 2017 -- Merged RickRedSix patch. Added direct message endpoints. 105 | 106 | v2.4.7, 22 Oct 2017 -- Added endpoints for Ads API. 107 | 108 | v2.4.8, 03 Feb 2018 -- Merged RickRedSix patch. Added account activity endpoints. 109 | 110 | v2.4.9, 03 Feb 2018 -- Merged torufurukawa patch. Added webhook endpoint. 111 | 112 | v2.4.10, 11 Mar 2018 -- Merged torufurukawa patch. Added direct message endpoint. 113 | 114 | v2.5.0, 15 Mar 2018 -- Renamed get_rest_quota() to get_quota(). 115 | Renamed TwitterRestPager to TwitterPager. 116 | 117 | v2.5.1, 19 May 2018 -- Added support for both HTTP and HTTPS proxies. 118 | 119 | v2.5.2, 19 May 2018 -- Added endpoints for Premium Search API. 120 | 121 | v2.5.3, 19 May 2018 -- Added 'results' to iterator and a new Premium Search example. 122 | 123 | v2.5.4, 25 May 2018 -- Modified TwitterPager to support Premium Search's cursor. 124 | 125 | v2.5.5, 07 Oct 2018 -- Merged x0139 patch. Added typing indicator endpoint. 126 | Added DM destroy endpoint. 127 | 128 | v2.5.6, 09 Oct 2018 -- Fixed TwitterPager. 129 | 130 | v2.5.7, 15 Dec 2018 -- Merged HyperManTT patch. Added subscripts/list endpoint. 131 | 132 | v2.5.8, 25 Dec 2018 -- Fixed bug in TwitterPager, repeating results. 133 | 134 | v2.5.9, 14 Jan 2019 -- Fixed bug in TwitterPager, stalling but not exiting when no more results. 135 | Moved USER_AGENT and TIMEOUT constants to TwitterAPI class static variables. 136 | 137 | v2.5.10, 28 Oct 2019 -- Merged HammadH-variable-timeouts. 138 | 139 | v2.5.11, 10 Apr 2020 -- Merged RickRedSix patch. Added Labs API. 140 | 141 | v2.5.12, 13 Jul 2020 -- Merged drbig patch. Improved error reporting. 142 | 143 | v2.5.13, 17 Jul 2020 -- Merged drbig patch. Override exception handling in TwitterPager. 144 | Merged reuning patch. Added new endpoint. 145 | 146 | v2.5.14, 17 Sep 2020 -- Fix TwitterPager to iterate integer ids. 147 | 148 | v2.6.0, 04 Oct 2020 -- Merged dylancaponi-twitter-v2 with V2 support. 149 | Made several modifications to complete V2 support. 150 | Dropping Python 2.7 compatibility. 151 | 152 | v2.6.1, 23 Oct 2020 -- Fix TwitterPager, add conversation example. 153 | 154 | v2.6.2, 27 Oct 2020 -- Merged ckovamees-master with add media endpoints. 155 | 156 | v2.6.3, 12 Jan 2021 -- Merged peguerosdc-master with v2 user endpoints. 157 | 158 | v2.6.4, 27 Jan 2021 -- Fixed conversation_tree.py example. 159 | 160 | v2.6.5, 30 Jan 2021 -- Merged dylancaponi-patch-1 with Ads endpoints. 161 | 162 | v2.6.6, 14 Feb 2021 -- Fixed v2 next/prev token in TwitterPager. 163 | 164 | v2.6.7, 19 Feb 2021 -- Fixed v2 next/prev token in TwitterPager for search. 165 | 166 | v2.6.8, 19 Feb 2021 -- Added v2 tweet hydration. 167 | 168 | v2.6.9, 21 Feb 2021 -- Fixed bug in _StreamingIterable. 169 | 170 | v2.6.10, 06 Apr 2021 -- Added 'mentions' to be hydrated. 171 | 172 | v2.7.0, 12 Apr 2021 -- Two types of hydrate: APPEND and REPLACE. 173 | Two new enums: HydrateType, OAuthType. 174 | 175 | v2.7.1, 14 Apr 2021 -- Fixed oAuth version resolution. 176 | 177 | v2.7.2, 22 Apr 2021 -- Fixed _StreamIterable for v2. 178 | 179 | v2.7.3, 26 Apr 2021 -- Fixed hydrate for media_keys. 180 | 181 | v2.7.4, 27 Jun 2021 -- Merged RojerGS-patch-1 to fix documentation links. 182 | Merged peguerosdc-endpoint/v2-liking to add support for V2 likes. 183 | Changed 'stall_warning' to 'stall_warnings'. 184 | 185 | v2.7.5, 18 Jul 2021 -- Merged Dheavyman-fix-meta-keyerror. 186 | 187 | v2.7.6, 11 Nov 2021 -- Merged dylancaponi-patch-2 to update ADS version. 188 | 189 | v2.7.7, 11 Nov 2021 -- Merged NeodymiumFerBore-patch-1, musicist288-master 190 | 191 | v2.7.8, 29 Nov 2021 -- Fixed hydrated media keys 192 | 193 | v2.7.9, 30 Nov 2021 -- Specify all supported endpoint methods in constants.py 194 | Added new v2 endpoints 195 | 196 | v2.7.10, 14 Dec 2021 -- Fixed v1.1 POST bug 197 | 198 | v2.7.11, 02 Jan 2022 -- Merged m0namon-counts_all_next_token 199 | 200 | v2.7.12, 11 Feb 2022 -- Merged Dheavyman-update-ads-api-v10 201 | 202 | v2.7.13, 06 Jun 2022 -- Fixed endpoint conflict in constanst.py 203 | 204 | v2.8.0, 16 Oct 2022 -- Merged codewithbas-feature/user-bookmarks 205 | 206 | v2.8.1, 16 Oct 2022 -- Merged Dheavyman-ads-api-v11 207 | 208 | v2.8.2, 22 Mar 2023 -- Merged ERosendo PR 209 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |LOGO| 2 | ====== 3 | 4 | .. |LOGO| image:: https://raw.githubusercontent.com/geduldig/TwitterAPI/master/logo.png 5 | .. |BADGE_VERSION| image:: http://img.shields.io/pypi/v/TwitterAPI.svg 6 | :target: https://crate.io/packages/TwitterAPI 7 | .. |BADGE_CHAT| image:: https://badges.gitter.im/Join%20Chat.svg 8 | :alt: Join the chat at https://gitter.im/geduldig/TwitterAPI 9 | :target: https://gitter.im/geduldig/TwitterAPI?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 10 | 11 | .. |BADGE_2| image:: https://img.shields.io/endpoint?url=https%3A%2F%2Ftwbadges.glitch.me%2Fbadges%2Fv2 12 | :target: https://developer.twitter.com/en/docs/twitter-api 13 | .. |BADGE_LABS| image:: https://img.shields.io/endpoint?url=https%3A%2F%2Ftwbadges.glitch.me%2Fbadges%2Flabs 14 | :target: https://developer.twitter.com/en/docs/labs 15 | .. |BADGE_ADS| image:: https://img.shields.io/endpoint?url=https%3A%2F%2Ftwbadges.glitch.me%2Fbadges%2Fadsv9 16 | :target: https://developer.twitter.com/en/docs/twitter-ads-api 17 | .. |BADGE_1.1| image:: https://img.shields.io/endpoint?url=https%3A%2F%2Ftwbadges.glitch.me%2Fbadges%2Fstandard 18 | :target: https://developer.twitter.com/en/docs/twitter-api 19 | .. |BADGE_PREMIUM| image:: https://img.shields.io/endpoint?url=https%3A%2F%2Ftwbadges.glitch.me%2Fbadges%2Fpremium 20 | :target: https://developer.twitter.com 21 | 22 | 23 | TwitterAPI is a minimal python wrapper for the TwitterAPIs. A list of what it can do: 24 | 25 | * Support for all V1.1 and V2 endpoints, plus Premium, Ads, Labs, Collections. 26 | * OAuth1 and bearer token authentication, and proxy server authentication. 27 | * Streaming endpoints. 28 | * Paging results. 29 | * The option to "hydrate" results returned by V2 endpoints. 30 | * Error handling. 31 | 32 | Installation 33 | ------------ 34 | 35 | > pip install TwitterAPI 36 | 37 | Twitter API Version 1.1 Code Snippets 38 | ------------------------------------- 39 | [More examples in `TwitterAPI/examples/v1.1 `_] 40 | 41 | Search for recent tweets 42 | :: 43 | 44 | from TwitterAPI import TwitterAPI 45 | api = TwitterAPI(consumer_key, consumer_secret, access_token_key, access_token_secret) 46 | r = api.request('search/tweets', {'q':'pizza'}) 47 | for item in r: 48 | print(item) 49 | 50 | Stream tweets from New York City as they get tweeted 51 | :: 52 | 53 | r = api.request('statuses/filter', {'locations':'-74,40,-73,41'}) 54 | for item in r: 55 | print(item) 56 | 57 | Twitter API Version 2 Code Snippets 58 | ------------------------------------ 59 | [More examples in `TwitterAPI/examples/v2 `_ ] 60 | 61 | Search for recent tweets, and specify `fields` and `expansions` 62 | :: 63 | 64 | from TwitterAPI import TwitterAPI 65 | api = TwitterAPI(consumer_key, consumer_secret, access_token_key, access_token_secret, api_version='2') 66 | r = api.request('tweets/search/recent', { 67 | 'query':'pizza', 68 | 'tweet.fields':'author_id', 69 | 'expansions':'author_id'}) 70 | for item in r: 71 | print(item) 72 | 73 | One Method For Everything 74 | ------------------------- 75 | 76 | The ``request()`` method works with all version 1.1 and version 2 endpoints. Typcally, ``request()`` takes two arguments: a Twitter endpoint and a dictionary of endpoint parameters. 77 | 78 | The method returns an object that will iterate either search results and streams. The returned object also gives you access to the raw response (``r.text``) and the HTTP status code (``r.status_code``). See the `requests `_ library documentation for more details. 79 | 80 | Documentation 81 | ------------- 82 | * `An Introduction `_ 83 | * `Authentication `_ 84 | * `Error Handling `_ 85 | * `Paging Results `_ 86 | * `Tiny Examples `_ 87 | * `Fault Tolerant Streams and Pages `_ 88 | 89 | Extra Goodies 90 | ------------- 91 | Command-Line Utility (`examples/cli `_) 92 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Build test suite with unittest. 2 | (If anyone wants to contribute, would be much appreciated.) 3 | 4 | Test Ads API endpoints. 5 | 6 | Add and test Enterprise Search API endpoints. 7 | -------------------------------------------------------------------------------- /TwitterAPI/BearerAuth.py: -------------------------------------------------------------------------------- 1 | __author__ = "Andrea Biancini, geduldig" 2 | __date__ = "January 3, 2014" 3 | __license__ = "MIT" 4 | 5 | 6 | from .constants import * 7 | import base64 8 | import requests 9 | 10 | 11 | OAUTH2_SUBDOMAIN = 'api' 12 | OAUTH2_ENDPOINT = 'oauth2/token' 13 | 14 | 15 | class BearerAuth(requests.auth.AuthBase): 16 | 17 | """Request bearer access token for oAuth2 authentication. 18 | 19 | :param consumer_key: Twitter application consumer key 20 | :param consumer_secret: Twitter application consumer secret 21 | :param proxies: Dictionary of proxy URLs (see documentation for python-requests). 22 | """ 23 | 24 | def __init__(self, consumer_key, consumer_secret, proxies=None, user_agent=None): 25 | self._consumer_key = consumer_key 26 | self._consumer_secret = consumer_secret 27 | self.proxies = proxies 28 | self.user_agent = user_agent 29 | self._bearer_token = self._get_access_token() 30 | 31 | def _get_access_token(self): 32 | token_url = '%s://%s.%s/%s' % (PROTOCOL, 33 | OAUTH2_SUBDOMAIN, 34 | DOMAIN, 35 | OAUTH2_ENDPOINT) 36 | auth = self._consumer_key + ':' + self._consumer_secret 37 | b64_bearer_token_creds = base64.b64encode(auth.encode('utf8')) 38 | params = {'grant_type': 'client_credentials'} 39 | headers = {} 40 | headers['User-Agent'] = self.user_agent 41 | headers['Authorization'] = 'Basic ' + b64_bearer_token_creds.decode('utf8') 42 | headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8' 43 | try: 44 | response = requests.post( 45 | token_url, 46 | params=params, 47 | headers=headers, 48 | proxies=self.proxies) 49 | data = response.json() 50 | return data['access_token'] 51 | except Exception as e: 52 | raise Exception('Error requesting bearer access token: %s' % e) 53 | 54 | def __call__(self, r): 55 | auth_list = [ 56 | self._consumer_key, 57 | self._consumer_secret, 58 | self._bearer_token] 59 | if all(auth_list): 60 | r.headers['Authorization'] = "Bearer %s" % self._bearer_token 61 | return r 62 | else: 63 | raise Exception('Not enough keys passed to Bearer token manager.') 64 | -------------------------------------------------------------------------------- /TwitterAPI/BearerAuthUser.py: -------------------------------------------------------------------------------- 1 | __author__ = "Andrea Biancini, geduldig" 2 | __date__ = "January 3, 2014" 3 | __license__ = "MIT" 4 | 5 | 6 | from .constants import * 7 | import requests 8 | 9 | 10 | class BearerAuthUser(requests.auth.AuthBase): 11 | 12 | """Request bearer access token for oAuth2 authentication. 13 | 14 | :param consumer_key: Twitter application consumer key 15 | :param consumer_secret: Twitter application consumer secret 16 | :param proxies: Dictionary of proxy URLs (see documentation for python-requests). 17 | """ 18 | 19 | def __init__(self, oauth2_access_token, proxies=None, user_agent=None): 20 | self.proxies = proxies 21 | self.user_agent = user_agent 22 | self._bearer_token = oauth2_access_token 23 | 24 | 25 | def __call__(self, r): 26 | auth_list = [ 27 | self._bearer_token] 28 | if all(auth_list): 29 | r.headers['Authorization'] = "Bearer %s" % self._bearer_token 30 | return r 31 | else: 32 | raise Exception('Not enough keys passed to Bearer token manager.') 33 | -------------------------------------------------------------------------------- /TwitterAPI/TwitterAPI.py: -------------------------------------------------------------------------------- 1 | __author__ = "geduldig" 2 | __date__ = "June 7, 2013" 3 | __license__ = "MIT" 4 | 5 | 6 | from .BearerAuth import BearerAuth as OAuth2 7 | from .BearerAuthUser import BearerAuthUser as OAuth2User 8 | from .constants import * 9 | from .TwitterError import * 10 | from datetime import datetime 11 | from enum import Enum 12 | from requests.exceptions import ConnectionError, ReadTimeout, SSLError 13 | from requests.packages.urllib3.exceptions import ReadTimeoutError, ProtocolError 14 | from requests_oauthlib import OAuth1 15 | import json 16 | import os 17 | import requests 18 | import socket 19 | import ssl 20 | import time 21 | 22 | 23 | DEFAULT_USER_AGENT = os.getenv('DEFAULT_USER_AGENT', 'python-TwitterAPI') 24 | DEFAULT_CONNECTION_TIMEOUT = os.getenv('DEFAULT_CONNECTION_TIMEOUT', 5) 25 | DEFAULT_STREAMING_TIMEOUT = os.getenv('DEFAULT_STREAMING_TIMEOUT', 90) 26 | DEFAULT_REST_TIMEOUT = os.getenv('DEFAULT_REST_TIMEOUT', 5) 27 | 28 | 29 | class OAuthType(Enum): 30 | OAUTH1 = 'oAuth1' 31 | OAUTH2 = 'oAuth2' 32 | OAUTH2USER = 'oAuth2User' 33 | 34 | 35 | class HydrateType(Enum): 36 | NONE = 0 37 | APPEND = 1 38 | REPLACE = 2 39 | 40 | 41 | class TwitterAPI(object): 42 | 43 | """Access REST API or Streaming API resources. 44 | 45 | :param consumer_key: Twitter application consumer key 46 | :param consumer_secret: Twitter application consumer secret 47 | :param access_token_key: Twitter application access token key 48 | :param access_token_secret: Twitter application access token secret 49 | :param auth_type: "oAuth1" (default) or "oAuth2" 50 | :param proxy_url: HTTPS proxy URL string (ex. 'https://USER:PASSWORD@SERVER:PORT'), 51 | or dict of URLs (ex. {'http':'http://SERVER', 'https':'https://SERVER'}) 52 | """ 53 | 54 | # static properties to be overridden if desired 55 | USER_AGENT = DEFAULT_USER_AGENT 56 | CONNECTION_TIMEOUT = DEFAULT_CONNECTION_TIMEOUT 57 | STREAMING_TIMEOUT = DEFAULT_STREAMING_TIMEOUT 58 | REST_TIMEOUT = DEFAULT_REST_TIMEOUT 59 | 60 | def __init__( 61 | self, 62 | consumer_key=None, 63 | consumer_secret=None, 64 | access_token_key=None, 65 | access_token_secret=None, 66 | oauth2_access_token=None, 67 | auth_type=OAuthType.OAUTH1, 68 | proxy_url=None, 69 | api_version=VERSION): 70 | """Initialize with your Twitter application credentials""" 71 | # if there are multiple API versions, this will be the default version which can 72 | # also be overridden by specifying the version when calling the request method. 73 | self.version = api_version 74 | 75 | # Optional proxy or proxies. 76 | if isinstance(proxy_url, dict): 77 | self.proxies = proxy_url 78 | elif proxy_url is not None: 79 | self.proxies = {'https': proxy_url} 80 | else: 81 | self.proxies = None 82 | 83 | # Twitter supports two types of authentication. 84 | if auth_type == OAuthType.OAUTH1 or auth_type == 'oAuth1': 85 | if not all([consumer_key, consumer_secret, access_token_key, access_token_secret]): 86 | raise Exception('Missing authentication parameter') 87 | self.auth = OAuth1( 88 | consumer_key, 89 | consumer_secret, 90 | access_token_key, 91 | access_token_secret) 92 | elif auth_type == OAuthType.OAUTH2 or auth_type == 'oAuth2': 93 | if not all([consumer_key, consumer_secret]): 94 | raise Exception('Missing authentication parameter') 95 | self.auth = OAuth2( 96 | consumer_key, 97 | consumer_secret, 98 | proxies=self.proxies, 99 | user_agent=self.USER_AGENT) 100 | elif auth_type == OAuthType.OAUTH2USER or auth_type == 'oAuth2User': 101 | if not all([oauth2_access_token]): 102 | raise Exception('Missing authentication parameter') 103 | self.auth = OAuth2User( 104 | oauth2_access_token=oauth2_access_token, 105 | proxies=self.proxies, 106 | user_agent=self.USER_AGENT) 107 | else: 108 | raise Exception('Unknown oAuth version') 109 | 110 | def _prepare_url(self, subdomain, path): 111 | if subdomain == 'curator': 112 | return '%s://%s.%s/%s/%s.json' % (PROTOCOL, 113 | subdomain, 114 | DOMAIN, 115 | CURATOR_VERSION, 116 | path) 117 | elif subdomain == 'ads-api': 118 | return '%s://%s.%s/%s/%s' % (PROTOCOL, 119 | subdomain, 120 | DOMAIN, 121 | ADS_VERSION, 122 | path) 123 | elif subdomain == 'api' and 'labs/' in path: 124 | return '%s://%s.%s/%s' % (PROTOCOL, 125 | subdomain, 126 | DOMAIN, 127 | path) 128 | elif self.version == '1.1': 129 | return '%s://%s.%s/%s/%s.json' % (PROTOCOL, 130 | subdomain, 131 | DOMAIN, 132 | self.version, 133 | path) 134 | elif self.version == '2': 135 | return '%s://%s.%s/%s/%s' % (PROTOCOL, 136 | subdomain, 137 | DOMAIN, 138 | self.version, 139 | path) 140 | else: 141 | raise Exception('Unsupported API version') 142 | 143 | def _get_endpoint(self, resource): 144 | """Substitute any parameters in the resource path with :PARAM.""" 145 | if ':' in resource: 146 | parts = resource.split('/') 147 | # embedded parameters start with ':' 148 | parts = [k if k[0] != ':' else ':PARAM' for k in parts] 149 | endpoint = '/'.join(parts) 150 | resource = resource.replace(':', '') 151 | return (resource, endpoint) 152 | else: 153 | return (resource, resource) 154 | 155 | def request(self, resource, params=None, files=None, method_override=None, hydrate_type=HydrateType.NONE): 156 | """Request a Twitter REST API or Streaming API resource. 157 | 158 | :param resource: A Twitter endpoint (ex. "search/tweets") 159 | :param params: Dictionary with endpoint parameters or None (default) 160 | :param files: Dictionary with multipart-encoded file or None (default) 161 | :param method_override: Request method to override or None (default). 162 | If an endpoint supports more than one method, the default method is the first in the list of methods. 163 | :param hydrate_type: HydrateType or int 164 | Do not hydrate ('includes' field and all its content will be lost for non-streaming requests) - NONE or 0 (default) 165 | Append new field with '_hydrate' suffix with hydrate values - APPEND or 1 166 | Replace current field value with hydrate values - REPLACE or 2 167 | 168 | :returns: TwitterResponse 169 | :raises: TwitterConnectionError 170 | """ 171 | # check that the endpoint is valid 172 | resource, endpoint = self._get_endpoint(resource) 173 | if endpoint not in ENDPOINTS: 174 | raise Exception('Endpoint "%s" unsupported' % endpoint) 175 | # check that the method is valid if the endpoint supports more than one method 176 | method, subdomain = ENDPOINTS[endpoint] 177 | if not method_override and isinstance(method, list): 178 | method = method[0] # by default use first method in list 179 | elif isinstance(method, list): 180 | if method_override in method: 181 | method = method_override # use method_override 182 | else: 183 | raise Exception(f'Endpoint "{endpoint}" with method "{method_override}" unsupported') 184 | 185 | with requests.Session() as session: 186 | session.auth = self.auth 187 | session.headers = {'User-Agent': self.USER_AGENT} 188 | url = self._prepare_url(subdomain, resource) 189 | if self.version == '1.1' and 'stream' in subdomain: 190 | session.stream = True 191 | timeout = self.STREAMING_TIMEOUT 192 | if not params: 193 | params = {} 194 | params['delimited'] = 'length' 195 | params['stall_warnings'] = 'true' 196 | elif self.version == '2' and resource.endswith('/stream'): 197 | session.stream = True 198 | timeout = self.STREAMING_TIMEOUT 199 | else: 200 | session.stream = False 201 | timeout = self.REST_TIMEOUT 202 | d = p = j = None 203 | if method == 'POST': 204 | if self.version == '1.1' and 'metadata' not in resource: 205 | d = params 206 | else: 207 | j = params 208 | elif method == 'PUT': 209 | j = params 210 | else: 211 | p = params 212 | 213 | try: 214 | if False and method == 'PUT': 215 | session.headers['Content-type'] = 'application/json' 216 | data = params 217 | r = session.request( 218 | method, 219 | url, 220 | json=data) 221 | else: 222 | r = session.request( 223 | method, 224 | url, 225 | data=d, 226 | params=p, 227 | json=j, 228 | timeout=(self.CONNECTION_TIMEOUT, timeout), 229 | files=files, 230 | proxies=self.proxies) 231 | except (ConnectionError, ProtocolError, ReadTimeout, ReadTimeoutError, 232 | SSLError, ssl.SSLError, socket.error) as e: 233 | raise TwitterConnectionError(e) 234 | 235 | options = { 236 | 'api_version': self.version, 237 | 'is_stream': session.stream, 238 | 'hydrate_type': hydrate_type 239 | } 240 | return TwitterResponse(r, options) 241 | 242 | 243 | class TwitterResponse(object): 244 | 245 | """Response from either a REST API or Streaming API resource call. 246 | 247 | :param response: The requests.Response object returned by the API call 248 | :param stream: Boolean connection type (True if a streaming connection) 249 | :param options: Dict containing parsing options 250 | """ 251 | 252 | def __init__(self, response, options): 253 | self.response = response 254 | self.options = options 255 | 256 | @property 257 | def headers(self): 258 | """:returns: Dictionary of API response header contents.""" 259 | return self.response.headers 260 | 261 | @property 262 | def status_code(self): 263 | """:returns: HTTP response status code.""" 264 | return self.response.status_code 265 | 266 | @property 267 | def text(self): 268 | """:returns: Raw API response text.""" 269 | return self.response.text 270 | 271 | def json(self, **kwargs): 272 | """Get the response as a JSON object. 273 | 274 | :param \*\*kwargs: Optional arguments that ``json.loads`` takes. 275 | :returns: response as JSON object. 276 | :raises: ValueError 277 | """ 278 | return self.response.json(**kwargs) 279 | 280 | def get_iterator(self): 281 | """Get API dependent iterator. 282 | 283 | :returns: Iterator for tweets or other message objects in response. 284 | :raises: TwitterConnectionError, TwitterRequestError 285 | """ 286 | if self.response.status_code != 200: 287 | raise TwitterRequestError(self.response.status_code, msg=self.response.text) 288 | 289 | if self.options['is_stream']: 290 | return iter(_StreamingIterable(self.response, self.options)) 291 | else: 292 | return iter(_RestIterable(self.response, self.options)) 293 | 294 | def __iter__(self): 295 | """Get API dependent iterator. 296 | 297 | :returns: Iterator for tweets or other message objects in response. 298 | :raises: TwitterConnectionError, TwitterRequestError 299 | """ 300 | return self.get_iterator() 301 | 302 | def get_quota(self): 303 | """Quota information in the REST-only response header. 304 | 305 | :returns: Dictionary of 'remaining' (count), 'limit' (count), 'reset' (time) 306 | """ 307 | remaining, limit, reset = None, None, None 308 | if self.response: 309 | if 'x-rate-limit-remaining' in self.response.headers: 310 | remaining = int( 311 | self.response.headers['x-rate-limit-remaining']) 312 | if remaining == 0: 313 | limit = int(self.response.headers['x-rate-limit-limit']) 314 | reset = int(self.response.headers['x-rate-limit-reset']) 315 | reset = datetime.fromtimestamp(reset) 316 | return {'remaining': remaining, 'limit': limit, 'reset': reset} 317 | 318 | def close(self): 319 | """Disconnect stream (blocks with Python 3).""" 320 | self.response.raw.close() 321 | 322 | 323 | class _RestIterable(object): 324 | 325 | """Iterate statuses, errors or other iterable objects in a REST API response. 326 | 327 | :param response: The request.Response from a Twitter REST API request 328 | :param options: Dict containing parsing options 329 | """ 330 | 331 | def __init__(self, response, options): 332 | resp = response.json() 333 | if options['api_version'] == '2': 334 | if 'data' in resp: 335 | if isinstance(resp['data'], dict): 336 | resp['data'] = [resp['data']] 337 | h_type = options['hydrate_type'] 338 | if 'includes' in resp and h_type != HydrateType.NONE: 339 | field_suffix = '' if h_type == HydrateType.REPLACE else '_hydrate' 340 | self.results = _hydrate_tweets(resp['data'], resp['includes'], field_suffix) 341 | else: 342 | self.results = resp['data'] 343 | else: 344 | self.results = [] 345 | elif options['api_version'] == '1.1': 346 | # convert json response into something iterable 347 | if 'errors' in resp: 348 | self.results = resp['errors'] 349 | elif 'statuses' in resp: 350 | self.results = resp['statuses'] 351 | elif 'users' in resp: 352 | self.results = resp['users'] 353 | elif 'ids' in resp: 354 | self.results = resp['ids'] 355 | elif 'results' in resp: 356 | self.results = resp['results'] 357 | elif 'data' in resp: 358 | if not isinstance(resp['data'], dict): 359 | self.results = resp['data'] 360 | else: 361 | self.results = [resp['data']] 362 | elif hasattr(resp, '__iter__') and not isinstance(resp, dict): 363 | if len(resp) > 0 and 'trends' in resp[0]: 364 | self.results = resp[0]['trends'] 365 | else: 366 | self.results = resp 367 | else: 368 | self.results = (resp,) 369 | else: 370 | self.results = [] 371 | 372 | def __iter__(self): 373 | """ 374 | :returns: Tweet status as a JSON object. 375 | """ 376 | for item in self.results: 377 | yield item 378 | 379 | 380 | class _StreamingIterable(object): 381 | 382 | """Iterate statuses or other objects in a Streaming API response. 383 | 384 | :param response: The request.Response from a Twitter Streaming API request 385 | :param options: Dict containing parsing options 386 | """ 387 | 388 | def __init__(self, response, options): 389 | self.stream = response.raw 390 | self.options = options 391 | 392 | def _iter_stream(self): 393 | """Stream parser. 394 | 395 | :returns: Next item in the stream (may or may not be 'delimited'). 396 | :raises: TwitterConnectionError, StopIteration 397 | """ 398 | while True: 399 | item = None 400 | buf = bytearray() 401 | stall_timer = None 402 | try: 403 | while True: 404 | # read bytes until item boundary reached 405 | buf += self.stream.read(1) 406 | if not buf: 407 | # check for stall (i.e. no data for 90 seconds) 408 | if not stall_timer: 409 | stall_timer = time.time() 410 | elif time.time() - stall_timer > TwitterAPI.STREAMING_TIMEOUT: 411 | raise TwitterConnectionError('Twitter stream stalled') 412 | elif stall_timer: 413 | stall_timer = None 414 | if buf[-2:] == b'\r\n': 415 | item = buf[0:-2] 416 | if item.isdigit(): 417 | # use byte size to read next item 418 | nbytes = int(item) 419 | item = None 420 | item = self.stream.read(nbytes) 421 | break 422 | if item: 423 | item = json.loads(item.decode('utf8')) 424 | if self.options['api_version'] == '2': 425 | h_type = self.options['hydrate_type'] 426 | if h_type != HydrateType.NONE: 427 | if 'data' in item and 'includes' in item: 428 | field_suffix = '' if h_type == HydrateType.REPLACE else '_hydrate' 429 | item = { 'data':_hydrate_tweets(item['data'], item['includes'], field_suffix) } 430 | yield item 431 | except (ConnectionError, ValueError, ProtocolError, ReadTimeout, ReadTimeoutError, 432 | SSLError, ssl.SSLError, socket.error, json.decoder.JSONDecodeError) as e: 433 | raise TwitterConnectionError(e) 434 | except AttributeError: 435 | # inform iterator to exit when client closes connection 436 | raise StopIteration 437 | 438 | def __iter__(self): 439 | """ 440 | :returns: Tweet status as a JSON object. 441 | :raises: TwitterConnectionError 442 | """ 443 | for item in self._iter_stream(): 444 | if item: 445 | yield item 446 | 447 | 448 | def _hydrate_tweets(data, includes, field_suffix): 449 | """Insert expansion fields back into tweet data by appending 450 | a new field as a sibling to the referenced field. 451 | 452 | :param data: "data" property value in JSON response 453 | :param includes: "includes" property value in JSON response 454 | :param field_suffix: Suffix appended to a hydrated field name. 455 | Either "_hydrate" which puts hydrated values into 456 | a new field, or "" which replaces the current 457 | field value with hydrated values. 458 | 459 | :returns: Tweet status as a JSON object. 460 | """ 461 | new_fields = [] 462 | for key in includes: 463 | incl = includes[key] 464 | for obj in incl: 465 | for field in ['id', 'media_key', 'username']: 466 | if field in obj: 467 | _create_include_fields(data, (obj[field], obj), new_fields) 468 | 469 | for item in new_fields: 470 | parent = item[0] 471 | field = item[1] + field_suffix 472 | include = item[2] 473 | if field in parent: 474 | if item[1] == 'media_keys': 475 | parent[field] += include 476 | if field_suffix == '': 477 | # REPLACE option 478 | parent[field].remove(include[0]['media_key']) 479 | else: 480 | parent[field] = include 481 | else: 482 | parent[field] = include 483 | return data 484 | 485 | 486 | def _create_include_fields(parent, include, new_fields): 487 | """Depth-first seach into 'parent' to locate fields referenced in 488 | 'include'. Each match is appended to 'new_fields'. 489 | """ 490 | if isinstance(parent, list): 491 | for item in parent: 492 | _create_include_fields(item, include, new_fields) 493 | elif isinstance(parent, dict): 494 | for key, value in parent.items(): 495 | if value == include[0]: 496 | new_fields.append((parent, key, include[1])) 497 | elif isinstance(value, list) and all(isinstance(elem, str) for elem in value): 498 | # this code gets executed for "media_keys" which are in a list object 499 | includes_list = [] 500 | for elem in value: 501 | if elem == include[0]: 502 | includes_list.append(include[1]) 503 | if len(includes_list) > 0: 504 | new_fields.append((parent, key, includes_list)) 505 | else: 506 | _create_include_fields(value, include, new_fields) 507 | -------------------------------------------------------------------------------- /TwitterAPI/TwitterError.py: -------------------------------------------------------------------------------- 1 | __author__ = "geduldig" 2 | __date__ = "November 30, 2014" 3 | __license__ = "MIT" 4 | 5 | 6 | import logging 7 | import json 8 | 9 | 10 | class TwitterError(Exception): 11 | 12 | """Base class for Twitter exceptions""" 13 | pass 14 | 15 | 16 | class TwitterConnectionError(TwitterError): 17 | 18 | """Raised when the connection needs to be re-established""" 19 | 20 | def __init__(self, value): 21 | super().__init__(value) 22 | logging.warning('%s %s' % (type(value), value)) 23 | 24 | 25 | class TwitterRequestError(TwitterError): 26 | 27 | """Raised when request fails""" 28 | 29 | def __init__(self, status_code, msg=None): 30 | if msg is None: 31 | if status_code >= 500: 32 | msg = 'Twitter internal error (you may re-try)' 33 | else: 34 | msg = 'Twitter request failed' 35 | logging.info('Status code %d: %s' % (status_code, msg)) 36 | super().__init__(msg) 37 | self.status_code = status_code 38 | self.msg = msg 39 | 40 | def __str__(self): 41 | return '%s (%d): %s' % (self.args, self.status_code, self.msg) 42 | 43 | 44 | def __iter__(self): 45 | try: 46 | msg = json.loads(self.msg) 47 | if 'errors' in msg: 48 | for error in msg['errors']: 49 | yield error['message'] 50 | elif 'detail' in msg: 51 | yield msg['detail'] 52 | else: 53 | yield self.msg 54 | except: 55 | yield self.msg -------------------------------------------------------------------------------- /TwitterAPI/TwitterOAuth.py: -------------------------------------------------------------------------------- 1 | __author__ = "geduldig" 2 | __date__ = "February 7, 2013" 3 | __license__ = "MIT" 4 | 5 | 6 | import os 7 | 8 | 9 | class TwitterOAuth: 10 | 11 | """Optional class for retrieving Twitter credentials stored in a text file. 12 | 13 | :param consumer_key: Twitter application consumer key 14 | :param consumer_secret: Twitter application consumer secret 15 | :param access_token_key: Twitter application access token key 16 | :param access_token_secret: Twitter application access token secret 17 | """ 18 | 19 | def __init__( 20 | self, 21 | consumer_key, 22 | consumer_secret, 23 | access_token_key, 24 | access_token_secret): 25 | self.consumer_key = consumer_key 26 | self.consumer_secret = consumer_secret 27 | self.access_token_key = access_token_key 28 | self.access_token_secret = access_token_secret 29 | 30 | @classmethod 31 | def read_file(cls, file_name=None): 32 | """Read OAuth credentials from a text file. File format: 33 | 34 | consumer_key=YOUR_CONSUMER_KEY 35 | 36 | consumer_secret=YOUR_CONSUMER_SECRET 37 | 38 | access_token_key=YOUR_ACCESS_TOKEN 39 | 40 | access_token_secret=YOUR_ACCESS_TOKEN_SECRET 41 | 42 | :param file_name: File containing credentials or None (default) reads credentials 43 | from TwitterAPI/credentials.txt 44 | """ 45 | if file_name is None: 46 | path = os.path.dirname(__file__) 47 | file_name = os.path.join(path, 'credentials.txt') 48 | 49 | with open(file_name) as f: 50 | oauth = {} 51 | for line in f: 52 | if '=' in line: 53 | tokens = line.strip().split('=', 1) 54 | if len(tokens) != 2 or not tokens[1]: 55 | raise Exception(cls.usage(file_name)) 56 | oauth[tokens[0].strip()] = tokens[1].strip() 57 | return TwitterOAuth( 58 | oauth['consumer_key'], 59 | oauth['consumer_secret'], 60 | oauth['access_token_key'], 61 | oauth['access_token_secret']) 62 | 63 | @classmethod 64 | def usage(cls, file_name): 65 | return (f'Credentials file: {file_name}\n' 66 | 'Expected format:\n' 67 | 'consumer_key=YOUR_CONSUMER_KEY\n' 68 | 'consumer_secret=YOUR_CONSUMER_SECRET\n' 69 | 'access_token_key=YOUR_ACCESS_TOKEN\n' 70 | 'access_token_secret=YOUR_ACCESS_TOKEN_SECRET\n') -------------------------------------------------------------------------------- /TwitterAPI/TwitterPager.py: -------------------------------------------------------------------------------- 1 | __author__ = "geduldig" 2 | __date__ = "June 8, 2013" 3 | __license__ = "MIT" 4 | 5 | 6 | from .TwitterAPI import HydrateType 7 | from .TwitterError import * 8 | from requests.exceptions import ConnectionError, ReadTimeout, SSLError 9 | from requests.packages.urllib3.exceptions import ReadTimeoutError, ProtocolError 10 | import requests 11 | import time 12 | 13 | 14 | class TwitterPager(object): 15 | 16 | """Continuous (stream-like) pagination of response from Twitter REST API resource. 17 | In addition to Public API endpoints, supports Premium Search API. 18 | 19 | :param api: An authenticated TwitterAPI object 20 | :param resource: String with the resource path (ex. search/tweets) 21 | :param params: Dictionary of resource parameters 22 | :param hydrate_type: HydrateType or int 23 | Do not hydrate - NONE or 0 (default) 24 | Append new field with '_hydrate' suffix with hydrate values - APPEND or 1 25 | Replace current field value with hydrate values - REPLACE or 2 26 | """ 27 | 28 | def __init__(self, api, resource, params=None, hydrate_type=HydrateType.NONE): 29 | self.api = api 30 | self.resource = resource 31 | if not params: 32 | params = {} 33 | self.params = params 34 | self.hydrate_type = hydrate_type 35 | 36 | def get_iterator(self, wait=5, new_tweets=False): 37 | """Iterate response from Twitter REST API resource. Resource is called 38 | in a loop to retrieve consecutive pages of results. 39 | 40 | :param wait: Floating point number (default=5) of seconds wait between requests. 41 | Depending on the resource, appropriate values are 5 or 60 seconds. 42 | :param new_tweets: Boolean determining the search direction. 43 | False (default) retrieves old results. 44 | True retrieves current results. 45 | 46 | :returns: JSON objects containing statuses, errors or other return info. 47 | """ 48 | elapsed = 0 49 | while True: 50 | try: 51 | # REQUEST ONE PAGE OF RESULTS... 52 | start = time.time() 53 | r = self.api.request(self.resource, self.params, hydrate_type=self.hydrate_type) 54 | it = r.get_iterator() 55 | if new_tweets: 56 | it = reversed(list(it)) 57 | 58 | # YIELD FOR EACH ITEM IN THE PAGE... 59 | item_count = 0 60 | id = None 61 | for item in it: 62 | item_count += 1 63 | if type(item) is dict: 64 | if 'id' in item: 65 | id = item['id'] 66 | if 'code' in item: 67 | if item['code'] in [130, 131]: 68 | # Twitter service error 69 | raise TwitterConnectionError(item) 70 | yield item 71 | 72 | data = r.json() 73 | 74 | # CHECK FOR NEXT PAGE OR BAIL... 75 | if self.api.version == '1.1': 76 | # if a cursor is present, use it to get next page 77 | # otherwise, use id to get next page 78 | is_premium_search = self.params and 'query' in self.params and self.api.version == '1.1' 79 | cursor = -1 80 | if new_tweets and 'previous_cursor' in data: 81 | cursor = data['previous_cursor'] 82 | cursor_param = 'cursor' 83 | elif not new_tweets: 84 | if 'next_cursor' in data: 85 | cursor = data['next_cursor'] 86 | cursor_param = 'cursor' 87 | elif 'next' in data: 88 | # 'next' is used by Premium Search (OLD searches only) 89 | cursor = data['next'] 90 | cursor_param = 'next' 91 | 92 | # bail when no more results 93 | if cursor == 0: 94 | break 95 | elif cursor == -1 and is_premium_search: 96 | break 97 | elif not new_tweets and item_count == 0: 98 | break 99 | else: # VERSION 2 100 | if 'meta' not in data: 101 | break 102 | meta = data['meta'] 103 | if not new_tweets and not 'next_token' in meta: 104 | break 105 | 106 | # SLEEP... 107 | elapsed = time.time() - start 108 | pause = wait - elapsed if elapsed < wait else 0 109 | time.sleep(pause) 110 | 111 | # SETUP REQUEST FOR NEXT PAGE... 112 | if self.api.version == '1.1': 113 | # get a page with cursor if present, or with id if not 114 | # a Premium search (i.e. 'query' is not a parameter) 115 | if cursor != -1: 116 | self.params[cursor_param] = cursor 117 | elif id is not None and not is_premium_search: 118 | if new_tweets: 119 | self.params['since_id'] = str(id) 120 | else: 121 | self.params['max_id'] = str(id - 1) 122 | else: 123 | continue 124 | else: # VERSION 2 125 | # TWITTER SHOULD STANDARDIZE ON pagination_token IN THE FUTURE 126 | SEARCH_ENDPOINTS = ['tweets/search/recent', 'tweets/search/all', 'tweets/counts/all'] 127 | pagination_token = 'next_token' if self.resource in SEARCH_ENDPOINTS else 'pagination_token' 128 | if new_tweets: 129 | self.params[pagination_token] = meta['previous_token'] 130 | else: 131 | self.params[pagination_token] = meta['next_token'] 132 | 133 | except TwitterRequestError as e: 134 | if e.status_code < 500: 135 | raise 136 | continue 137 | except TwitterConnectionError: 138 | continue -------------------------------------------------------------------------------- /TwitterAPI/__init__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'TwitterAPI' 2 | __version__ = '2.8.2' 3 | __author__ = 'geduldig' 4 | __license__ = 'MIT' 5 | __copyright__ = 'Copyright 2013 geduldig' 6 | 7 | 8 | import logging 9 | 10 | 11 | # Suppress logging unless the client provides a handler 12 | logging.getLogger(__name__).addHandler(logging.NullHandler()) 13 | 14 | 15 | try: 16 | from .TwitterAPI import TwitterAPI, TwitterResponse, OAuthType, HydrateType 17 | from .TwitterError import TwitterConnectionError, TwitterRequestError 18 | from .TwitterOAuth import TwitterOAuth 19 | from .TwitterPager import TwitterPager 20 | except: 21 | pass 22 | 23 | 24 | __all__ = [ 25 | 'TwitterAPI', 26 | 'TwitterConnectionError', 27 | 'TwitterRequestError', 28 | 'TwitterOAuth', 29 | 'TwitterPager' 30 | ] 31 | -------------------------------------------------------------------------------- /TwitterAPI/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | Constants For All Twitter Endpoints 3 | ----------------------------------- 4 | 5 | Version 1.1, Streaming API and REST API. 6 | 7 | URLs for each endpoint are composed of the following pieces: 8 | PROTOCOL://{subdomain}.DOMAIN/VERSION/{resource}?{parameters} 9 | """ 10 | 11 | 12 | __author__ = "geduldig" 13 | __date__ = "February 3, 2012" 14 | __license__ = "MIT" 15 | 16 | 17 | PROTOCOL = 'https' 18 | DOMAIN = 'twitter.com' 19 | 20 | 21 | VERSION = '1.1' 22 | CURATOR_VERSION = 'broadcast/1' 23 | ADS_VERSION = '11' 24 | 25 | 26 | ENDPOINTS = { 27 | # resource: (method(s), subdomain) 28 | 29 | # STREAMING API 30 | 31 | 'statuses/filter': ('POST', 'stream'), 32 | 'statuses/firehose': ('GET', 'stream'), 33 | 'statuses/sample': ('GET', 'stream'), 34 | 'site': ('GET', 'sitestream'), 35 | 'user': ('GET', 'userstream'), 36 | 37 | # PUBLIC API 38 | 39 | 'account/remove_profile_banner': ('POST', 'api'), 40 | 'account/settings': ('GET', 'api'), 41 | 'account/update_delivery_device': ('POST', 'api'), 42 | 'account/update_profile': ('POST', 'api'), 43 | 'account/update_profile_background_image': ('POST', 'api'), 44 | 'account/update_profile_banner': ('POST', 'api'), 45 | 'account/update_profile_colors': ('POST', 'api'), 46 | 'account/update_profile_image': ('POST', 'api'), 47 | 'account/verify_credentials': ('GET', 'api'), 48 | 49 | 'application/rate_limit_status': ('GET', 'api'), 50 | 51 | 'blocks/create': ('POST', 'api'), 52 | 'blocks/destroy': ('POST', 'api'), 53 | 'blocks/ids': ('GET', 'api'), 54 | 'blocks/list': ('GET', 'api'), 55 | 56 | 'direct_messages': ('GET', 'api'), # deprecated 57 | 'direct_messages/destroy': ('POST', 'api'), # deprecated 58 | 'direct_messages/events/destroy': ('DELETE', 'api'), 59 | 'direct_messages/events/new': ('POST', 'api'), 60 | 'direct_messages/events/list': ('GET', 'api'), 61 | 'direct_messages/events/show': ('GET', 'api'), 62 | 'direct_messages/indicate_typing': ('POST', 'api'), 63 | 'direct_messages/new': ('POST', 'api'), # deprecated 64 | 'direct_messages/sent': ('GET', 'api'), # deprecated 65 | 'direct_messages/show': ('GET', 'api'), # deprecated 66 | 'direct_messages/welcome_messages/new': ('POST', 'api'), 67 | 'direct_messages/welcome_messages/list': ('GET', 'api'), 68 | 'direct_messages/welcome_messages/show': ('GET', 'api'), 69 | 'direct_messages/welcome_messages/destroy': ('DELETE', 'api'), 70 | 'direct_messages/welcome_messages/rules/new': ('POST', 'api'), 71 | 'direct_messages/welcome_messages/rules/list': ('GET', 'api'), 72 | 'direct_messages/welcome_messages/rules/show': ('GET', 'api'), 73 | 'direct_messages/welcome_messages/rules/destroy': ('DELETE', 'api'), 74 | 75 | 'favorites/create': ('POST', 'api'), 76 | 'favorites/destroy': ('POST', 'api'), 77 | 'favorites/list': ('GET', 'api'), 78 | 79 | 'followers/ids': ('GET', 'api'), 80 | 'followers/list': ('GET', 'api'), 81 | 82 | 'friends/ids': ('GET', 'api'), 83 | 'friends/list': ('GET', 'api'), 84 | 85 | 'friendships/create': ('POST', 'api'), 86 | 'friendships/destroy': ('POST', 'api'), 87 | 'friendships/incoming': ('GET', 'api'), 88 | 'friendships/lookup': ('GET', 'api'), 89 | 'friendships/no_retweets/ids': ('GET', 'api'), 90 | 'friendships/outgoing': ('GET', 'api'), 91 | 'friendships/show': ('GET', 'api'), 92 | 'friendships/update': ('POST', 'api'), 93 | 94 | 'lists/create': ('POST', 'api'), 95 | 'lists/destroy': ('POST', 'api'), 96 | 'lists/list': ('GET', 'api'), 97 | 'lists/members': ('GET', 'api'), 98 | 'lists/members/create': ('POST', 'api'), 99 | 'lists/members/create_all': ('POST', 'api'), 100 | 'lists/members/destroy': ('POST', 'api'), 101 | 'lists/members/destroy_all': ('POST', 'api'), 102 | 'lists/members/show': ('GET', 'api'), 103 | 'lists/memberships': ('GET', 'api'), 104 | 'lists/ownerships': ('GET', 'api'), 105 | 'lists/show': ('GET', 'api'), 106 | 'lists/statuses': ('GET', 'api'), 107 | 'lists/subscribers': ('GET', 'api'), 108 | 'lists/subscribers/create': ('POST', 'api'), 109 | 'lists/subscribers/destroy': ('POST', 'api'), 110 | 'lists/subscribers/show': ('GET', 'api'), 111 | 'lists/subscriptions': ('GET', 'api'), 112 | 'lists/update': ('POST', 'api'), 113 | 114 | 'media/metadata/create': ('POST', 'upload'), 115 | 'media/upload': (['POST','GET'], 'upload'), 116 | 'media/subtitles/create': ('POST', 'upload'), 117 | 'media/subtitles/delete': ('POST', 'upload'), 118 | 119 | 'mutes/users/create': ('POST', 'api'), 120 | 'mutes/users/destroy': ('POST', 'api'), 121 | 'mutes/users/ids': ('GET', 'api'), 122 | 'mutes/users/list': ('GET', 'api'), 123 | 124 | 'geo/id/:PARAM': ('GET', 'api'), # PLACE_ID 125 | 'geo/place': ('POST', 'api'), 126 | 'geo/reverse_geocode': ('GET', 'api'), 127 | 'geo/search': ('GET', 'api'), 128 | 'geo/similar_places': ('GET', 'api'), 129 | 130 | 'help/configuration': ('GET', 'api'), 131 | 'help/languages': ('GET', 'api'), 132 | 'help/privacy': ('GET', 'api'), 133 | 'help/tos': ('GET', 'api'), 134 | 135 | 'saved_searches/create': ('POST', 'api'), 136 | 'saved_searches/destroy/:PARAM': ('POST', 'api'), # ID 137 | 'saved_searches/list': ('GET', 'api'), 138 | 'saved_searches/show/:PARAM': ('GET', 'api'), # ID 139 | 140 | 'search/tweets': ('GET', 'api'), 141 | 142 | 'statuses/destroy/:PARAM': ('POST', 'api'), # ID 143 | 'statuses/home_timeline': ('GET', 'api'), 144 | 'statuses/lookup': ('GET', 'api'), 145 | 'statuses/mentions_timeline': ('GET', 'api'), 146 | 'statuses/oembed': ('GET', 'api'), 147 | 'statuses/retweet/:PARAM': ('POST', 'api'), # ID 148 | 'statuses/retweeters/ids': ('GET', 'api'), 149 | 'statuses/retweets/:PARAM': ('GET', 'api'), # ID 150 | 'statuses/retweets_of_me': ('GET', 'api'), 151 | 'statuses/show/:PARAM': ('GET', 'api'), # ID 152 | 'statuses/unretweet/:PARAM': ('POST', 'api'), # ID 153 | 'statuses/user_timeline': ('GET', 'api'), 154 | 'statuses/update': ('POST', 'api'), 155 | 'statuses/update_with_media': ('POST', 'api'), # deprecated 156 | 157 | 'trends/available': ('GET', 'api'), 158 | 'trends/closest': ('GET', 'api'), 159 | 'trends/place': ('GET', 'api'), 160 | 161 | 'users/contributees': ('GET', 'api'), 162 | 'users/contributors': ('GET', 'api'), 163 | 'users/lookup': ('POST', 'api'), 164 | 'users/profile_banner': ('GET' 'api'), 165 | 'users/report_spam': ('POST', 'api'), 166 | 'users/search': ('GET', 'api'), 167 | 'users/show': ('GET', 'api'), 168 | 'users/suggestions': ('GET', 'api'), 169 | 'users/suggestions/:PARAM': ('GET', 'api'), # SLUG 170 | 'users/suggestions/:PARAM/members': ('GET', 'api'), # SLUG 171 | 172 | # COLLECTIONS API 173 | 174 | 'collections/create': ('POST', 'api'), 175 | 'collections/destroy': ('POST', 'api'), 176 | 'collections/entries': ('GET', 'api'), 177 | 'collections/entries/add': ('POST', 'api'), 178 | 'collections/entries/curate': ('POST', 'api'), 179 | 'collections/entries/move': ('POST', 'api'), 180 | 'collections/entries/remove': ('POST', 'api'), 181 | 'collections/list': ('GET', 'api'), 182 | 'collections/show': ('GET', 'api'), 183 | 'collections/update': ('POST', 'api'), 184 | 185 | # CURATOR API 186 | 187 | 'collections/:PARAM/content': ('GET', 'curator'), # ID 188 | 'projects': ('GET', 'curator'), 189 | 'projects/:PARAM': ('GET', 'curator'), # ID 190 | 'streams/:PARAM/content': ('GET', 'curator'), # ID 191 | 'streams/:PARAM/metrics': ('GET', 'curator'), # ID 192 | 'streams/:PARAM/trendline': ('GET', 'curator'), # ID 193 | 'streams/compare': ('GET', 'curator'), 194 | 'streams/compare_to_target': ('GET', 'curator'), 195 | 196 | # ADS API (not tested!!) 197 | 198 | 'accounts/:PARAM/auction_insights': ('GET', 'ads-api'), # ACCOUNT ID 199 | 'accounts/:PARAM/promoted_tweets': ('GET', 'ads-api'), # ACCOUNT ID 200 | 'stats/accounts/:PARAM': ('GET', 'ads-api'), # ACCOUNT ID 201 | 'stats/accounts/:PARAM/reach/funding_instruments': ('GET', 'ads-api'), # ACCOUNT ID 202 | 'stats/jobs/accounts/:PARAM': ('GET', 'ads-api'), # ACCOUNT ID 203 | 'stats/jobs/accounts/:PARAM': ('POST', 'ads-api'), # ACCOUNT ID 204 | 'stats/jobs/accounts/:PARAM/:PARAM': ('DELETE', 'ads-api'), # ACCOUNT ID, JOB ID 205 | 'stats/jobs/summaries': ('GET', 'ads-api'), 206 | 207 | # ACCOUNT ACTIVITY WEBHOOK API 208 | 209 | 'account_activity/all/:PARAM/subscriptions': (['POST', 'DELETE'], 'api'), # ENVIRONMENT NAME 210 | 'account_activity/all/:PARAM/subscriptions/all': ('GET', 'api'), # ENVIRONMENT NAME 211 | 'account_activity/all/:PARAM/subscriptions/all/list': ('GET', 'api'), # ENVIRONMENT NAME 212 | 'account_activity/all/:PARAM/subscriptions/list': ('GET', 'api'), # ENVIRONMENT NAME 213 | 'account_activity/all/:PARAM/webhooks': ('POST', 'api'), # ENVIRONMENT NAME 214 | 'account_activity/all/:PARAM/webhooks/:PARAM': ('DELETE', 'api'), # ENVIRONMENT NAME, WEBHOOK ID 215 | 'account_activity/all/count': ('GET', 'api'), 216 | 'account_activity/all/webhooks': ('GET', 'api'), 217 | 'account_activity/webhooks': ('POST', 'api'), 218 | 'account_activity/webhooks/:PARAM': ('DELETE', 'api'), # WEBHOOK ID 219 | 'account_activity/webhooks/:PARAM/subscriptions': ('POST', 'api'), # ENVIRONMENT NAME 220 | 'account_activity/webhooks/:PARAM/subscriptions/list': ('GET', 'api'), # ENVIRONMENT NAME 221 | 222 | # PREMIUM SEARCH API 223 | 224 | 'tweets/search/30day/:PARAM': ('GET', 'api'), # LABEL 225 | 'tweets/search/30day/:PARAM/counts': ('GET', 'api'), # LABEL 226 | 'tweets/search/fullarchive/:PARAM': ('GET', 'api'), # LABEL 227 | 'tweets/search/fullarchive/:PARAM/counts': ('GET', 'api'), # LABEL 228 | 229 | # LABS API (BETAS) WILL NEED APPLICATION APPROVAL 230 | 231 | 'labs/1/tweets/metrics/private': ('GET', 'api'), 232 | 'labs/2/tweets/:PARAM': ('GET', 'api'), # TWEET ID 233 | 'labs/2/tweets': ('GET', 'api'), 234 | 'labs/2/tweets/search': ('GET', 'api'), 235 | 'labs/2/tweets/:PARAM/hidden': ('PUT', 'api'), # TWEET ID 236 | 'labs/2/users/:PARAM': ('GET', 'api'), # USER ID 237 | 'labs/2/users': ('GET', 'api'), 238 | 239 | # !!!!!!!!!!!!!!!!!!! 240 | # API V2 EARLY ACCESS 241 | # !!!!!!!!!!!!!!!!!!! 242 | 243 | 'compliance/jobs': (['GET','POST'], 'api'), 244 | 'compliance/jobs/:PARAM': ('GET', 'api'), # ID 245 | 246 | 'lists': ('POST', 'api'), 247 | 'lists/:PARAM': (['GET','PUT','DELETE'], 'api'), # ID 248 | 'lists/:PARAM/followers': ('GET', 'api'), 249 | 'lists/:PARAM/members': (['GET','POST'], 'api'), # ID 250 | 'lists/:PARAM/members/:PARAM': ('DELETE', 'api'), # ID, USER ID 251 | 'lists/:PARAM/tweets': ('GET', 'api'), # ID 252 | 253 | 'spaces': ('GET', 'api'), 254 | 'spaces/:PARAM': ('GET', 'api'), # ID 255 | 'spaces/:PARAM/buyers': ('GET', 'api'), # ID 256 | 'spaces/by/creator_ids': ('GET', 'api'), 257 | 'spaces/search': ('GET', 'api'), 258 | 259 | 'tweets': (['GET','POST'], 'api'), 260 | 'tweets/:PARAM': (['GET','DELETE'], 'api'), # ID 261 | 'tweets/:PARAM/hidden': ('PUT', 'api'), # ID 262 | 'tweets/:PARAM/liking_users': ('GET', 'api'), # ID 263 | 'tweets/:PARAM/retweeted_by': ('GET', 'api'), # ID 264 | 'tweets/counts/all': ('GET', 'api'), 265 | 'tweets/counts/recent': ('GET', 'api'), 266 | 'tweets/sample/stream': ('GET', 'api'), 267 | 'tweets/search/all': ('GET', 'api'), 268 | 'tweets/search/recent': ('GET', 'api'), 269 | 'tweets/search/stream': ('GET', 'api'), 270 | 'tweets/search/stream/rules': (['POST','GET'], 'api'), 271 | 272 | 'users': ('GET', 'api'), 273 | 'users/:PARAM': ('GET', 'api'), # ID 274 | 'users/:PARAM/blocking': (['GET','POST'], 'api'), # ID 275 | 'users/:PARAM/bookmarks': (['GET','DELETE'], 'api'), # ID 276 | 'users/:PARAM/followed_lists': (['GET','POST'], 'api'), # ID 277 | 'users/:PARAM/followed_lists/:PARAM': ('DELETE', 'api'), # ID, LIST ID 278 | 'users/:PARAM/followers': ('GET', 'api'), # ID 279 | 'users/:PARAM/following': (['GET','POST'], 'api'), # ID 280 | 'users/:PARAM/following/:PARAM': ('DELETE', 'api'), # USER SOURCE ID, USER TARGET ID 281 | 'users/:PARAM/liked_tweets': ('GET', 'api'), # ID 282 | 'users/:PARAM/likes': ('POST', 'api'), # ID 283 | 'users/:PARAM/likes/:PARAM': ('DELETE', 'api'), # USER ID, TWEET ID 284 | 'users/:PARAM/liked_tweets': ('GET', 'api'), # ID 285 | 'users/:PARAM/list_memberships': ('GET', 'api'), # ID 286 | 'users/:PARAM/mentions': ('GET', 'api'), # ID 287 | 'users/:PARAM/muting': ('GET', 'api'), # ID 288 | 'users/:PARAM/owned_lists': ('GET', 'api'), # ID 289 | 'users/:PARAM/pinned_lists': (['GET','POST'], 'api'), # ID 290 | 'users/:PARAM/pinned_lists/:PARAM': ('DELETE', 'api'), # ID, LIST ID 291 | 'users/:PARAM/retweets': ('POST', 'api'), # ID 292 | 'users/:PARAM/retweets/:PARAM': ('DELETE', 'api'), # ID, SOURCE TWEET ID 293 | 'users/:PARAM/tweets': ('GET', 'api'), # ID 294 | 'users/:PARAM/blocking/:PARAM': ('DELETE', 'api'), # SOURCE USER ID, TARGET USER ID 295 | 'users/:PARAM/following/:PARAM': ('DELETE', 'api'), # SOURCE USER ID, TARGET USER ID 296 | 'users/:PARAM/muting/:PARAM': ('DELETE', 'api'), # SOURCE USER ID, TARGET USER ID 297 | 'users/by': ('GET', 'api'), 298 | 'users/by/username/:PARAM': ('GET', 'api'), # USERNAME 299 | } 300 | -------------------------------------------------------------------------------- /TwitterAPI/credentials.txt: -------------------------------------------------------------------------------- 1 | consumer_key= 2 | consumer_secret= 3 | access_token_key= 4 | access_token_secret= 5 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TwitterAPI.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TwitterAPI.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/TwitterAPI" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TwitterAPI" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | Instructions for Generating Documentation with Sphinx 2 | ===================================================== 3 | 4 | INSTALL SPHINX:: 5 | 6 | pip3 install -U Sphinx 7 | 8 | MAKE NEW DOCS IN MASTER BRANCH:: 9 | 10 | git clone git@github.com:geduldig/TwitterAPI.git 11 | cd TwitterAPI/docs 12 | vi conf.py (modify version and release) 13 | make html 14 | 15 | TEST DOC WEB PAGE:: 16 | 17 | TwitterAPI/docs/_build/html/index.html 18 | 19 | DOWNLOAD OLD DOCS FROM GH-PAGES BRANCH:: 20 | 21 | git clone -b gh-pages git@github.com:geduldig/TwitterAPI.git 22 | 23 | COPY NEW DOCS OVER OLD DOCS:: 24 | 25 | cp ./_build/html/*.* ./TwitterAPI 26 | cp -r ./_build/html/* ./TwitterAPI 27 | rm -r ./_build 28 | 29 | UPLOAD NEW DOCS TO GH-PAGES BRANCH::: 30 | 31 | cd TwitterAPI 32 | git commit -am "2.5 docs" 33 | git push origin gh-pages 34 | 35 | CLEAN UP:: 36 | cd .. 37 | rm -r TwitterAPI -------------------------------------------------------------------------------- /docs/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | Before you can do anything you must create an application on `apps.twitter.com `_ and generate oAuth keys. 5 | 6 | Twitter supports both user and application authentication, called oAuth 1 and oAuth 2, respectively. User authentication gives you access to all API endpoints, basically read and write persmission. It is also required in order to using the Streaming API. Application authentication gives you access to just the read portion of the API -- so, no creating or destroying tweets. Application authentication, however, has elevated rate limits. 7 | 8 | User Authentication 9 | ------------------- 10 | 11 | .. code-block:: python 12 | 13 | api = TwitterAPI(consumer_key, 14 | consumer_secret, 15 | access_token_key, 16 | access_token_secret) 17 | 18 | Application Authentication 19 | -------------------------- 20 | 21 | .. code-block:: python 22 | 23 | api = TwitterAPI(consumer_key, 24 | consumer_secret, 25 | auth_type='oAuth2') 26 | 27 | Proxy Server Authentication 28 | --------------------------- 29 | 30 | If you are behind a firewall, you may also need to authenticate with a web proxy server in order to reach Twitter's servers. For this situation you include an additional argument in the initializer: 31 | 32 | .. code-block:: python 33 | 34 | api = TwitterAPI(consumer_key, 35 | consumer_secret, 36 | access_token_key, 37 | access_token_secret, 38 | proxy_url='https://USER:PASSWORD@SERVER:PORT') 39 | 40 | Replace SERVER:PORT with your proxy server, and replace USER and PASSWORD with your proxy server credentials. -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # TwitterAPI documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Sep 27 12:02:24 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, '../') 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | "sphinx.ext.intersphinx", 34 | "sphinx.ext.mathjax", 35 | "sphinx.ext.viewcode", 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'TwitterAPI' 52 | copyright = u'2021, geduldig' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = '2.7' 60 | # The full version, including alpha/beta/rc tags. 61 | release = '2.7' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = ['_build'] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all 78 | # documents. 79 | #default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | #add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | #add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | #show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | #modindex_common_prefix = [] 97 | 98 | # If true, keep warnings as "system message" paragraphs in the built documents. 99 | #keep_warnings = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | html_theme = 'haiku' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | #html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | #html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | #html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | #html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | #html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | #html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # Add any extra paths that contain custom files (such as robots.txt or 138 | # .htaccess) here, relative to this directory. These files are copied 139 | # directly to the root of the documentation. 140 | #html_extra_path = [] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | #html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | #html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | #html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | #html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | #html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | #html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | #html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | #html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | #html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | #html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | #html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | #html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'TwitterAPIdoc' 185 | 186 | 187 | # -- Options for LaTeX output --------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | #'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, 202 | # author, documentclass [howto/manual]). 203 | latex_documents = [ 204 | ('index', 'TwitterAPI.tex', u'TwitterAPI Documentation', 205 | u'geduldig', 'manual'), 206 | ] 207 | 208 | # The name of an image file (relative to this directory) to place at the top of 209 | # the title page. 210 | #latex_logo = None 211 | 212 | # For "manual" documents, if this is true, then toplevel headings are parts, 213 | # not chapters. 214 | #latex_use_parts = False 215 | 216 | # If true, show page references after internal links. 217 | #latex_show_pagerefs = False 218 | 219 | # If true, show URL addresses after external links. 220 | #latex_show_urls = False 221 | 222 | # Documents to append as an appendix to all manuals. 223 | #latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | #latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output --------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [ 234 | ('index', 'twitterapi', u'TwitterAPI Documentation', 235 | [u'geduldig'], 1) 236 | ] 237 | 238 | # If true, show URL addresses after external links. 239 | #man_show_urls = False 240 | 241 | 242 | # -- Options for Texinfo output ------------------------------------------- 243 | 244 | # Grouping the document tree into Texinfo files. List of tuples 245 | # (source start file, target name, title, author, 246 | # dir menu entry, description, category) 247 | texinfo_documents = [ 248 | ('index', 'TwitterAPI', u'TwitterAPI Documentation', 249 | u'geduldig', 'TwitterAPI', 'One line description of project.', 250 | 'Miscellaneous'), 251 | ] 252 | 253 | # Documents to append as an appendix to all manuals. 254 | #texinfo_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | #texinfo_domain_indices = True 258 | 259 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 260 | #texinfo_show_urls = 'footnote' 261 | 262 | # If true, do not generate a @detailmenu in the "Top" node's menu. 263 | #texinfo_no_detailmenu = False 264 | 265 | html_show_sourcelink = False 266 | -------------------------------------------------------------------------------- /docs/errors.rst: -------------------------------------------------------------------------------- 1 | Error Handling 2 | ============== 3 | 4 | Besides tweet statuses, the REST API and Streaming API iterators can return errors and other messages. It is up to the application to test what type of object has been returned. Message types are documented `here `_ and `here `_. 5 | 6 | REST API Messages 7 | ----------------- 8 | 9 | REST API endpoints can return many more types of messages than Streaming API endpoints. Depending on the endpoint, you may want to handle a particular type of message, such as exceeding a rate limit or posting a duplicate tweet. Here is a general pattern for simply printing out any message and error code: 10 | 11 | .. code-block:: python 12 | 13 | r = api.request('search/tweets', {'q':'pizza'}) 14 | for item in r.get_iterator(): 15 | if 'text' in item: 16 | print item['text'] 17 | elif 'message' in item: 18 | print '%s (%d)' % (item['message'], item['code']) 19 | 20 | Streaming API Messages 21 | ---------------------- 22 | 23 | Streaming API endpoints return a variety of messages. Some are not errors. For example, a "limit" message contains the number of tweets missing from the stream. This happens when the number of tweets matching your filter exceeds a threshold set by Twitter. Other useful messages are "disconnect" and "delete". 24 | 25 | .. code-block:: python 26 | 27 | r = api.request('statuses/filter', {'track':'pizza'}) 28 | for item in r.get_iterator(): 29 | if 'text' in item: 30 | print item['text'] 31 | elif 'limit' in item: 32 | print '%d tweets missed' % item['limit']['track'] 33 | elif 'disconnect' in item: 34 | print 'disconnecting because %s' % item['disconnect']['reason'] 35 | break 36 | 37 | Even if you are not interested in handling errors it is necessary to test that the object returned by an iterator is a valid tweet status before using the object. Valid tweet objects have a 'text' property (or a 'full_text' property if it is an extended tweet). -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Tiny Examples 2 | ============= 3 | 4 | All the examples assume `api` is an authenticated instance of `TwitterAPI <./twitterapi.html>`_. Typically, this is done as follows: 5 | 6 | .. code-block:: python 7 | 8 | api = TwitterAPI(consumer_key, 9 | consumer_secret, 10 | access_token_key, 11 | access_token_secret) 12 | 13 | 14 | Get your last 50 tweets 15 | ----------------------- 16 | 17 | .. code-block:: python 18 | 19 | r = api.request('statuses/home_timeline', {'count':50}) 20 | for item in r.get_iterator(): 21 | if 'text' in item: 22 | print item['text'] 23 | 24 | Get your entire timeline 25 | ------------------------ 26 | 27 | .. code-block:: python 28 | 29 | pager = TwitterPager(api, 'statuses/home_timeline', {'count':200}) 30 | for item in pager.get_iterator(wait=60): 31 | if 'text' in item: 32 | print item['text'] 33 | 34 | Post a tweet 35 | ------------ 36 | 37 | .. code-block:: python 38 | 39 | r = api.request('statuses/update', {'status': 'I need pizza!'}) 40 | print 'SUCCESS' if r.status_code == 200 else 'FAILURE' 41 | 42 | Post a tweet with a picture 43 | --------------------------- 44 | 45 | .. code-block:: python 46 | 47 | # STEP 1 - upload image 48 | file = open('./image_of_pizza.png', 'rb') 49 | data = file.read() 50 | r = api.request('media/upload', None, {'media': data}) 51 | print('UPLOAD MEDIA SUCCESS' if r.status_code == 200 else 'UPLOAD MEDIA FAILURE') 52 | 53 | # STEP 2 - post tweet with reference to uploaded image 54 | if r.status_code == 200: 55 | media_id = r.json()['media_id'] 56 | r = api.request('statuses/update', {'status':'I found pizza!', 'media_ids':media_id}) 57 | print('UPDATE STATUS SUCCESS' if r.status_code == 200 else 'UPDATE STATUS FAILURE') 58 | 59 | Delete a tweet 60 | -------------- 61 | 62 | .. code-block:: python 63 | 64 | r = api.request('statuses/destroy/:%d' % TWEET_ID) 65 | print 'SUCCESS' if r.status_code == 200 else 'FAILURE' 66 | 67 | 68 | Stream tweets 69 | ------------- 70 | 71 | .. code-block:: python 72 | 73 | r = api.request('statuses/filter', {'track':'pizza'}) 74 | for item in r.get_iterator(): 75 | if 'text' in item: 76 | print item['text'] 77 | -------------------------------------------------------------------------------- /docs/faulttolerance.rst: -------------------------------------------------------------------------------- 1 | Fault Tolerant Streams and Pages 2 | ================================ 3 | 4 | There are a number of reasons for a stream to stop. Twitter will break your connection if you have more than two streams open with the same credentials, or if your credentials are not valid. Occassionally, the problem will be internal to Twitter and you will be disconnected. Other causes might be network instability or connection timeout. 5 | 6 | Endless Stream 7 | -------------- 8 | 9 | In order to keep a Streaming API request going indefinitely, you will need to re-make the request whenever the connection drops. TwitterAPI defines two exception classes for this purpose. 10 | 11 | `TwitterRequestError <./twittererror.html>`_ is thrown whenever the request fails (i.e. when the response status code is not 200). A status code of 500 or higher indicates a server error which is safe to ignore. Any other status code indicates an error with your request which you should fix before re-trying. 12 | 13 | `TwitterConnectionError <./twittererror.html>`_ is thrown when the connection times out or is interrupted. You can always immediately try making the request again. 14 | 15 | Sometimes Twitter will inform you to close the connection by sending you a "disconnect" message. The message will contain a code which indicates the reason. Messages with a code of 2, 5, 6, or 7 are serious and you will need to fix the problem before making a new request. You can ignore all other messages. 16 | 17 | .. code-block:: python 18 | 19 | while True: 20 | try: 21 | iterator = api.request('statuses/filter', {'track':'pizza'}).get_iterator() 22 | for item in iterator: 23 | if 'text' in item: 24 | print(item['text']) 25 | elif 'disconnect' in item: 26 | event = item['disconnect'] 27 | if event['code'] in [2,5,6,7]: 28 | # something needs to be fixed before re-connecting 29 | raise Exception(event['reason']) 30 | else: 31 | # temporary interruption, re-try request 32 | break 33 | except TwitterRequestError as e: 34 | if e.status_code < 500: 35 | # something needs to be fixed before re-connecting 36 | raise 37 | else: 38 | # temporary interruption, re-try request 39 | pass 40 | except TwitterConnectionError: 41 | # temporary interruption, re-try request 42 | pass 43 | 44 | Last Week's Pages 45 | ----------------- 46 | 47 | Requests for REST API endpoints can throw `TwitterRequestError <./twittererror.html>`_ and `TwitterConnectionError <./twittererror.html>`_. They do not, however, return "disconnect" messages. Twitter returns error messages for these endpoints with "message". Most of these errors require attention before re-trying the request, except those with codes of 130 or 131, which are internal server errors. 48 | 49 | For making continuos REST API requests (i.e. paging), TwitterAPI provides `TwitterPager <./paging.html>`_. If you use this class to request tweets that have been posted back to one week old, for example, the class's iterator will handle both types of exceptions automatically. The iterator also handles "message" objects with 130 or 131 codes for you. Any other "message" object gets passed on for you to handle. 50 | 51 | One final consideration is the endpoint's rate limit, determinted by the endpoint and whether you authenticate with oAuth 1 or oAuth 2. By default, the iterator waits 5 seconds between requests. This is sufficient for 180 requests in 15 minutes, the rate limit for "search/tweets" with oAuth 1. You can do better with oAuth 2. It permits 450 requests every 15 minutes, or 1 request per 2 seconds. The example below sets the wait assuming oAuth 2 rate limits. 52 | 53 | .. code-block:: python 54 | 55 | iterator = TwitterPager(api, 'search/tweets', {'q':'pizza'}).get_iterator(wait=2) 56 | for item in iterator: 57 | if 'text' in item: 58 | print(item['text']) 59 | elif 'message' in item: 60 | # something needs to be fixed before re-connecting 61 | raise Exception(item['message']) -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | *Minimal Python wrapper for Twitter's REST and Streaming APIs* 5 | 6 | The principle behind TwitterAPI's design is to provide a single method for accessing the Twitter API. The ``request`` method allows one to call *any* endpoint found on Twitter's `developer site `_, the complete reference for all endpoints. The benefits of a single-method approach are: 1) less code for me to maintain, and 2) a single method for you to learn. Here is a quck example: 7 | 8 | .. code-block:: python 9 | 10 | from TwitterAPI import TwitterAPI 11 | api = TwitterAPI(consumer_key, consumer_secret, access_token_key, access_token_secret) 12 | r = api.request('search/tweets', {'q':'pizza'}) 13 | print r.status_code 14 | 15 | Get Twitter's entire response as one long string containing tweets (in this example) by calling ``r.text``. More often an iterator is useful: 16 | 17 | .. code-block:: python 18 | 19 | for item in r.get_iterator(): 20 | print item['user']['screen_name'], item['text'] 21 | 22 | The iterator returns JSON objects. What makes the iterator very powerful is it works with both REST API and Streaming API endpoints. No syntax changes required -- supply any endpoint and parameters that are found on Twitter's developer site. 23 | 24 | TwitterAPI is compatible with Python 2 and Python 3. It authenticates using either OAauth 1 or OAuth 2. It also supports web proxy server authentication. All this with minimal code change for you. 25 | 26 | Topics 27 | ====== 28 | 29 | .. toctree:: 30 | :maxdepth: 1 31 | 32 | authentication.rst 33 | paging.rst 34 | errors.rst 35 | faulttolerance.rst 36 | examples.rst 37 | 38 | Modules 39 | ======= 40 | 41 | .. toctree:: 42 | :maxdepth: 2 43 | 44 | twitterapi.rst 45 | twittererror.rst 46 | 47 | Optional: 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | 52 | twitterpager.rst 53 | twitteroauth.rst 54 | -------------------------------------------------------------------------------- /docs/paging.rst: -------------------------------------------------------------------------------- 1 | Paging Results 2 | ============== 3 | 4 | Whether one is searching for tweets with `search/tweets` or downloading a user's timeline with `statuses/user_timeline` Twitter limits the number of tweets. So, in order to get more tweets, one must make successive requests and with each request skip the previously acquired tweets. This is done by specifying the tweet id from where to start. Twitter has a description `here `_. If you don't want to implement paging yourself, you can use the `TwitterPager <./twitterpager.html>`_ helper class with any REST API endpoint that returns multiples of something. The following, for example, searches for all tweets containing 'pizza' that Twitter has stored -- up to about a week's worth maximum. 5 | 6 | .. code-block:: python 7 | 8 | r = TwitterPager(api, 'search/tweets', {'q':'pizza', 'count':100}) 9 | for item in r.get_iterator(): 10 | if 'text' in item: 11 | print item['text'] 12 | elif 'message' in item and item['code'] == 88: 13 | print 'SUSPEND, RATE LIMIT EXCEEDED: %s\n' % item['message'] 14 | break 15 | 16 | By default there is a built-in wait time of 5 seconds between successive calls. This value is overridden with an argument to `get_iterator()`. See the documentation also to learn how to wait for new tweets. In other words, the iterator can be setup to poll for newer pages of tweets rather than older pages. -------------------------------------------------------------------------------- /docs/twitterapi.rst: -------------------------------------------------------------------------------- 1 | TwitterAPI.TwitterAPI 2 | ===================== 3 | 4 | .. automodule:: TwitterAPI.TwitterAPI 5 | 6 | .. autoclass:: TwitterAPI 7 | :members: 8 | :undoc-members: 9 | 10 | .. autoclass:: TwitterResponse 11 | :members: 12 | :undoc-members: 13 | -------------------------------------------------------------------------------- /docs/twittererror.rst: -------------------------------------------------------------------------------- 1 | TwitterAPI.TwitterError 2 | =========================== 3 | 4 | .. automodule:: TwitterAPI.TwitterError 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/twitteroauth.rst: -------------------------------------------------------------------------------- 1 | TwitterAPI.TwitterOAuth 2 | ======================= 3 | 4 | .. automodule:: TwitterAPI.TwitterOAuth 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/twitterpager.rst: -------------------------------------------------------------------------------- 1 | TwitterAPI.TwitterPager 2 | =========================== 3 | 4 | .. automodule:: TwitterAPI.TwitterPager 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /examples/cli/README.rst: -------------------------------------------------------------------------------- 1 | Command-Line Utility 2 | -------------------- 3 | For syntax help:: 4 | 5 | python cli.py -h 6 | 7 | You will need to supply your Twitter application OAuth credentials. The easiest option is to save them in TwitterAPI/credentials.txt. It is the default place where cli.py will look for them. You also may supply an alternative credentials file as a command-line argument. 8 | 9 | Call any REST API endpoint:: 10 | 11 | python cli.py -endpoint statuses/update -parameters status='my tweet' 12 | 13 | Another example (here using abbreviated option names) that parses selected output fields:: 14 | 15 | python cli.py -e search/tweets -p q=zzz count=10 -field screen_name text 16 | 17 | Calling any Streaming API endpoint works too:: 18 | 19 | python cli.py -e statuses/filter -p track=zzz -f screen_name text 20 | 21 | After the ``-field`` option you must supply one or more key names from the raw JSON response object. This will print values only for these keys. When the ``-field`` option is omitted cli.py prints the entire JSON response object. 22 | -------------------------------------------------------------------------------- /examples/cli/Unicode_win32.py: -------------------------------------------------------------------------------- 1 | """ Stdout, stdin, stderr and argv support for unicode. """ 2 | ############################################## 3 | # Support for unicode in windows cmd.exe 4 | # Posted on Stack Overflow [1], available under CC-BY-SA [2] 5 | # 6 | # Question: "Windows cmd encoding change causes Python crash" [3] by Alex [4], 7 | # Answered [5] by David-Sarah Hopwood [6]. 8 | # 9 | # [1] https://stackoverflow.com 10 | # [2] https://creativecommons.org/licenses/by-sa/3.0/ 11 | # [3] https://stackoverflow.com/questions/878972 12 | # [4] https://stackoverflow.com/users/85185 13 | # [4] https://stackoverflow.com/a/3259271/118671 14 | # [5] https://stackoverflow.com/users/393146 15 | # 16 | ############################################## 17 | 18 | from __future__ import print_function 19 | import sys 20 | stdin = sys.stdin 21 | stdout = sys.stdout 22 | stderr = sys.stderr 23 | argv = sys.argv 24 | 25 | if sys.version_info[0] > 2: 26 | unicode = str 27 | 28 | if sys.platform == "win32": 29 | import codecs 30 | from ctypes import WINFUNCTYPE, windll, POINTER 31 | from ctypes import byref, c_int, create_unicode_buffer 32 | from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR 33 | try: 34 | from ctypes.wintypes import LPVOID 35 | except ImportError: 36 | from ctypes import c_void_p as LPVOID 37 | 38 | original_stderr = sys.stderr 39 | 40 | # If any exception occurs in this code, we'll probably try to print it on stderr, 41 | # which makes for frustrating debugging if stderr is directed to our wrapper. 42 | # So be paranoid about catching errors and reporting them to original_stderr, 43 | # so that we can at least see them. 44 | def _complain(message): 45 | print(isinstance(message, str) and message or repr(message), file=original_stderr) 46 | 47 | # Work around . 48 | codecs.register(lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None) 49 | 50 | # Make Unicode console output work independently of the current code page. 51 | # This also fixes . 52 | # Credit to Michael Kaplan 53 | # and TZOmegaTZIOY 54 | # . 55 | try: 56 | # 57 | # HANDLE WINAPI GetStdHandle(DWORD nStdHandle); 58 | # returns INVALID_HANDLE_VALUE, NULL, or a valid handle 59 | # 60 | # 61 | # DWORD WINAPI GetFileType(DWORD hFile); 62 | # 63 | # 64 | # BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode); 65 | 66 | GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32)) 67 | STD_INPUT_HANDLE = DWORD(-10) 68 | STD_OUTPUT_HANDLE = DWORD(-11) 69 | STD_ERROR_HANDLE = DWORD(-12) 70 | GetFileType = WINFUNCTYPE(DWORD, DWORD)(("GetFileType", windll.kernel32)) 71 | FILE_TYPE_CHAR = 0x0002 72 | FILE_TYPE_REMOTE = 0x8000 73 | GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(("GetConsoleMode", windll.kernel32)) 74 | INVALID_HANDLE_VALUE = DWORD(-1).value 75 | 76 | def not_a_console(handle): 77 | if handle == INVALID_HANDLE_VALUE or handle is None: 78 | return True 79 | return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR 80 | or GetConsoleMode(handle, byref(DWORD())) == 0) 81 | 82 | old_stdin_fileno = None 83 | old_stdout_fileno = None 84 | old_stderr_fileno = None 85 | 86 | if hasattr(sys.stdin, 'fileno'): 87 | old_stdin_fileno = sys.stdin.fileno() 88 | if hasattr(sys.stdout, 'fileno'): 89 | old_stdout_fileno = sys.stdout.fileno() 90 | if hasattr(sys.stderr, 'fileno'): 91 | old_stderr_fileno = sys.stderr.fileno() 92 | 93 | STDIN_FILENO = 0 94 | STDOUT_FILENO = 1 95 | STDERR_FILENO = 2 96 | real_stdin = (old_stdin_fileno == STDIN_FILENO) 97 | real_stdout = (old_stdout_fileno == STDOUT_FILENO) 98 | real_stderr = (old_stderr_fileno == STDERR_FILENO) 99 | 100 | if real_stdin: 101 | hStdin = GetStdHandle(STD_INPUT_HANDLE) 102 | if not_a_console(hStdin): 103 | real_stdin = False 104 | 105 | if real_stdout: 106 | hStdout = GetStdHandle(STD_OUTPUT_HANDLE) 107 | if not_a_console(hStdout): 108 | real_stdout = False 109 | 110 | if real_stderr: 111 | hStderr = GetStdHandle(STD_ERROR_HANDLE) 112 | if not_a_console(hStderr): 113 | real_stderr = False 114 | 115 | if real_stdin: 116 | ReadConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPVOID, DWORD, POINTER(DWORD), 117 | LPVOID)(("ReadConsoleW", windll.kernel32)) 118 | 119 | class UnicodeInput: 120 | 121 | """Unicode terminal input class.""" 122 | 123 | def __init__(self, hConsole, name, bufsize=1024): 124 | self._hConsole = hConsole 125 | self.bufsize = bufsize 126 | self.buffer = create_unicode_buffer(bufsize) 127 | self.name = name 128 | self.encoding = 'utf-8' 129 | 130 | def readline(self): 131 | maxnum = DWORD(self.bufsize - 1) 132 | numrecv = DWORD(0) 133 | result = ReadConsoleW(self._hConsole, self.buffer, maxnum, byref(numrecv), None) 134 | if not result: 135 | raise Exception("stdin failure") 136 | return self.buffer.value[:numrecv.value].encode(self.encoding) 137 | 138 | if real_stdout or real_stderr: 139 | # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars, 140 | # LPDWORD lpCharsWritten, LPVOID lpReserved); 141 | 142 | WriteConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), 143 | LPVOID)(("WriteConsoleW", windll.kernel32)) 144 | 145 | class UnicodeOutput: 146 | 147 | """Unicode terminal output class.""" 148 | 149 | def __init__(self, hConsole, stream, fileno, name): 150 | self._hConsole = hConsole 151 | self._stream = stream 152 | self._fileno = fileno 153 | self.closed = False 154 | self.softspace = False 155 | self.mode = 'w' 156 | self.encoding = 'utf-8' 157 | self.name = name 158 | self.flush() 159 | 160 | def isatty(self): 161 | return False 162 | 163 | def close(self): 164 | # don't really close the handle, that would only cause problems 165 | self.closed = True 166 | 167 | def fileno(self): 168 | return self._fileno 169 | 170 | def flush(self): 171 | if self._hConsole is None: 172 | try: 173 | self._stream.flush() 174 | except Exception as e: 175 | _complain("%s.flush: %r from %r" 176 | % (self.name, e, self._stream)) 177 | raise 178 | 179 | def write(self, text): 180 | try: 181 | if self._hConsole is None: 182 | if isinstance(text, unicode): 183 | text = text.encode('utf-8') 184 | self._stream.write(text) 185 | else: 186 | if not isinstance(text, unicode): 187 | text = bytes(text).decode('utf-8') 188 | remaining = len(text) 189 | while remaining > 0: 190 | n = DWORD(0) 191 | # There is a shorter-than-documented limitation on the 192 | # length of the string passed to WriteConsoleW (see 193 | # . 194 | retval = WriteConsoleW(self._hConsole, text, 195 | min(remaining, 10000), 196 | byref(n), None) 197 | if retval == 0 or n.value == 0: 198 | raise IOError("WriteConsoleW returned %r, n.value = %r" 199 | % (retval, n.value)) 200 | remaining -= n.value 201 | if remaining == 0: 202 | break 203 | text = text[n.value:] 204 | except Exception as e: 205 | _complain("%s.write: %r" % (self.name, e)) 206 | raise 207 | 208 | def writelines(self, lines): 209 | try: 210 | for line in lines: 211 | self.write(line) 212 | except Exception as e: 213 | _complain("%s.writelines: %r" % (self.name, e)) 214 | raise 215 | 216 | if real_stdin: 217 | stdin = UnicodeInput(hStdin, name='') 218 | 219 | if real_stdout: 220 | stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, 221 | '') 222 | else: 223 | stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, 224 | '') 225 | 226 | if real_stderr: 227 | stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, 228 | '') 229 | else: 230 | stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, 231 | '') 232 | except Exception as e: 233 | _complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,)) 234 | 235 | # While we're at it, let's unmangle the command-line arguments: 236 | 237 | # This works around . 238 | GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) 239 | CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(("CommandLineToArgvW", windll.shell32)) 240 | 241 | argc = c_int(0) 242 | argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) 243 | 244 | argv = [argv_unicode[i].encode('utf-8') for i in range(0, argc.value)] 245 | 246 | if not hasattr(sys, 'frozen'): 247 | # If this is an executable produced by py2exe or bbfreeze, then it will 248 | # have been invoked directly. Otherwise, unicode_argv[0] is the Python 249 | # interpreter, so skip that. 250 | argv = argv[1:] 251 | 252 | # Also skip option arguments to the Python interpreter. 253 | while len(argv) > 0: 254 | arg = argv[0] 255 | if not arg.startswith(b"-") or arg == u"-": 256 | break 257 | argv = argv[1:] 258 | if arg == u'-m': 259 | # sys.argv[0] should really be the absolute path of the module source, 260 | # but never mind 261 | break 262 | if arg == u'-c': 263 | argv[0] = u'-c' 264 | break 265 | 266 | if argv == []: 267 | argv = [u''] 268 | -------------------------------------------------------------------------------- /examples/cli/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Command-Line Interface to Twitter's REST API and Streaming API. 3 | ----------------------------------------------------------------- 4 | 5 | Run this command line script with any Twitter endpoint. The json-formatted 6 | response is printed to the console. The script works with both Streaming API and 7 | REST API endpoints. 8 | 9 | IMPORTANT: Before using this script, you must enter your Twitter application's OAuth 10 | credentials in TwitterAPI/credentials.txt. Log into http://dev.twitter.com to create 11 | your application. 12 | 13 | Examples: 14 | 15 | :: 16 | 17 | python cli -endpoint search/tweets -parameters q=zzz 18 | python cli -endpoint statuses/filter -parameters track=zzz 19 | 20 | These examples print the raw json response. You can also print one or more fields 21 | from the response, for instance the tweet 'text' field, like this: 22 | 23 | :: 24 | 25 | python cli -endpoint statuses/filter -parameters track=zzz -fields text 26 | 27 | Documentation for all Twitter endpoints is located at: 28 | https://dev.twitter.com/docs/api/1.1 29 | """ 30 | 31 | 32 | __author__ = "Jonas Geduldig" 33 | __date__ = "June 7, 2013" 34 | __license__ = "MIT" 35 | 36 | 37 | from TwitterAPI import TwitterAPI, TwitterRequestError, TwitterConnectionError, __version__ 38 | from TwitterAPI.TwitterOAuth import TwitterOAuth 39 | import argparse 40 | import codecs 41 | import json 42 | import sys 43 | 44 | 45 | DEFAULT_API_VERSION = '1.1' 46 | 47 | 48 | # print UTF-8 to the console 49 | if sys.platform == "win32": 50 | from Unicode_win32 import stdout 51 | sys.stdout = stdout 52 | 53 | 54 | def _search(name, obj): 55 | """Breadth-first search for name in the JSON response and return value.""" 56 | q = [] 57 | q.append(obj) 58 | while q: 59 | obj = q.pop(0) 60 | if hasattr(obj, '__iter__') and type(obj) is not str: 61 | isdict = isinstance(obj, dict) 62 | if isdict and name in obj: 63 | return obj[name] 64 | for k in obj: 65 | q.append(obj[k] if isdict else k) 66 | else: 67 | return None 68 | 69 | 70 | def _to_dict(param_list): 71 | """Convert a list of key=value to dict[key]=value""" 72 | if param_list: 73 | return { 74 | name: value for (name, value) in 75 | [param.split('=') for param in param_list]} 76 | else: 77 | return None 78 | 79 | 80 | if __name__ == '__main__': 81 | print('TwitterAPI %s by Jonas Geduldig' % __version__) 82 | 83 | parser = argparse.ArgumentParser( 84 | description='Request any Twitter Streaming or REST API endpoint') 85 | parser.add_argument( 86 | '-oauth', 87 | metavar='FILENAME', 88 | type=str, 89 | help='file containing OAuth credentials') 90 | parser.add_argument( 91 | '-bearertoken', 92 | metavar='BEARERTOKEN', 93 | type=eval, 94 | choices=[True, False], 95 | default=False, 96 | help='use OAuth 2.0') 97 | parser.add_argument( 98 | '-endpoint', 99 | metavar='ENDPOINT', 100 | type=str, 101 | help='Twitter endpoint', 102 | required=True) 103 | parser.add_argument( 104 | '-parameters', 105 | metavar='NAME_VALUE', 106 | type=str, 107 | help='parameter NAME=VALUE', 108 | nargs='+') 109 | parser.add_argument( 110 | '-fields', 111 | metavar='NAME', 112 | type=str, 113 | help='print a top-level field in the json response', 114 | nargs='+') 115 | parser.add_argument( 116 | '-indent', 117 | metavar='SPACES', 118 | type=int, 119 | help='number of spaces to indent json output', 120 | default=None) 121 | parser.add_argument( 122 | '-version', 123 | metavar='VERSION', 124 | type=str, 125 | help='Twitter API version', 126 | default=DEFAULT_API_VERSION) 127 | parser.add_argument( 128 | '-methodoverride', 129 | metavar='METHOD', 130 | type=str, 131 | help='override default HTTP method', 132 | default=None) 133 | args = parser.parse_args() 134 | 135 | try: 136 | params = _to_dict(args.parameters) 137 | oauth = TwitterOAuth.read_file(args.oauth) 138 | auth_type = 'oAuth2' if args.bearertoken else 'oAuth1' 139 | 140 | api = TwitterAPI(oauth.consumer_key, 141 | oauth.consumer_secret, 142 | oauth.access_token_key, 143 | oauth.access_token_secret, 144 | api_version=args.version, 145 | auth_type=auth_type) 146 | response = api.request(args.endpoint, params, method_override=args.methodoverride) 147 | 148 | for item in response.get_iterator(): 149 | if not args.fields: 150 | print(json.dumps(item, ensure_ascii='False', indent=args.indent)) 151 | else: 152 | for name in args.fields: 153 | value = _search(name, item) 154 | if value: 155 | print('%s: %s' % (name, value)) 156 | 157 | except TwitterRequestError as e: 158 | print(e.status_code) 159 | for msg in iter(e): 160 | print(msg) 161 | 162 | except TwitterConnectionError as e: 163 | print(e) 164 | 165 | except KeyboardInterrupt: 166 | print('Terminated by user') 167 | 168 | except Exception as e: 169 | print('STOPPED: %s' % e) 170 | -------------------------------------------------------------------------------- /examples/v1.1/connect_using_login.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests_oauthlib import OAuth1 3 | from urlparse import parse_qs 4 | from TwitterAPI import TwitterAPI 5 | 6 | consumer_key = 7 | consumer_secret = 8 | 9 | # obtain request token 10 | oauth = OAuth1(consumer_key, consumer_secret) 11 | r = requests.post( 12 | url='https://api.twitter.com/oauth/request_token', 13 | auth=oauth) 14 | credentials = parse_qs(r.content) 15 | request_key = credentials.get('oauth_token')[0] 16 | request_secret = credentials.get('oauth_token_secret')[0] 17 | 18 | # obtain authorization from resource owner 19 | print( 20 | 'Go here to authorize:\n https://api.twitter.com/oauth/authorize?oauth_token=%s' % 21 | request_key) 22 | verifier = raw_input('Enter your authorization code: ') 23 | 24 | # obtain access token 25 | oauth = OAuth1(consumer_key, 26 | consumer_secret, 27 | request_key, 28 | request_secret, 29 | verifier=verifier) 30 | r = requests.post(url='https://api.twitter.com/oauth/access_token', auth=oauth) 31 | credentials = parse_qs(r.content) 32 | access_token_key = credentials.get('oauth_token')[0] 33 | access_token_secret = credentials.get('oauth_token_secret')[0] 34 | 35 | # access resource 36 | api = TwitterAPI(consumer_key, 37 | consumer_secret, 38 | access_token_key, 39 | access_token_secret) 40 | for item in api.request('statuses/filter', {'track': 'zzz'}): 41 | print(item['text']) -------------------------------------------------------------------------------- /examples/v1.1/connect_using_oauth.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | # If you are behind a firewall you may need to provide proxy server 4 | # authentication. 5 | proxy_url = None # Example: 'https://USERNAME:PASSWORD@PROXYSERVER:PORT' 6 | 7 | # Using OAuth 1.0 to authenticate you have access all Twitter endpoints. 8 | api = TwitterAPI(, 9 | , 10 | , 11 | , 12 | auth_type='oAuth1', 13 | proxy_url=proxy_url) 14 | # Using OAuth 2.0 to authenticate you lose access to user specific endpoints 15 | # (ex. statuses/update and statuses/filter), but you increase your rate limits. 16 | # api = TwitterAPI(, 17 | # , 18 | # auth_type='oAuth2', 19 | # proxy_url=proxy_url) 20 | 21 | r = api.request('application/rate_limit_status') 22 | 23 | # Print HTTP status code (=200 when no errors). 24 | print(r.status_code) 25 | 26 | # Print the raw response. 27 | print(r.text) 28 | 29 | # Parse the JSON response. 30 | j = r.response.json() 31 | print(j['resources']['search']) -------------------------------------------------------------------------------- /examples/v1.1/delete_last_tweet.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from TwitterAPI import TwitterAPI 3 | 4 | NUMBER_OF_TWEETS_TO_DELETE = 1 5 | 6 | api = TwitterAPI(, 7 | , 8 | , 9 | ) 10 | 11 | class DeleteTweet(Thread): 12 | 13 | def __init__(self, tweet_id, count): 14 | Thread.__init__(self) 15 | self.tweet_id = tweet_id 16 | self.count = count 17 | 18 | def run(self): 19 | r = api.request('statuses/destroy/:%d' % self.tweet_id) 20 | print(self.count if r.status_code == 200 else 'PROBLEM: ' + r.text) 21 | 22 | try: 23 | count = 0 24 | r = api.request( 25 | 'statuses/user_timeline', {'count': NUMBER_OF_TWEETS_TO_DELETE}) 26 | for item in r: 27 | if 'id' in item: 28 | count += 1 29 | tweet_id = item['id'] 30 | DeleteTweet(tweet_id, count).start() 31 | else: 32 | raise Exception(item) 33 | except Exception as e: 34 | print('Stopping: %s' % str(e)) -------------------------------------------------------------------------------- /examples/v1.1/direct_message.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | import json 3 | 4 | api = TwitterAPI(, 5 | , 6 | , 7 | ) 8 | 9 | user_id = 10 | message_text = 11 | 12 | event = { 13 | "event": { 14 | "type": "message_create", 15 | "message_create": { 16 | "target": { 17 | "recipient_id": user_id 18 | }, 19 | "message_data": { 20 | "text": message_text 21 | } 22 | } 23 | } 24 | } 25 | 26 | r = api.request('direct_messages/events/new', json.dumps(event)) 27 | print('SUCCESS' if r.status_code == 200 else 'PROBLEM: ' + r.text) -------------------------------------------------------------------------------- /examples/v1.1/dump_timeline.py: -------------------------------------------------------------------------------- 1 | # Print a user's timeline. This will get up to 3,200 tweets, which 2 | # is the maximum the Twitter API allows. 3 | 4 | from TwitterAPI import TwitterAPI, TwitterPager 5 | 6 | SCREEN_NAME = 'TheTweetOfGod' 7 | 8 | api = TwitterAPI(, 9 | , 10 | auth_type='oAuth2') 11 | 12 | pager = TwitterPager(api, 13 | 'statuses/user_timeline', 14 | {'screen_name':SCREEN_NAME, 'count':200}) 15 | 16 | count = 0 17 | for item in pager.get_iterator(wait=3.5): 18 | if 'text' in item: 19 | count = count + 1 20 | print(count, item['text']) 21 | elif 'message' in item: 22 | print(item['message']) 23 | break -------------------------------------------------------------------------------- /examples/v1.1/logging.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | import logging 3 | 4 | # SET UP LOGGING TO FILE AND TO CONSOLE 5 | formatter = logging.Formatter('%(levelname)s %(asctime)s %(message)s', 6 | '%m/%d/%Y %I:%M:%S %p') 7 | fh = logging.FileHandler('logging.log') 8 | fh.setFormatter(formatter) 9 | ch = logging.StreamHandler() 10 | ch.setFormatter(formatter) 11 | logger = logging.getLogger() 12 | logger.setLevel(logging.INFO) 13 | logger.addHandler(fh) 14 | logger.addHandler(ch) 15 | 16 | api = TwitterAPI(, 17 | , 18 | , 19 | ) 20 | 21 | logging.info('START SAMPLE STREAM') 22 | 23 | try: 24 | r = api.request('statuses/sample') 25 | for item in r: 26 | if 'text' in item: 27 | print(item['text']) 28 | elif: 29 | logging.info('NOT A TWEET: %s' % item.text) 30 | except Exception as e: 31 | logging.warning('STOPPING: %s' % str(e)) -------------------------------------------------------------------------------- /examples/v1.1/lookup_tweet.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | TWEET_ID = '964575983633252353' 4 | 5 | api = TwitterAPI(, 6 | , 7 | auth_type='oAuth2') 8 | 9 | r = api.request('statuses/show/:' + TWEET_ID) 10 | tweet = r.json() 11 | print(tweet['user']['screen_name'] + ':' + tweet['text'] if r.status_code == 200 12 | else 'PROBLEM: ' + r.text) -------------------------------------------------------------------------------- /examples/v1.1/lookup_user.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | SCREEN_NAME = 'TheTweetOfGod' 4 | 5 | api = TwitterAPI(, 6 | , 7 | auth_type='oAuth2') 8 | 9 | r = api.request('users/lookup', {'screen_name':SCREEN_NAME}) 10 | print(r.json()[0]['id'] if r.status_code == 200 else 'PROBLEM: ' + r.text) 11 | -------------------------------------------------------------------------------- /examples/v1.1/page_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterPager 2 | 3 | SEARCH_TERM = 'pizza' 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | pager = TwitterPager(api, 'search/tweets', {'q': SEARCH_TERM}) 11 | 12 | for item in pager.get_iterator(): 13 | print(item['text'] if 'text' in item else item) -------------------------------------------------------------------------------- /examples/v1.1/post_tweet.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | TWEET_TEXT = "Ce n'est pas un tweet tweet." 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | r = api.request('statuses/update', {'status': TWEET_TEXT}) 11 | print('SUCCESS' if r.status_code == 200 else 'PROBLEM: ' + r.text) -------------------------------------------------------------------------------- /examples/v1.1/premium_search.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | SEARCH_TERM = 'pizza' 4 | PRODUCT = '30day' 5 | LABEL = 'your label' 6 | 7 | api = TwitterAPI(, 8 | , 9 | , 10 | ) 11 | 12 | r = api.request('tweets/search/%s/:%s' % (PRODUCT, LABEL), 13 | {'query':SEARCH_TERM}) 14 | 15 | for item in r: 16 | print(item['text'] if 'text' in item else item) 17 | -------------------------------------------------------------------------------- /examples/v1.1/sample_frequency.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterConnectionError, TwitterRequestError 2 | import time 3 | 4 | api = TwitterAPI(, 5 | , 6 | , 7 | ) 8 | 9 | class Frequency: 10 | 11 | """Track tweet download statistics""" 12 | 13 | def __init__(self): 14 | self.interval = 5 # seconds 15 | self.total_count = 0 16 | self.total_start = time.time() 17 | self.interval_count = 0 18 | self.interval_start = self.total_start 19 | 20 | def update(self): 21 | self.interval_count += 1 22 | self.total_count += 1 23 | now = time.time() 24 | elapsed = now - self.interval_start 25 | if elapsed >= self.interval: 26 | # timestamp : tps : total cumulative tweets : average tps 27 | print('%s -- %d\t%d\t%d' % (time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)), 28 | int(self.interval_count / elapsed), 29 | self.total_count, 30 | int(self.total_count / (now - self.total_start)))) 31 | self.interval_start = now 32 | self.interval_count = 0 33 | 34 | freq = Frequency() 35 | 36 | # THIS DEMONSTRATES HOW TO HANDLE ALL TYPES OF STREAMING ERRORS. 37 | # SO, THIS IS APPLICAPLE TO 'statuses/filter' AS WELL. ONLY WHEN 38 | # APPROPRIATE, A DROPPED CONNECTION IS RE-ESTABLISHED. 39 | 40 | while True: 41 | try: 42 | r = api.request('statuses/sample') 43 | for item in r: 44 | if 'text' in item: 45 | freq.update() 46 | elif 'limit' in item: 47 | print('TWEETS SKIPPED: %s' % item['limit']['track']) 48 | elif 'warning' in item: 49 | print(item['warning']) 50 | elif 'disconnect' in item: 51 | event = item['disconnect'] 52 | if event['code'] in [2,5,6,7]: 53 | # streaming connection rejected 54 | raise Exception(event) 55 | print('RE-CONNECTING: %s' % event) 56 | break 57 | except TwitterRequestError as e: 58 | if e.status_code < 500: 59 | print('REQUEST FAILED: %s' % e) 60 | break 61 | except TwitterConnectionError: 62 | pass 63 | except KeyboardInterrupt: 64 | print('TERMINATED BY USER') 65 | break 66 | except Exception as e: 67 | print('STOPPED: %s %s' % (type(e), e)) 68 | break -------------------------------------------------------------------------------- /examples/v1.1/search_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | SEARCH_TERM = 'pizza' 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | r = api.request('search/tweets', {'q': SEARCH_TERM}) 11 | 12 | for item in r: 13 | print(item['text'] if 'text' in item else item) 14 | 15 | print('\nQUOTA: %s' % r.get_quota()) -------------------------------------------------------------------------------- /examples/v1.1/stream_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | TRACK_TERM = 'pizza' 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | r = api.request('statuses/filter', {'track': TRACK_TERM}) 11 | 12 | for item in r: 13 | print(item['text'] if 'text' in item else item) -------------------------------------------------------------------------------- /examples/v1.1/unfollow_friends.py: -------------------------------------------------------------------------------- 1 | # Unfollow friends that do not follow you. 2 | 3 | from TwitterAPI import TwitterAPI 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | friends = set(id for id in api.request('friends/ids')) 11 | followers = set(id for id in api.request('followers/ids')) 12 | unfollow = set(friends) - set(followers) 13 | 14 | for id in unfollow: 15 | r = api.request('friendships/destroy', {'user_id': id}) 16 | if r.status_code == 200: 17 | status = r.json() 18 | print 'unfollowed %s' % status['screen_name'] 19 | -------------------------------------------------------------------------------- /examples/v1.1/upload_image.py: -------------------------------------------------------------------------------- 1 | # Post an image with tweet. Requires two API calls. 2 | 3 | from TwitterAPI import TwitterAPI 4 | 5 | TWEET_TEXT = 'some tweet text' 6 | IMAGE_PATH = './some_image.png' 7 | 8 | api = TwitterAPI(, 9 | , 10 | , 11 | ) 12 | 13 | # STEP 1 - upload image 14 | file = open(IMAGE_PATH, 'rb') 15 | data = file.read() 16 | r = api.request('media/upload', None, {'media': data}) 17 | print('UPLOAD MEDIA SUCCESS' if r.status_code == 200 else 'UPLOAD MEDIA FAILURE: ' + r.text) 18 | 19 | # STEP 2 - post tweet with a reference to uploaded image 20 | if r.status_code == 200: 21 | media_id = r.json()['media_id'] 22 | r = api.request('statuses/update', {'status': TWEET_TEXT, 'media_ids': media_id}) 23 | print('UPDATE STATUS SUCCESS' if r.status_code == 200 else 'UPDATE STATUS FAILURE: ' + r.text) -------------------------------------------------------------------------------- /examples/v1.1/upload_tweet.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | TWEET_TEXT = 'some tweet text' 4 | 5 | api = TwitterAPI(, 6 | , 7 | , 8 | ) 9 | 10 | r = api.request('statuses/update', {'status':TWEET_TEXT}) 11 | print('SUCCESS' if r.status_code == 200 else 'PROBLEM: ' + r.text) -------------------------------------------------------------------------------- /examples/v1.1/upload_video.py: -------------------------------------------------------------------------------- 1 | # Twitter places several restrictions on uploaded video. If your upload fails 2 | # with error 324, your video probably violates these restrictions. 3 | # 4 | # Read about them here: 5 | # https://dev.twitter.com/rest/public/uploading-media#videorecs 6 | 7 | from TwitterAPI import TwitterAPI 8 | import os 9 | import sys 10 | 11 | VIDEO_FILENAME = 'test.mp4' 12 | TWEET_TEXT = 'Video upload test' 13 | 14 | api = TwitterAPI(, 15 | , 16 | , 17 | ) 18 | 19 | bytes_sent = 0 20 | total_bytes = os.path.getsize(VIDEO_FILENAME) 21 | file = open(VIDEO_FILENAME, 'rb') 22 | 23 | def check_status(r): 24 | # EXIT PROGRAM WITH ERROR MESSAGE 25 | if r.status_code < 200 or r.status_code > 299: 26 | print(r.status_code) 27 | print(r.text) 28 | sys.exit(0) 29 | 30 | r = api.request('media/upload', {'command':'INIT', 'media_type':'video/mp4', 'total_bytes':total_bytes}) 31 | check_status(r) 32 | 33 | media_id = r.json()['media_id'] 34 | segment_id = 0 35 | 36 | while bytes_sent < total_bytes: 37 | chunk = file.read(4*1024*1024) 38 | r = api.request('media/upload', {'command':'APPEND', 'media_id':media_id, 'segment_index':segment_id}, {'media':chunk}) 39 | check_status(r) 40 | segment_id = segment_id + 1 41 | bytes_sent = file.tell() 42 | print('[' + str(total_bytes) + ']', str(bytes_sent)) 43 | 44 | r = api.request('media/upload', {'command':'FINALIZE', 'media_id':media_id}) 45 | check_status(r) 46 | 47 | r = api.request('statuses/update', {'status':TWEET_TEXT, 'media_ids':media_id}) 48 | check_status(r) -------------------------------------------------------------------------------- /examples/v2/conversation_tree.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import (TwitterAPI, TwitterOAuth, TwitterRequestError, 2 | TwitterConnectionError, TwitterPager, HydrateType) 3 | 4 | 5 | # NOTE: If any of the conversation is over a week old then it will not get 6 | # returned unless you are using academic credentials. 7 | CONVERSATION_ID = '20' 8 | 9 | 10 | # 11 | # UTILITY CLASS 12 | # 13 | 14 | class TreeNode: 15 | """TreeNode is used to organize tweets as a tree structure""" 16 | 17 | def __init__(self, data): 18 | """data is a tweet's json object""" 19 | self.data = data 20 | self.children = [] 21 | self.replied_to_tweet = None 22 | if 'referenced_tweets' in self.data: 23 | for tweet in self.data['referenced_tweets']: 24 | if tweet['type'] == 'replied_to': 25 | self.replied_to_tweet = tweet['id'] 26 | break 27 | 28 | def id(self): 29 | """a node is identified by its tweet id""" 30 | return self.data['id'] 31 | 32 | def parent(self): 33 | """the reply-to tweet is the parent of the node""" 34 | return self.replied_to_tweet 35 | 36 | def find_parent_of(self, node): 37 | """append a node to the children of it's parent tweet""" 38 | if node.parent() == self.id(): 39 | self.children.append(node) 40 | return True 41 | for child in self.children: 42 | if child.find_parent_of(node): 43 | return True 44 | return False 45 | 46 | def print_tree(self, level): 47 | """level 0 is the root node, then incremented for subsequent generations""" 48 | created_at = self.data['created_at'] 49 | username = self.data['author_id']['username'] 50 | text_80chars = self.data['text'][0:80].replace('\n', ' ') 51 | print(f'{level*"_"}{level}: [{created_at}][{username}] {text_80chars}') 52 | level += 1 53 | for child in reversed(self.children): 54 | child.print_tree(level) 55 | 56 | 57 | # 58 | # PROGRAM BEGINS HERE 59 | # 60 | 61 | try: 62 | o = TwitterOAuth.read_file() 63 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 64 | 65 | # GET ROOT OF THE CONVERSATION 66 | 67 | r = api.request(f'tweets/:{CONVERSATION_ID}', 68 | { 69 | 'expansions':'author_id', 70 | 'tweet.fields':'author_id,conversation_id,created_at,referenced_tweets' 71 | }, 72 | hydrate_type=HydrateType.REPLACE) 73 | 74 | root = None 75 | for item in r: 76 | root = TreeNode(item) 77 | print(f'ROOT {root.id()}') 78 | if not root: 79 | print(f'Conversation ID {CONVERSATION_ID} does not exist') 80 | exit() 81 | 82 | # GET ALL REPLIES IN CONVERSATION 83 | # (RETURNED IN REVERSE CHRONOLOGICAL ORDER) 84 | 85 | pager = TwitterPager(api, 'tweets/search/recent', 86 | { 87 | 'query':f'conversation_id:{CONVERSATION_ID}', 88 | 'expansions':'author_id', 89 | 'tweet.fields':'author_id,conversation_id,created_at,referenced_tweets' 90 | }, 91 | hydrate_type=HydrateType.REPLACE) 92 | 93 | # "wait=2" means wait 2 seconds between each request. 94 | # The rate limit is 450 requests per 15 minutes, or 1 request every 15*60/450 = 2 seconds. 95 | 96 | orphans = [] 97 | 98 | for item in pager.get_iterator(wait=2): 99 | node = TreeNode(item) 100 | print(f'{node.id()} => {node.parent()}', item['author_id']['username']) 101 | # COLLECT ANY ORPHANS THAT ARE CHILDREN OF THE NEW NODE 102 | orphans = [orphan for orphan in orphans if not node.find_parent_of(orphan)] 103 | # IF THE NEW NODE CANNOT BE PLACED IN TREE, ORPHAN IT UNTIL ITS PARENT IS FOUND 104 | if not root.find_parent_of(node): 105 | orphans.append(node) 106 | 107 | print('\nTREE...') 108 | root.print_tree(0) 109 | 110 | # YOU MIGHT GET ORPHANS WHEN PART OF THE CONVERSATION IS OLDER THAN A WEEK 111 | assert len(orphans) == 0, f'{len(orphans)} orphaned tweets' 112 | 113 | except TwitterRequestError as e: 114 | print(e.status_code) 115 | for msg in iter(e): 116 | print(msg) 117 | 118 | except TwitterConnectionError as e: 119 | print(e) 120 | 121 | except Exception as e: 122 | print(e) 123 | -------------------------------------------------------------------------------- /examples/v2/expansions_media.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | TWEET_ID = '1318585013944942593' 5 | EXPANSIONS = 'attachments.media_keys' 6 | MEDIA_FIELDS = 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics' 7 | 8 | try: 9 | o = TwitterOAuth.read_file() 10 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 11 | r = api.request(f'tweets/:{TWEET_ID}', 12 | { 13 | 'expansions':EXPANSIONS, 14 | 'media.fields':MEDIA_FIELDS 15 | }) 16 | 17 | for item in r: 18 | print(json.dumps(item, indent=2)) 19 | 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/expansions_place.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | TWEET_ID = '1295386980495364101' 5 | EXPANSIONS = 'geo.place_id' 6 | PLACE_FIELDS = 'contained_within,country,country_code,full_name,geo,id,name,place_type' 7 | 8 | try: 9 | o = TwitterOAuth.read_file() 10 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 11 | r = api.request(f'tweets/:{TWEET_ID}', 12 | { 13 | 'expansions':EXPANSIONS, 14 | 'place.fields':PLACE_FIELDS 15 | }) 16 | 17 | for item in r: 18 | print(json.dumps(item, indent=2)) 19 | 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/expansions_poll.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | TWEET_ID = '1317617296978833408' 5 | EXPANSIONS = 'attachments.poll_ids' 6 | POLL_FIELDS = 'duration_minutes,end_datetime,id,options,voting_status' 7 | 8 | try: 9 | o = TwitterOAuth.read_file() 10 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 11 | r = api.request(f'tweets/:{TWEET_ID}', 12 | { 13 | 'expansions':EXPANSIONS, 14 | 'poll.fields':POLL_FIELDS 15 | }) 16 | 17 | for item in r: 18 | print(json.dumps(item, indent=2)) 19 | 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/expansions_tweet.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | TWEET_ID = '1275914014867050499' 5 | EXPANSIONS = 'referenced_tweets.id' 6 | TWEET_FIELDS = 'attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,public_metrics,text' 7 | 8 | try: 9 | o = TwitterOAuth.read_file() 10 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 11 | r = api.request(f'tweets/:{TWEET_ID}', 12 | { 13 | 'expansions':EXPANSIONS, 14 | 'tweet.fields':TWEET_FIELDS 15 | }) 16 | 17 | for item in r: 18 | print(json.dumps(item, indent=2)) 19 | 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/expansions_user.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | TWEET_ID = '1275914014867050499' 5 | EXPANSIONS = 'author_id,in_reply_to_user_id,referenced_tweets.id.author_id' 6 | USER_FIELDS = 'location,profile_image_url,verified' 7 | 8 | try: 9 | o = TwitterOAuth.read_file() 10 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 11 | r = api.request(f'tweets/:{TWEET_ID}', 12 | { 13 | 'expansions':EXPANSIONS, 14 | 'user.fields':USER_FIELDS 15 | }) 16 | 17 | for item in r: 18 | print(json.dumps(item, indent=2)) 19 | 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/lookup_tweet.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | TWEET_ID = '964575983633252353' 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | r = api.request(f'tweets/:{TWEET_ID}') 9 | 10 | for item in r: 11 | print(item) 12 | 13 | print(r.get_quota()) 14 | 15 | except TwitterRequestError as e: 16 | print(e.status_code) 17 | for msg in iter(e): 18 | print(msg) 19 | 20 | except TwitterConnectionError as e: 21 | print(e) 22 | 23 | except Exception as e: 24 | print(e) -------------------------------------------------------------------------------- /examples/v2/lookup_tweet_hydrate.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError, HydrateType 2 | import json 3 | 4 | TWEET_ID = '1360029234773770240' 5 | 6 | EXPANSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,in_reply_to_user_id,attachments.media_keys,attachments.poll_ids,geo.place_id,entities.mentions.username' 7 | MEDIA_FIELDS = 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics' 8 | TWEET_FIELDS = 'created_at,author_id,public_metrics,context_annotations,entities' 9 | USER_FIELDS = 'location,profile_image_url,verified' 10 | 11 | try: 12 | o = TwitterOAuth.read_file() 13 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 14 | r = api.request(f'tweets/:{TWEET_ID}', 15 | { 16 | 'expansions': EXPANSIONS, 17 | 'tweet.fields': TWEET_FIELDS, 18 | 'user.fields': USER_FIELDS, 19 | 'media.fields': MEDIA_FIELDS, 20 | }, 21 | hydrate_type=HydrateType.APPEND) 22 | 23 | for item in r: 24 | print(json.dumps(item, indent=2, sort_keys=True)) 25 | 26 | except TwitterRequestError as e: 27 | print(e.status_code) 28 | for msg in iter(e): 29 | print(msg) 30 | 31 | except TwitterConnectionError as e: 32 | print(e) 33 | 34 | except Exception as e: 35 | print(e) 36 | -------------------------------------------------------------------------------- /examples/v2/lookup_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | TWEET_IDS = ['964575983633252353', '1497014169178185730'] 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | r = api.request(f'tweets',{'ids':",".join(TWEET_IDS)}) 9 | 10 | for item in r: 11 | print(item) 12 | 13 | print(r.get_quota()) 14 | 15 | except TwitterRequestError as e: 16 | print(e.status_code) 17 | for msg in iter(e): 18 | print(msg) 19 | 20 | except TwitterConnectionError as e: 21 | print(e) 22 | 23 | except Exception as e: 24 | print(e) -------------------------------------------------------------------------------- /examples/v2/lookup_user.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | SCREEN_NAME = 'TheTweetOfGod' 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | r = api.request(f'users/by/username/:{SCREEN_NAME}') 9 | 10 | for item in r: 11 | print(item) 12 | 13 | print(r.get_quota()) 14 | 15 | except TwitterRequestError as e: 16 | print(e.status_code) 17 | for msg in iter(e): 18 | print(msg) 19 | 20 | except TwitterConnectionError as e: 21 | print(e) 22 | 23 | except Exception as e: 24 | print(e) 25 | -------------------------------------------------------------------------------- /examples/v2/page_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError, TwitterPager 2 | 3 | QUERY = 'pizza' 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | pager = TwitterPager(api, 'tweets/search/recent', {'query':QUERY}) 9 | 10 | for item in pager.get_iterator(new_tweets=False): 11 | print(item) 12 | 13 | except TwitterRequestError as e: 14 | print(e.status_code) 15 | for msg in iter(e): 16 | print(msg) 17 | 18 | except TwitterConnectionError as e: 19 | print(e) 20 | 21 | except Exception as e: 22 | print(e) 23 | -------------------------------------------------------------------------------- /examples/v2/page_tweets_hydrate.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError, TwitterPager, HydrateType 2 | import json 3 | 4 | QUERY = 'pizza' 5 | 6 | EXPANSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,in_reply_to_user_id,attachments.media_keys,attachments.poll_ids,geo.place_id,entities.mentions.username' 7 | MEDIA_FIELDS = 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics' 8 | TWEET_FIELDS = 'created_at,author_id,public_metrics' 9 | USER_FIELDS = 'location,profile_image_url,verified' 10 | 11 | try: 12 | o = TwitterOAuth.read_file() 13 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 14 | pager = TwitterPager(api, 'tweets/search/recent', 15 | { 16 | 'query': {QUERY}, 17 | 'expansions': EXPANSIONS, 18 | 'tweet.fields': TWEET_FIELDS, 19 | 'user.fields': USER_FIELDS, 20 | 'media.fields': MEDIA_FIELDS, 21 | }, 22 | hydrate_type=HydrateType.APPEND) 23 | 24 | for item in pager.get_iterator(new_tweets=False): 25 | print(json.dumps(item, indent=2)) 26 | 27 | except TwitterRequestError as e: 28 | print(e.status_code) 29 | for msg in iter(e): 30 | print(msg) 31 | 32 | except TwitterConnectionError as e: 33 | print(e) 34 | 35 | except Exception as e: 36 | print(e) 37 | -------------------------------------------------------------------------------- /examples/v2/rules_add.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | QUERY = 'pizza"' 5 | 6 | try: 7 | o = TwitterOAuth.read_file() 8 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 9 | 10 | # ADD STREAM RULES 11 | 12 | r = api.request('tweets/search/stream/rules', {'add': [{'value':QUERY}]}) 13 | print(f'[{r.status_code}] RULE ADDED: {json.dumps(r.json(), indent=2)}\n') 14 | 15 | except TwitterRequestError as e: 16 | print(e.status_code) 17 | for msg in iter(e): 18 | print(msg) 19 | 20 | except TwitterConnectionError as e: 21 | print(e) 22 | 23 | except Exception as e: 24 | print(e) 25 | -------------------------------------------------------------------------------- /examples/v2/rules_del.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | RULE_IDS = [ 5 | "1317935598431309824", 6 | "1321813278532751361", 7 | ] 8 | 9 | try: 10 | o = TwitterOAuth.read_file() 11 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 12 | 13 | # DELETE STREAM RULES 14 | 15 | r = api.request('tweets/search/stream/rules', {'delete': {'ids':RULE_IDS}}) 16 | print(f'[{r.status_code}] RULES DELETED: {json.dumps(r.json(), indent=2)}\n') 17 | 18 | except TwitterRequestError as e: 19 | print(e.status_code) 20 | for msg in iter(e): 21 | print(msg) 22 | 23 | except TwitterConnectionError as e: 24 | print(e) 25 | 26 | except Exception as e: 27 | print(e) 28 | -------------------------------------------------------------------------------- /examples/v2/rules_del_all.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | try: 5 | o = TwitterOAuth.read_file() 6 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 7 | 8 | # GET STREAM RULES 9 | 10 | rule_ids = [] 11 | r = api.request('tweets/search/stream/rules', method_override='GET') 12 | for item in r: 13 | if 'id' in item: 14 | rule_ids.append(item['id']) 15 | else: 16 | print(json.dumps(item, indent=2)) 17 | 18 | # DELETE STREAM RULES 19 | 20 | if len(rule_ids) > 0: 21 | r = api.request('tweets/search/stream/rules', {'delete': {'ids':rule_ids}}) 22 | print(f'[{r.status_code}] RULES DELETED: {json.dumps(r.json(), indent=2)}\n') 23 | 24 | except TwitterRequestError as e: 25 | print(e.status_code) 26 | for msg in iter(e): 27 | print(msg) 28 | 29 | except TwitterConnectionError as e: 30 | print(e) 31 | 32 | except Exception as e: 33 | print(e) 34 | -------------------------------------------------------------------------------- /examples/v2/rules_get.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | import json 3 | 4 | try: 5 | o = TwitterOAuth.read_file() 6 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 7 | 8 | # GET STREAM RULES 9 | 10 | r = api.request('tweets/search/stream/rules', method_override='GET') 11 | print(f'[{r.status_code}] RULES: {json.dumps(r.json(), indent=2)}\n') 12 | 13 | except TwitterRequestError as e: 14 | print(e.status_code) 15 | for msg in iter(e): 16 | print(msg) 17 | 18 | except TwitterConnectionError as e: 19 | print(e) 20 | 21 | except Exception as e: 22 | print(e) 23 | -------------------------------------------------------------------------------- /examples/v2/search_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | QUERY = 'pizza' 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | r = api.request('tweets/search/recent', { 9 | 'query':QUERY, 10 | 'tweet.fields':'author_id', 11 | 'expansions':'author_id'}) 12 | 13 | for item in r: 14 | print(item) 15 | 16 | print('\nINCLUDES') 17 | print(r.json()['includes']) 18 | 19 | print('\nQUOTA') 20 | print(r.get_quota()) 21 | 22 | except TwitterRequestError as e: 23 | print(e.status_code) 24 | for msg in iter(e): 25 | print(msg) 26 | 27 | except TwitterConnectionError as e: 28 | print(e) 29 | 30 | except Exception as e: 31 | print(e) -------------------------------------------------------------------------------- /examples/v2/search_tweets_hydrate.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError, HydrateType 2 | import json 3 | 4 | QUERY = 'kitten' 5 | EXPANSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,in_reply_to_user_id,attachments.media_keys' 6 | MEDIA_FIELDS = 'duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics' 7 | TWEET_FIELDS = 'created_at,author_id,public_metrics' 8 | USER_FIELDS = 'location,profile_image_url,verified' 9 | 10 | try: 11 | o = TwitterOAuth.read_file() 12 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 13 | r = api.request('tweets/search/recent', 14 | { 15 | 'query': {QUERY}, 16 | 'expansions': EXPANSIONS, 17 | 'media.fields': MEDIA_FIELDS, 18 | 'tweet.fields': TWEET_FIELDS, 19 | 'user.fields': USER_FIELDS, 20 | }, 21 | hydrate_type=HydrateType.APPEND) 22 | 23 | for item in r: 24 | print(json.dumps(item, indent=2)) 25 | 26 | print(r.get_quota()) 27 | 28 | except TwitterRequestError as e: 29 | print(e.status_code) 30 | for msg in iter(e): 31 | print(msg) 32 | 33 | except TwitterConnectionError as e: 34 | print(e) 35 | 36 | except Exception as e: 37 | print(e) 38 | -------------------------------------------------------------------------------- /examples/v2/stream_forever.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import (TwitterAPI, 2 | TwitterOAuth, 3 | TwitterRequestError, 4 | TwitterConnectionError, 5 | HydrateType) 6 | 7 | 8 | RULES = ['pizza','lambrusco'] 9 | 10 | EXPANSIONS = 'author_id' 11 | USER_FIELDS = 'created_at,description,location,name,username' 12 | TWEET_FIELDS = 'author_id,created_at,entities,id,lang,public_metrics,source,text' 13 | 14 | 15 | ## 16 | ## SET UP LOGGING TO FILE AND TO CONSOLE 17 | ## 18 | 19 | import logging 20 | formatter = logging.Formatter('%(levelname)s %(asctime)s %(message)s', '%m/%d/%Y %I:%M:%S %p') 21 | fh = logging.FileHandler('stream_forever.log') 22 | sh = logging.StreamHandler() 23 | fh.setFormatter(formatter) 24 | sh.setFormatter(formatter) 25 | logger = logging.getLogger() 26 | logger.setLevel(logging.INFO) 27 | logger.addHandler(fh) 28 | logger.addHandler(sh) 29 | 30 | 31 | ## 32 | ## AUTHENTICATE WITH TWITTER 33 | ## 34 | 35 | o = TwitterOAuth.read_file() 36 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 37 | 38 | 39 | ## 40 | ## STREAMING RULES 41 | ## 42 | 43 | # ADD RULES (NO HARM RE-ADDING, BUT ONCE IS ENOUGH) 44 | for rule in RULES: 45 | r = api.request('tweets/search/stream/rules', {'add': [{'value':rule}]}) 46 | print(f'[{r.status_code}] RULE ADDED: {r.text}') 47 | if r.status_code != 201: exit() 48 | 49 | # PRINT RULES 50 | r = api.request('tweets/search/stream/rules', method_override='GET') 51 | print(f'\n[{r.status_code}] RULES:') 52 | if r.status_code != 200: exit() 53 | for item in r: 54 | print(item['value']) 55 | 56 | 57 | ## 58 | ## "BACK OFF" STRATEGY FOR DISCONNECTS AND RATE LIMITS 59 | ## USE 60 SECOND INTERVAL FOR HTTP 429 ERRORS 60 | ## USE 5 SECOND INTERVAL FOR ALL OTHER HTTP ERRORS 61 | ## REFER TO: https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/integrate/handling-disconnections 62 | ## 63 | 64 | from time import sleep 65 | 66 | backOffCount = 0 67 | 68 | def backOff(interval): 69 | global backOffCount, logger 70 | seconds = pow(2, backOffCount) * interval 71 | backOffCount = min(backOffCount + 1, 6) 72 | logger.info(f'Back off {seconds} seconds') 73 | sleep(seconds) 74 | 75 | 76 | ## 77 | ## STREAM FOREVER... 78 | ## 79 | 80 | while True: 81 | try: 82 | # START STREAM 83 | logger.info('START STREAM...') 84 | r = api.request('tweets/search/stream', 85 | { 86 | 'expansions': EXPANSIONS, 87 | 'user.fields': USER_FIELDS, 88 | 'tweet.fields': TWEET_FIELDS 89 | }, 90 | hydrate_type=HydrateType.APPEND) 91 | 92 | if r.status_code == 200: 93 | logger.info(f'[{r.status_code}] STREAM CONNECTED') 94 | backOffCount = 0 95 | else: 96 | logger.info(f'[{r.status_code}] FAILED TO CONNECT. REASON:\n{r.text}') 97 | backOff(60 if r.status_code == 429 else 5) 98 | continue 99 | 100 | for item in r: 101 | if 'data' in item: 102 | print(item['data']['text']) 103 | elif 'errors' in item: 104 | logger.error(item['errors']) 105 | else: 106 | # PROBABLY SHOULD NEVER BRANCH TO HERE 107 | logger.warning(f'UNKNOWN ITEM TYPE: {item}') 108 | 109 | except TwitterConnectionError: 110 | backOff(5) 111 | continue 112 | 113 | except TwitterRequestError: 114 | break 115 | 116 | except KeyboardInterrupt: 117 | break 118 | 119 | finally: 120 | logger.info('STREAM STOPPED') -------------------------------------------------------------------------------- /examples/v2/stream_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | QUERY = 'pizza' 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type='oAuth2', api_version='2') 8 | 9 | # ADD STREAM RULES 10 | 11 | r = api.request('tweets/search/stream/rules', {'add': [{'value':QUERY}]}) 12 | print(f'[{r.status_code}] RULE ADDED: {r.text}') 13 | if r.status_code != 201: exit() 14 | 15 | # GET STREAM RULES 16 | 17 | r = api.request('tweets/search/stream/rules', method_override='GET') 18 | print(f'[{r.status_code}] RULES: {r.text}') 19 | if r.status_code != 200: exit() 20 | 21 | # START STREAM 22 | 23 | r = api.request('tweets/search/stream') 24 | print(f'[{r.status_code}] START...') 25 | if r.status_code != 200: exit() 26 | for item in r: 27 | print(item) 28 | 29 | except TwitterRequestError as e: 30 | print(e.status_code) 31 | for msg in iter(e): 32 | print(msg) 33 | 34 | except TwitterConnectionError as e: 35 | print(e) 36 | 37 | except Exception as e: 38 | print(e) -------------------------------------------------------------------------------- /examples/v2/stream_tweets_hydrate.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError, HydrateType, OAuthType 2 | import json 3 | 4 | QUERY = '"pizza" OR "burger"' 5 | EXPANSIONS = 'author_id,referenced_tweets.id,referenced_tweets.id.author_id,in_reply_to_user_id,attachments.media_keys,attachments.poll_ids,geo.place_id,entities.mentions.username' 6 | TWEET_FIELDS='author_id,conversation_id,created_at,entities,geo,id,lang,public_metrics,source,text' 7 | USER_FIELDS='created_at,description,entities,location,name,profile_image_url,public_metrics,url,username' 8 | 9 | try: 10 | o = TwitterOAuth.read_file() 11 | api = TwitterAPI(o.consumer_key, o.consumer_secret, auth_type=OAuthType.OAUTH2, api_version='2') 12 | 13 | # ADD STREAM RULES 14 | 15 | r = api.request('tweets/search/stream/rules', {'add': [{'value':QUERY}]}) 16 | print(f'[{r.status_code}] RULE ADDED: {json.dumps(r.json(), indent=2)}\n') 17 | if r.status_code != 201: exit() 18 | 19 | # GET STREAM RULES 20 | 21 | r = api.request('tweets/search/stream/rules', method_override='GET') 22 | print(f'[{r.status_code}] RULES: {json.dumps(r.json(), indent=2)}\n') 23 | if r.status_code != 200: exit() 24 | 25 | # START STREAM 26 | 27 | r = api.request('tweets/search/stream', { 28 | 'expansions': EXPANSIONS, 29 | 'tweet.fields': TWEET_FIELDS, 30 | 'user.fields': USER_FIELDS, 31 | }, 32 | hydrate_type=HydrateType.APPEND) 33 | 34 | print(f'[{r.status_code}] START...') 35 | if r.status_code != 200: exit() 36 | for item in r: 37 | print(json.dumps(item, indent=2)) 38 | 39 | except KeyboardInterrupt: 40 | print('\nDone!') 41 | 42 | except TwitterRequestError as e: 43 | print(f'\n{e.status_code}') 44 | for msg in iter(e): 45 | print(msg) 46 | 47 | except TwitterConnectionError as e: 48 | print(e) 49 | 50 | except Exception as e: 51 | print(e) 52 | -------------------------------------------------------------------------------- /examples/v2/user_bookmarks.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | from pprint import pprint 3 | 4 | USER_ID = '1249069835562233858' # https://twitter.com/bascodes 5 | access_token = "" 6 | 7 | try: 8 | o = TwitterOAuth.read_file() 9 | api = TwitterAPI(oauth2_access_token=access_token, api_version='2', auth_type="oAuth2User") 10 | 11 | # Get tweets - default setting 12 | tweets = api.request(f'users/:{USER_ID}/bookmarks') 13 | for t in tweets: 14 | print(t) 15 | 16 | # Get tweets with customization - (5 tweets only with created_at timestamp) 17 | print() 18 | params = {'max_results': 5, 'tweet.fields': 'created_at'} 19 | tweets = api.request(f'users/:{USER_ID}/bookmarks', params) 20 | for t in tweets: 21 | pprint(t) 22 | 23 | # Get next 5 tweets 24 | print() 25 | next_token = tweets.json()['meta']['next_token'] 26 | params = {'max_results': 5, 'tweet.fields': 'created_at', 'pagination_token': next_token} 27 | tweets = api.request(f'users/:{USER_ID}/bookmarks', params) 28 | for t in tweets: 29 | pprint(t) 30 | 31 | 32 | except TwitterRequestError as e: 33 | print('Request error') 34 | print(e.status_code) 35 | for msg in iter(e): 36 | print(msg) 37 | 38 | except TwitterConnectionError as e: 39 | print('Connection error') 40 | print(e) 41 | 42 | except Exception as e: 43 | print('Exception') 44 | print(e) -------------------------------------------------------------------------------- /examples/v2/user_followers_following.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | 3 | USER_ID = '2244994945' # https://twitter.com/TwitterDev 4 | 5 | try: 6 | o = TwitterOAuth.read_file() 7 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 8 | 9 | # Get followers 10 | followers = api.request(f'users/:{USER_ID}/followers') 11 | for f in followers: 12 | print(f) 13 | 14 | # Get following 15 | following = api.request(f'users/:{USER_ID}/following') 16 | for f in following: 17 | print(f) 18 | 19 | except TwitterRequestError as e: 20 | print(e.status_code) 21 | for msg in iter(e): 22 | print(msg) 23 | 24 | except TwitterConnectionError as e: 25 | print(e) 26 | 27 | except Exception as e: 28 | print(e) 29 | -------------------------------------------------------------------------------- /examples/v2/user_tweets.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth, TwitterRequestError, TwitterConnectionError 2 | from pprint import pprint 3 | 4 | USER_ID = '2244994945' # https://twitter.com/TwitterDev 5 | 6 | try: 7 | o = TwitterOAuth.read_file() 8 | api = TwitterAPI(o.consumer_key, o.consumer_secret, o.access_token_key, o.access_token_secret, api_version='2') 9 | 10 | # Get tweets - default setting 11 | tweets = api.request(f'users/:{USER_ID}/tweets') 12 | for t in tweets: 13 | print(t) 14 | 15 | # Get tweets with customization - (5 tweets only with created_at timestamp) 16 | print() 17 | params = {'max_results': 5, 'tweet.fields': 'created_at'} 18 | tweets = api.request(f'users/:{USER_ID}/tweets', params) 19 | for t in tweets: 20 | pprint(t) 21 | 22 | # Get next 5 tweets 23 | print() 24 | next_token = tweets.json()['meta']['next_token'] 25 | params = {'max_results': 5, 'tweet.fields': 'created_at', 'pagination_token': next_token} 26 | tweets = api.request(f'users/:{USER_ID}/tweets', params) 27 | for t in tweets: 28 | pprint(t) 29 | 30 | 31 | except TwitterRequestError as e: 32 | print('Request error') 33 | print(e.status_code) 34 | for msg in iter(e): 35 | print(msg) 36 | 37 | except TwitterConnectionError as e: 38 | print('Connection error') 39 | print(e) 40 | 41 | except Exception as e: 42 | print('Exception') 43 | print(e) -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geduldig/TwitterAPI/ffa9cad2903a0b6c29c7d8da2f7718ecdf70d111/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | requests_oauthlib 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | from TwitterAPI import __version__ 3 | import io 4 | 5 | 6 | def read(*filenames, **kwargs): 7 | encoding = kwargs.get('encoding', 'utf-8') 8 | sep = kwargs.get('sep', '\n') 9 | buf = [] 10 | for filename in filenames: 11 | with io.open(filename, encoding=encoding) as f: 12 | buf.append(f.read()) 13 | return sep.join(buf) 14 | 15 | setup( 16 | name='TwitterAPI', 17 | version=__version__, 18 | author='geduldig', 19 | author_email='boxnumber03@gmail.com', 20 | packages=['TwitterAPI'], 21 | package_data={'': ['credentials.txt']}, 22 | url='https://github.com/geduldig/TwitterAPI', 23 | download_url='https://github.com/geduldig/TwitterAPI/tarball/master', 24 | license='MIT', 25 | keywords='twitter', 26 | description='Minimal wrapper for Twitter\'s REST and Streaming APIs', 27 | install_requires=['requests', 'requests_oauthlib'] 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_iterators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import TwitterAPI 3 | 4 | 5 | class IteratorTest(unittest.TestCase): 6 | 7 | """Test REST API and Streaming API iterators.""" 8 | 9 | def setUp(self): 10 | """Read credentials from TwitterAPI/credentials.txt. You 11 | must copy your credentials into this text file. 12 | """ 13 | oa = TwitterAPI.TwitterOAuth.read_file() 14 | self.api = TwitterAPI.TwitterAPI(oa.consumer_key, 15 | oa.consumer_secret, 16 | oa.access_token_key, 17 | oa.access_token_secret) 18 | 19 | def test_rest_iterator(self): 20 | r = self.api.request('search/tweets', {'q': 'pizza'}) 21 | self.assertIsInstance(r, TwitterAPI.TwitterResponse) 22 | # 200 means success 23 | self.assertEqual(r.status_code, 200) 24 | it = r.get_iterator() 25 | self.use_iterator(it) 26 | 27 | def test_streaming_iterator(self): 28 | r = self.api.request('statuses/filter', {'track': 'pizza'}) 29 | self.assertIsInstance(r, TwitterAPI.TwitterResponse) 30 | # 200 means success 31 | # 420 means too many simultaneous connections, ok 32 | self.assertIn(r.status_code, [200, 420]) 33 | if r.status_code == 200: 34 | it = r.get_iterator() 35 | self.use_iterator(it) 36 | 37 | def test_paging_iterator(self): 38 | pager = TwitterAPI.TwitterPager(self.api, 39 | 'search/tweets', 40 | {'q': 'pizza'}) 41 | self.assertIsInstance(pager, TwitterAPI.TwitterPager) 42 | it = pager.get_iterator() 43 | self.use_iterator(it) 44 | 45 | def use_iterator(self, it): 46 | """Checks the first item for a tweet id.""" 47 | self.assertTrue(hasattr(it, '__iter__')) 48 | item = next(it) 49 | self.assertIn('id', item) 50 | 51 | 52 | if __name__ == '__main__': 53 | unittest.main() 54 | -------------------------------------------------------------------------------- /tests/test_oauth.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI, TwitterOAuth 2 | import unittest 3 | 4 | 5 | class OAuthTest(unittest.TestCase): 6 | 7 | """Test user and application authentication.""" 8 | 9 | def setUp(self): 10 | """Read credentials from TwitterAPI/credentials.txt. You 11 | must copy your credentials into this text file. 12 | """ 13 | self.oa = TwitterOAuth.read_file() 14 | 15 | def test_oauth_1(self): 16 | """Test user authentication.""" 17 | api = TwitterAPI(self.oa.consumer_key, self.oa.consumer_secret, 18 | self.oa.access_token_key, self.oa.access_token_secret) 19 | status_code = self.verify_credentials(api) 20 | # 200 means success 21 | self.assertEqual(status_code, 200) 22 | 23 | def test_oauth_2(self): 24 | """Test application authentication.""" 25 | api = TwitterAPI(self.oa.consumer_key, self.oa.consumer_secret, 26 | auth_type='oAuth2') 27 | status_code = self.verify_credentials(api) 28 | # 403 means no access, which is correct since no user credentials 29 | # provided 30 | self.assertEqual(status_code, 403) 31 | 32 | def verify_credentials(self, api): 33 | r = api.request('account/verify_credentials') 34 | return r.status_code 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | --------------------------------------------------------------------------------