# Tweepy # Copyright 2009-2023 Joshua Roesslein # See LICENSE for details. import contextlib import functools import imghdr import logging import mimetypes from platform import python_version import sys import time from urllib.parse import urlencode import requests import tweepy from tweepy.errors import ( BadRequest, Forbidden, HTTPException, NotFound, TooManyRequests, TweepyException, TwitterServerError, Unauthorized ) from tweepy.models import Model from tweepy.parsers import ModelParser, Parser from tweepy.utils import list_to_csv log = logging.getLogger(__name__) def pagination(mode): def decorator(method): @functools.wraps(method) def wrapper(*args, **kwargs): return method(*args, **kwargs) wrapper.pagination_mode = mode return wrapper return decorator def payload(payload_type, **payload_kwargs): payload_list = payload_kwargs.get('list', False) def decorator(method): @functools.wraps(method) def wrapper(*args, **kwargs): kwargs['payload_list'] = payload_list kwargs['payload_type'] = payload_type return method(*args, **kwargs) wrapper.payload_list = payload_list wrapper.payload_type = payload_type return wrapper return decorator class API: """Twitter API v1.1 Interface .. versionchanged:: 4.11 Added support for ``include_ext_edit_control`` endpoint/method parameter .. versionchanged:: 4.14 Removed ``search_30_day`` and ``search_full_archive`` methods, as `the Premium v1.1 API has been deprecated`_ Parameters ---------- auth The authentication handler to be used cache The cache to query if a GET method is used host The general REST API host server URL parser The Parser instance to use for parsing the response from Twitter; defaults to an instance of ModelParser proxy The full url to an HTTPS proxy to use for connecting to Twitter retry_count Number of retries to attempt when an error occurs retry_delay Number of seconds to wait between retries retry_errors Which HTTP status codes to retry timeout The maximum amount of time to wait for a response from Twitter upload_host The URL of the upload server wait_on_rate_limit Whether or not to automatically wait for rate limits to replenish Raises ------ TypeError If the given parser is not a Parser instance References ---------- https://developer.twitter.com/en/docs/api-reference-index .. _the Premium v1.1 API has been deprecated: https://twittercommunity.com/t/deprecating-the-premium-v1-1-api/191092 """ def __init__( self, auth=None, *, cache=None, host='api.twitter.com', parser=None, proxy=None, retry_count=0, retry_delay=0, retry_errors=None, timeout=60, upload_host='upload.twitter.com', user_agent=None, wait_on_rate_limit=False ): self.auth = auth self.cache = cache self.host = host if parser is None: parser = ModelParser() self.parser = parser self.proxy = {} if proxy is not None: self.proxy['https'] = proxy self.retry_count = retry_count self.retry_delay = retry_delay self.retry_errors = retry_errors self.timeout = timeout self.upload_host = upload_host if user_agent is None: user_agent = ( f"Python/{python_version()} " f"Requests/{requests.__version__} " f"Tweepy/{tweepy.__version__}" ) self.user_agent = user_agent self.wait_on_rate_limit = wait_on_rate_limit # Attempt to explain more clearly the parser argument requirements # https://github.com/tweepy/tweepy/issues/421 if not isinstance(self.parser, Parser): raise TypeError( "parser should be an instance of Parser, not " + str(type(self.parser)) ) self.session = requests.Session() def request( self, method, endpoint, *, endpoint_parameters=(), params=None, headers=None, json_payload=None, parser=None, payload_list=False, payload_type=None, post_data=None, files=None, require_auth=True, return_cursors=False, upload_api=False, use_cache=True, **kwargs ): # If authentication is required and no credentials # are provided, throw an error. if require_auth and not self.auth: raise TweepyException('Authentication required!') self.cached_result = False if headers is None: headers = {} headers["User-Agent"] = self.user_agent # Build the request URL path = f'/1.1/{endpoint}.json' if upload_api: url = 'https://' + self.upload_host + path else: url = 'https://' + self.host + path if params is None: params = {} for k, arg in kwargs.items(): if arg is None: continue if k not in endpoint_parameters + ( "include_ext_edit_control", "tweet_mode" ): log.warning(f'Unexpected parameter: {k}') params[k] = str(arg) log.debug("PARAMS: %r", params) # Query the cache if one is available # and this request uses a GET method. if use_cache and self.cache and method == 'GET': cache_result = self.cache.get(f'{path}?{urlencode(params)}') # if cache result found and not expired, return it if cache_result: # must restore api reference if isinstance(cache_result, list): for result in cache_result: if isinstance(result, Model): result._api = self else: if isinstance(cache_result, Model): cache_result._api = self self.cached_result = True return cache_result # Monitoring rate limits remaining_calls = None reset_time = None if parser is None: parser = self.parser try: # Continue attempting request until successful # or maximum number of retries is reached. retries_performed = 0 while retries_performed <= self.retry_count: if (self.wait_on_rate_limit and reset_time is not None and remaining_calls is not None and remaining_calls < 1): # Handle running out of API calls sleep_time = reset_time - int(time.time()) if sleep_time > 0: log.warning(f"Rate limit reached. Sleeping for: {sleep_time}") time.sleep(sleep_time + 1) # Sleep for extra sec # Apply authentication auth = None if self.auth: auth = self.auth.apply_auth() # Execute request try: resp = self.session.request( method, url, params=params, headers=headers, data=post_data, files=files, json=json_payload, timeout=self.timeout, auth=auth, proxies=self.proxy ) except Exception as e: raise TweepyException(f'Failed to send request: {e}').with_traceback(sys.exc_info()[2]) if 200 <= resp.status_code < 300: break rem_calls = resp.headers.get('x-rate-limit-remaining') if rem_calls is not None: remaining_calls = int(rem_calls) elif remaining_calls is not None: remaining_calls -= 1 reset_time = resp.headers.get('x-rate-limit-reset') if reset_time is not None: reset_time = int(reset_time) retry_delay = self.retry_delay if resp.status_code in (420, 429) and self.wait_on_rate_limit: if remaining_calls == 0: # If ran out of calls before waiting switching retry last call continue if 'retry-after' in resp.headers: retry_delay = float(resp.headers['retry-after']) elif self.retry_errors and resp.status_code not in self.retry_errors: # Exit request loop if non-retry error code break # Sleep before retrying request again time.sleep(retry_delay) retries_performed += 1 # If an error was returned, throw an exception self.last_response = resp if resp.status_code == 400: raise BadRequest(resp) if resp.status_code == 401: raise Unauthorized(resp) if resp.status_code == 403: raise Forbidden(resp) if resp.status_code == 404: raise NotFound(resp) if resp.status_code == 429: raise TooManyRequests(resp) if resp.status_code >= 500: raise TwitterServerError(resp) if resp.status_code and not 200 <= resp.status_code < 300: raise HTTPException(resp) # Parse the response payload return_cursors = return_cursors or 'cursor' in params or 'next' in params result = parser.parse( resp.text, api=self, payload_list=payload_list, payload_type=payload_type, return_cursors=return_cursors ) # Store result into cache if one is available. if use_cache and self.cache and method == 'GET' and result: self.cache.store(f'{path}?{urlencode(params)}', result) return result finally: self.session.close() # Get Tweet timelines @pagination(mode='id') @payload('status', list=True) def home_timeline(self, **kwargs): """home_timeline(*, count, since_id, max_id, trim_user, \ exclude_replies, include_entities) Returns the 20 most recent statuses, including retweets, posted by the authenticating user and that user's friends. This is the equivalent of /timeline/home on the Web. Parameters ---------- count |count| since_id |since_id| max_id |max_id| trim_user |trim_user| exclude_replies |exclude_replies| include_entities |include_entities| Returns ------- :py:class:`List`\[:class:`~tweepy.models.Status`] References ---------- https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-home_timeline """ return self.request( 'GET', 'statuses/home_timeline', endpoint_parameters=( 'count', 'since_id', 'max_id', 'trim_user', 'exclude_replies', 'include_entities' ), **kwargs ) @pagination(mode='id') @payload('status', list=True) def mentions_timeline(self, **kwargs): """mentions_timeline(*, count, since_id, max_id, trim_user, \ include_entities) Returns the 20 most recent mentions, including retweets. Parameters ---------- count |count| since_id |since_id| max_id |max_id| trim_user |trim_user| include_entities |include_entities| Returns ------- :py:class:`List`\[:class:`~tweepy.models.Status`] References ---------- https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-mentions_timeline """ return self.request( 'GET', 'statuses/mentions_timeline', endpoint_parameters=( 'count', 'since_id', 'max_id', 'trim_user', 'include_entities' ), **kwargs ) @pagination(mode='id') @payload('status', list=True) def user_timeline(self, **kwargs): """user_timeline(*, user_id, screen_name, since_id, count, max_id, \ trim_user, exclude_replies, include_rts) Returns the 20 most recent statuses posted from the authenticating user or the user specified. It's also possible to request another user's timeline via the id parameter. Parameters ---------- user_id |user_id| screen_name |screen_name| since_id |since_id| count |count| max_id |max_id| trim_user |trim_user| exclude_replies |exclude_replies| include_rts When set to ``false``, the timeline will strip any native retweets (though they will still count toward both the maximal length of the timeline and the slice selected by the count parameter). Note: If you're using the trim_user parameter in conjunction with include_rts, the retweets will still contain a full user object. Returns ------- :py:class:`List`\[:class:`~tweepy.models.Status`] References ---------- https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-user_timeline """ return self.request( 'GET', 'statuses/user_timeline', endpoint_parameters=( 'user_id', 'screen_name', 'since_id', 'count', 'max_id', 'trim_user', 'exclude_replies', 'include_rts' ), **kwargs ) # Post, retrieve, and engage with Tweets @pagination(mode='id') @payload('status', list=True) def get_favorites(self, **kwargs): """get_favorites(*, user_id, screen_name, count, since_id, max_id, \ include_entities) Returns the favorite statuses for the authenticating user or user specified by the ID parameter. .. versionchanged:: 4.0 Renamed from ``API.favorites`` Parameters ---------- user_id |user_id| screen_name |screen_name| count |count| since_id |since_id| max_id |max_id| include_entities |include_entities| Returns ------- :py:class:`List`\[:class:`~tweepy.models.Status`] References ---------- https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-favorites-list """ return self.request( 'GET', 'favorites/list', endpoint_parameters=( 'user_id', 'screen_name', 'count', 'since_id', 'max_id', 'include_entities' ), **kwargs ) @payload('status', list=True) def lookup_statuses(self, id, **kwargs): """lookup_statuses(id, *, include_entities, trim_user, map, \ include_ext_alt_text, include_card_uri) Returns full Tweet objects for up to 100 Tweets per request, specified by the ``id`` parameter. .. versionchanged:: 4.0 Renamed from ``API.statuses_lookup`` Parameters ---------- id A list of Tweet IDs to lookup, up to 100 include_entities |include_entities| trim_user |trim_user| map A boolean indicating whether or not to include Tweets that cannot be shown. Defaults to False. include_ext_alt_text |include_ext_alt_text| include_card_uri |include_card_uri| Returns ------- :py:class:`List`\[:class:`~tweepy.models.Status`] References ---------- https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-lookup """ return self.request( 'GET', 'statuses/lookup', endpoint_parameters=( 'id', 'include_entities', 'trim_user', 'map', 'include_ext_alt_text', 'include_card_uri' ), id=list_to_csv(id), **kwargs ) @payload('json') def get_oembed(self, url, **kwargs): """get_oembed( \ url, *, maxwidth, hide_media, hide_thread, omit_script, align, \ related, lang, theme, link_color, widget_type, dnt \ ) Returns a single Tweet, specified by either a Tweet web URL or the Tweet ID, in an oEmbed-compatible format. The returned HTML snippet will be automatically recognized as an Embedded Tweet when Twitter's widget JavaScript is included on the page. The oEmbed endpoint allows customization of the final appearance of an Embedded Tweet by setting the corresponding properties in HTML markup to be interpreted by Twitter's JavaScript bundled with the HTML response by default. The format of the returned markup may change over time as Twitter adds new features or adjusts its Tweet representation. The Tweet fallback markup is meant to be cached on your servers for up to the suggested cache lifetime specified in the ``cache_age``. Parameters ---------- url The URL of the Tweet to be embedded maxwidth The maximum width of a rendered Tweet in whole pixels. A supplied value under or over the allowed range will be returned as the minimum or maximum supported width respectively; the reset width value will be reflected in the returned ``width`` property. Note that Twitter does not support the oEmbed ``maxheight`` parameter. Tweets are fundamentally text, and are therefore of unpredictable height that cannot be scaled like an image or video. Relatedly, the oEmbed response will not provide a value for ``height``. Implementations that need consistent heights for Tweets should refer to the ``hide_thread`` and ``hide_media`` parameters below. hide_media When set to ``true``, ``"t"``, or ``1``, links in a Tweet are not expanded to photo, video, or link previews. hide_thread When set to ``true``, ``"t"``, or ``1``, a collapsed version of the previous Tweet in a conversation thread will not be displayed when the requested Tweet is in reply to another Tweet. omit_script When set to ``true``, ``"t"``, or ``1``, the ``