pytter.api.api module
import uuid import base64 import urllib import requests from typing import Iterator, List, Dict from requests_oauthlib import OAuth2 from .credentials import Credentials from .exceptions import ( RateLimitException, NoneResponseException, ParameterOutOfBoundsException, ParameterNoneException ) from ..utils import utils from ..objects import Tweet, Media, User from ..utils import FileInfo class APISession: """ Barebone Twitter API wrapper representing Twitters API endpoints directly. Initialized with App Credentials for authentication and authorization. **Parameters** - `credentials: Credentials` APP or User credentials to authenticate against the Twitter API. """ API_ROOT_URI = 'https://api.twitter.com' API_VERSION = '1.1' API_UPLOAD_ROOT_URI = 'https://upload.twitter.com/1.1' UPLOAD_CHUNK_SIZE = 1024 * 1024 * 1024 # 1 MiB def __init__(self, credentials: Credentials): self._session = requests.Session() self._credentials = credentials self._session = requests.Session() if all([self._credentials.access_token_key, self._credentials.access_token_secret, self._credentials.consumer_key, self._credentials.consumer_secret]): self._oauth = self._credentials.to_oauth1() else: self.obtain_user_context_token() ############################ # GENERAL REQUEST HANDLING # ############################ def request(self, method: str, resource_path: str, **kwargs) -> object: """ Request the Twitter API with the defined authentication credentials. This method raises an exception on failed authentication or request. **Parameters** - `method : str` Request method. - `resource_path : str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `**kwargs` Optional arguments passed to request.request() **Returns** - `Object` JSON-parsed response body. """ res = self._session.request( auth=self._oauth, method=method, url='{0}/{1}/{2}'.format(self.API_ROOT_URI, self.API_VERSION, (resource_path[1:] if resource_path.startswith('/') else resource_path)), **kwargs) if res.status_code == 429: raise RateLimitException() if not res.ok: raise Exception('request failed with status code {} and message: {}' .format(res.status_code, res.text)) return res.json() def cursor_request(self, resource_path: str, expected_key: str, count: int = 200, params: dict = {}) -> List[object]: """ Issues cursored GET requests to the Twitter API follwoing the respond cursor for next requests returning the entire response objects as one array of objects. **Parameters** - `resource_path: str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `expected_key: str` Key expected to be contained in the response grouping all response objects. - `count: int` Ammount of objects which will be requested at once. Must be in range of [1, 200]. - `params: dict` Parameters passed to the single GET requests. **Returns** - `List[object]` List of concat objects expected in `expected_key`. """ results = [] cursor = -1 params['count'] = count if count > 200 or count < 1: raise ParameterOutOfBoundsException("must be in range of [1, 200]") while cursor is not 0: params['cursor'] = cursor res = self.request('GET', resource_path, params=params) data = res.get(expected_key) if data: results.extend(data) cursor = res.get('next_cursor') return results def obtain_user_context_token(self): """ Collect a user context bearer token from client_key and client_secret and sets it as OAuth2 authentication method for further requests. """ key = urllib.parse.quote_plus(self._credentials.consumer_key) secret = urllib.parse.quote_plus(self._credentials.consumer_secret) basic_token = base64.b64encode( '{0}:{1}'.format(key, secret).encode('utf8')).decode('utf8') res = self._session.post( url='{0}/oauth2/token'.format(self.API_ROOT_URI), headers={ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Authorization': 'Basic {0}'.format(basic_token), }, data={ 'grant_type': 'client_credentials', }) if res.status_code == 429: raise RateLimitException() if res.status_code != 200: raise Exception('request failed with status code {0}'.format(res.status_code)) body = res.json() self._oauth = OAuth2(token=body) ############## # UPLOAD API # ############## def upload_media_request(self, command=None, params={}, raw=None, **kwargs) -> object: """ Wrap a request to initiate, append or finalize a chunked media upload. **Parameters** - `command : str` Must be 'INIT', 'APPEND' or 'FINALIZE'. If 'raw' is set, this must not be passed. - `params : dict` Request body parameters. If 'raw' is set, this must not be passed. - `raw` Raw data used as reuqest body. This option overwrites 'command' and 'params'. - `**kwargs:` Additional arguments which will be passed to the request method. **Returns** - `object` Resulting FINALIZE response object. """ if raw is None: params['command'] = command params = utils.sort_dict_alphabetically(params) res = self._session.post( auth=self._oauth, url='{0}/media/upload.json'.format(self.API_UPLOAD_ROOT_URI), data=(params if raw is None else raw), **kwargs) if res.status_code == 429: raise RateLimitException() if res.status_code < 200 or res.status_code >= 300: raise Exception('request failed with status code {0}'.format(res.status_code)) return res def upload_file_cunked(self, file_info: utils.FileInfo, close_after: bool = False) -> Media: """ Try to grab the FileInfo of the specified media file, checks if it can be uploaded to twitter and then tries to upload the file via the chunked twitter media upload endpoint. **Parameters** - `media : str` Either a local file location or a HTTP(S) URL to an online file resource. - `close_after: bool` Wether the file info reader should be closed after upload or not. *Default: `False`* **Returns** - `Media` Result media object. """ # --- INIT ------------------------------------------------------------ res = self.upload_media_request( command='INIT', params={ 'total_bytes': file_info.size, 'media_type': file_info.mime_type, }) res_data = res.json() if 'media_id_string' not in res_data: raise Exception('"media_id_string" not contained in response body') media_id = res_data['media_id'] # --- APPEND ---------------------------------------------------------- boundary_uuid = uuid.uuid4().hex boundary = '--{0}'.format(boundary_uuid).encode('utf8') for chunk in utils.chunk_file(file_info, self.UPLOAD_CHUNK_SIZE): body_data = ( # COMMAND boundary, b'Content-Disposition: form-data; name="command"', b'', b'APPEND', # MEDIA_ID boundary, b'Content-Disposition: form-data; name="media_id"', b'', str(media_id).encode('utf8'), # MEDIA boundary, 'Content-Disposition: form-data; name="media"; filename="{0!r}"' .format(file_info.file_name).encode('utf8'), b'Content-Type: application/octet-stream', b'', chunk.data, # SEGMENT_INDEX boundary, b'Content-Disposition: form-data; name="segment_index"', b'', str(chunk.index).encode('utf8'), boundary + b'--' ) body = b'\r\n'.join(body_data) self.upload_media_request( headers={ 'Content-Type': 'multipart/form-data; boundary={0}' .format(boundary_uuid), 'Content-Length': str(len(body)), }, raw=body) # --- FINALIZE -------------------------------------------------------- res = self.upload_media_request( command='FINALIZE', params={ 'media_id': str(media_id), }) if close_after: file_info.close() if res == None: raise NoneResponseException() return Media(res.json()) def upload_attachments(self, media: list, close_after: bool = False) -> Iterator[Media]: """ Upload a list of media using chunked upload. The list of media can only contain either 4 photos, 1 gif or 1 video. Everything else will raise an exception. **Parameters** - `media: list` List of media objects as path to a local file, as URI to an online file which will be downloaded and atatched then or an existing FileInfo object. - `close_after: bool` Wether to close each opened file handler after uploading or not. *Default: `False`* **Returns** - `Iterator[Media]` Iterator of uploaded Media objects. """ max_attachable = None files = [] for m in media: file_info = m if type(m) == FileInfo else utils.try_get_file(m) i = utils.check_upload_compatibility(file_info) files.append(file_info) if not max_attachable: max_attachable = i if max_attachable and max_attachable > len(media): raise Exception('you can only attach up to {} files using this attachment type.' .format(max_attachable)) for f in files: yield self.upload_file_cunked(f, close_after=close_after) ################ # STATUSES API # ################ def statuses_update(self, status: str, media: [list, str] = None, **kwargs) -> Tweet: """ Create a Tweet with specified content. **Parameters** - `status : str` The status text. This can not be None, but empty ('') if you do not want to have text content in your tweet. - `img : str` Image media identifier. This can be either a local file location or an online file linked by a HHTP(S) URL. - `**kwargs` Additional keyword arguments which will be directly passed to the request arguments. You should only use valid arguments supported by the POST statuses/update endpoint: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update **Returns** - `Tweet` The result Tweet object. """ data = kwargs data['status'] = status if media is not None: if type(media) is not list: media = [media] media_objs = [] for media_obj in self.upload_attachments(media): media_objs.append(media_obj) data['media_ids'] = ','.join([m.id_str for m in media_objs]) res = self.request('POST', 'statuses/update.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) def statuses_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Delete a tweet. **Parameters** - `id: [str, int]` ID of the tweet to delete. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The tweet ofbject which was deleted. """ data = kwargs res = self.request('POST', 'statuses/destroy/{}.json'.format(id), data=data) if not res: raise NoneResponseException() return Tweet(res) def statuses_show(self, id: [str, int], **kwargs) -> Tweet: """ Fetches a single tweet by its ID. The tweets author user object will be in the response Tweet object. If there was no tweet found by the specified ID, the result will be `None`. **Parameters** - `id: [str, int]` ID of the Tweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Result Tweet obect or `None`. """ data = kwargs data['id'] = id res = self.request('GET', 'statuses/show.json', params=data) if not res: raise NoneResponseException() return Tweet(res, self) def statuses_lookup(self, ids: List[str], raise_on_none: bool = False, **kwargs) -> Dict[str, Tweet]: """ Get details about up to 100 tweets. The returned dictionary keys represent the original requested IDs of the Tweets and are paired with the corresponding Tweet object, if found. Defaultly, the value can be `None` if no Tweet was found matching the given ID. **Parameters** - `ids: list` List of tweet IDs to be fetched. - `raise_on_none: boolean` Raise an `NoneResponseException` exception if a Tweet could not be fetched for a given ID. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[[int, str], Tweet]` Tweet IDs as keys paired with the corresponding result Tweet object, which can be `None`. """ ln = len(ids) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException('index must be in range [1, 100]') data = kwargs data['id'] = ','.join([str(id) for id in ids]) data['map'] = True res = self.request('GET', 'statuses/lookup.json', params=data) if not res or 'id' not in res: raise NoneResponseException() tweets = {} for tid, obj in res.get('id').items(): if not obj and raise_on_none: raise NoneResponseException() tweets[tid] = Tweet(obj, self) if obj else None return tweets def statuses_retweet(self, id: [str, int], **kwargs) -> Tweet: """ Retweet a Tweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to retweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Resulting Tweet object containing retweet data information. """ data = kwargs res = self.request('POST', 'statuses/retweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res, self) def statuses_unretweet(self, id: [str, int], **kwargs) -> Tweet: """ Revoke a retweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to unretweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Tweet object of the revoked retweet. """ data = kwargs res = self.request('POST', 'statuses/unretweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res) def statuses_retweets(self, id: [str, int], count: int = None, **kwargs) -> List[Tweet]: """ Returns a list of up to 100 of the most recent retweets of the specified Tweet by ID. **Parameters** - `id: [str, int]` The ID of the Tweet to get the list of retweets from. - `count: int` The ammount of retweets to be collected (in range of [1, 100]). *Default`: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` List of Tweet objects representing the retweets details. """ data = kwargs if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count res = self.request('GET', 'statuses/retweets/{}.json'.format(id), params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res] def statuses_retweets_of_me(self, count: int = None, since_id: [int, str] = None, max_id: [int, str] = None, include_entities: bool = True, include_user_entities: bool = True, **kwargs) -> List[Tweet]: """ Returns the most recent retweets of tweets authored by the authenticated user. This is a subset of the user timeline. **Parameters** - `count: int` Number of records to be retrieved in range of [1, 100]. If omitted, 20 will be assumed. *Default: `None`* - `since_id: [int, str]` Results only after the given tweet ID (which means more recent then the given tweet) *Default: `None`* - `max_id: [int, str]` Retuned results will be less than or equal the given tweet ID. *Default: `None`* - `include_entities: bool` Include tweet entities in result objects. *Default: `True`* - `include_user_entities: bool` Include user objects in result objects. *Default: `True`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` Result list of Tweet objects. """ data = kwargs data['include_entities'] = include_entities data['include_user_entities'] = include_user_entities if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count if since_id: data['since_id'] = since_id if max_id: data['max_id'] = max_id res = self.request('GET', 'statuses/retweets_of_mine', params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res] ################# # FAVORITES API # ################# def favorites_create(self, id: [str, int], **kwargs) -> Tweet: """ Favorite (like) a Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired Tweet to favorite/like. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The favorized/liked Tweets object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/create.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) def favorites_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Unfavorite (unlike) a liked Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired liked tweet to unfavorite/unlike. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The unfavorized/unliked Tweet object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/destroy.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) ############# # USERS API # ############# def users_show(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> User: """ Fetches a single user by its ID or screen name (Twitter handle). **Parameters** - `id: [str, int]` ID of the desired user. *Default: `None`* - `screen_name: str` Screen name (handle) of the desired user. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `User` Fetched user object. """ if not id and not screen_name: raise ParameterNoneException() data = kwargs if id: data['user_id'] = id if screen_name: data['screen_name'] = screen_name res = self.request('GET', 'users/show.json', params=data) if not res: raise NoneResponseException() return User(res, self) def users_lookup(self, ids: List[str] = None, screen_names: List[str] = None, **kwargs) -> Dict[str, User]: """ Fetches up to 100 users by their ids OR screen names (Twitter handles). IDs and screen names can no be mixed. Screen names value list will be prefered. **Parameters** - `ids: List[str]` List of desired user IDs. *Default: `none`* - `screen_names: List[str]` List of desired user screen names (handles). *Default: `none`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[str, User]` A dict of user IDs and user names as keys linked to the corresponding user objects. So, for each user, there are two values in the dict. Firstly linked to an ID key and secondly linked to the users user name as key. """ if not ids and not screen_names: raise ParameterNoneException() data = kwargs ln = 0 if ids and len(ids) > 0: data['user_id'] = ','.join([str(id) for id in ids]) ln += len(ids) if screen_names and len(screen_names) > 0: data['screen_name'] = ','.join(screen_names) ln += len(screen_names) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException( 'ids + screen_names length must be in range [1, 100]') res = self.request('GET', 'users/lookup.json', params=data) if not res: return NoneResponseException() users = {} for r in res: user = User(r, self) users[user.id_str] = user users[user.username or user.screen_name] = user return users def followers_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all followers of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all followers of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('followers/ids.json', 'ids', params=params) def followers_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users following the target user. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all followers of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('followers/list.json', 'users', params=params) return [User(r, self) for r in results] def friends_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all friends (the user is following) of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all friends of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('friends/ids.json', 'ids', params=params) def friends_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users the target user is following (friends). **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all friends of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('friends/list.json', 'users', params=params) return [User(r, self) for r in results]
Classes
class APISession
Barebone Twitter API wrapper representing Twitters API endpoints directly. Initialized with App Credentials for authentication and authorization.
Parameters
credentials: Credentials
APP or User credentials to authenticate against the Twitter API.
class APISession: """ Barebone Twitter API wrapper representing Twitters API endpoints directly. Initialized with App Credentials for authentication and authorization. **Parameters** - `credentials: Credentials` APP or User credentials to authenticate against the Twitter API. """ API_ROOT_URI = 'https://api.twitter.com' API_VERSION = '1.1' API_UPLOAD_ROOT_URI = 'https://upload.twitter.com/1.1' UPLOAD_CHUNK_SIZE = 1024 * 1024 * 1024 # 1 MiB def __init__(self, credentials: Credentials): self._session = requests.Session() self._credentials = credentials self._session = requests.Session() if all([self._credentials.access_token_key, self._credentials.access_token_secret, self._credentials.consumer_key, self._credentials.consumer_secret]): self._oauth = self._credentials.to_oauth1() else: self.obtain_user_context_token() ############################ # GENERAL REQUEST HANDLING # ############################ def request(self, method: str, resource_path: str, **kwargs) -> object: """ Request the Twitter API with the defined authentication credentials. This method raises an exception on failed authentication or request. **Parameters** - `method : str` Request method. - `resource_path : str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `**kwargs` Optional arguments passed to request.request() **Returns** - `Object` JSON-parsed response body. """ res = self._session.request( auth=self._oauth, method=method, url='{0}/{1}/{2}'.format(self.API_ROOT_URI, self.API_VERSION, (resource_path[1:] if resource_path.startswith('/') else resource_path)), **kwargs) if res.status_code == 429: raise RateLimitException() if not res.ok: raise Exception('request failed with status code {} and message: {}' .format(res.status_code, res.text)) return res.json() def cursor_request(self, resource_path: str, expected_key: str, count: int = 200, params: dict = {}) -> List[object]: """ Issues cursored GET requests to the Twitter API follwoing the respond cursor for next requests returning the entire response objects as one array of objects. **Parameters** - `resource_path: str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `expected_key: str` Key expected to be contained in the response grouping all response objects. - `count: int` Ammount of objects which will be requested at once. Must be in range of [1, 200]. - `params: dict` Parameters passed to the single GET requests. **Returns** - `List[object]` List of concat objects expected in `expected_key`. """ results = [] cursor = -1 params['count'] = count if count > 200 or count < 1: raise ParameterOutOfBoundsException("must be in range of [1, 200]") while cursor is not 0: params['cursor'] = cursor res = self.request('GET', resource_path, params=params) data = res.get(expected_key) if data: results.extend(data) cursor = res.get('next_cursor') return results def obtain_user_context_token(self): """ Collect a user context bearer token from client_key and client_secret and sets it as OAuth2 authentication method for further requests. """ key = urllib.parse.quote_plus(self._credentials.consumer_key) secret = urllib.parse.quote_plus(self._credentials.consumer_secret) basic_token = base64.b64encode( '{0}:{1}'.format(key, secret).encode('utf8')).decode('utf8') res = self._session.post( url='{0}/oauth2/token'.format(self.API_ROOT_URI), headers={ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Authorization': 'Basic {0}'.format(basic_token), }, data={ 'grant_type': 'client_credentials', }) if res.status_code == 429: raise RateLimitException() if res.status_code != 200: raise Exception('request failed with status code {0}'.format(res.status_code)) body = res.json() self._oauth = OAuth2(token=body) ############## # UPLOAD API # ############## def upload_media_request(self, command=None, params={}, raw=None, **kwargs) -> object: """ Wrap a request to initiate, append or finalize a chunked media upload. **Parameters** - `command : str` Must be 'INIT', 'APPEND' or 'FINALIZE'. If 'raw' is set, this must not be passed. - `params : dict` Request body parameters. If 'raw' is set, this must not be passed. - `raw` Raw data used as reuqest body. This option overwrites 'command' and 'params'. - `**kwargs:` Additional arguments which will be passed to the request method. **Returns** - `object` Resulting FINALIZE response object. """ if raw is None: params['command'] = command params = utils.sort_dict_alphabetically(params) res = self._session.post( auth=self._oauth, url='{0}/media/upload.json'.format(self.API_UPLOAD_ROOT_URI), data=(params if raw is None else raw), **kwargs) if res.status_code == 429: raise RateLimitException() if res.status_code < 200 or res.status_code >= 300: raise Exception('request failed with status code {0}'.format(res.status_code)) return res def upload_file_cunked(self, file_info: utils.FileInfo, close_after: bool = False) -> Media: """ Try to grab the FileInfo of the specified media file, checks if it can be uploaded to twitter and then tries to upload the file via the chunked twitter media upload endpoint. **Parameters** - `media : str` Either a local file location or a HTTP(S) URL to an online file resource. - `close_after: bool` Wether the file info reader should be closed after upload or not. *Default: `False`* **Returns** - `Media` Result media object. """ # --- INIT ------------------------------------------------------------ res = self.upload_media_request( command='INIT', params={ 'total_bytes': file_info.size, 'media_type': file_info.mime_type, }) res_data = res.json() if 'media_id_string' not in res_data: raise Exception('"media_id_string" not contained in response body') media_id = res_data['media_id'] # --- APPEND ---------------------------------------------------------- boundary_uuid = uuid.uuid4().hex boundary = '--{0}'.format(boundary_uuid).encode('utf8') for chunk in utils.chunk_file(file_info, self.UPLOAD_CHUNK_SIZE): body_data = ( # COMMAND boundary, b'Content-Disposition: form-data; name="command"', b'', b'APPEND', # MEDIA_ID boundary, b'Content-Disposition: form-data; name="media_id"', b'', str(media_id).encode('utf8'), # MEDIA boundary, 'Content-Disposition: form-data; name="media"; filename="{0!r}"' .format(file_info.file_name).encode('utf8'), b'Content-Type: application/octet-stream', b'', chunk.data, # SEGMENT_INDEX boundary, b'Content-Disposition: form-data; name="segment_index"', b'', str(chunk.index).encode('utf8'), boundary + b'--' ) body = b'\r\n'.join(body_data) self.upload_media_request( headers={ 'Content-Type': 'multipart/form-data; boundary={0}' .format(boundary_uuid), 'Content-Length': str(len(body)), }, raw=body) # --- FINALIZE -------------------------------------------------------- res = self.upload_media_request( command='FINALIZE', params={ 'media_id': str(media_id), }) if close_after: file_info.close() if res == None: raise NoneResponseException() return Media(res.json()) def upload_attachments(self, media: list, close_after: bool = False) -> Iterator[Media]: """ Upload a list of media using chunked upload. The list of media can only contain either 4 photos, 1 gif or 1 video. Everything else will raise an exception. **Parameters** - `media: list` List of media objects as path to a local file, as URI to an online file which will be downloaded and atatched then or an existing FileInfo object. - `close_after: bool` Wether to close each opened file handler after uploading or not. *Default: `False`* **Returns** - `Iterator[Media]` Iterator of uploaded Media objects. """ max_attachable = None files = [] for m in media: file_info = m if type(m) == FileInfo else utils.try_get_file(m) i = utils.check_upload_compatibility(file_info) files.append(file_info) if not max_attachable: max_attachable = i if max_attachable and max_attachable > len(media): raise Exception('you can only attach up to {} files using this attachment type.' .format(max_attachable)) for f in files: yield self.upload_file_cunked(f, close_after=close_after) ################ # STATUSES API # ################ def statuses_update(self, status: str, media: [list, str] = None, **kwargs) -> Tweet: """ Create a Tweet with specified content. **Parameters** - `status : str` The status text. This can not be None, but empty ('') if you do not want to have text content in your tweet. - `img : str` Image media identifier. This can be either a local file location or an online file linked by a HHTP(S) URL. - `**kwargs` Additional keyword arguments which will be directly passed to the request arguments. You should only use valid arguments supported by the POST statuses/update endpoint: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update **Returns** - `Tweet` The result Tweet object. """ data = kwargs data['status'] = status if media is not None: if type(media) is not list: media = [media] media_objs = [] for media_obj in self.upload_attachments(media): media_objs.append(media_obj) data['media_ids'] = ','.join([m.id_str for m in media_objs]) res = self.request('POST', 'statuses/update.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) def statuses_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Delete a tweet. **Parameters** - `id: [str, int]` ID of the tweet to delete. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The tweet ofbject which was deleted. """ data = kwargs res = self.request('POST', 'statuses/destroy/{}.json'.format(id), data=data) if not res: raise NoneResponseException() return Tweet(res) def statuses_show(self, id: [str, int], **kwargs) -> Tweet: """ Fetches a single tweet by its ID. The tweets author user object will be in the response Tweet object. If there was no tweet found by the specified ID, the result will be `None`. **Parameters** - `id: [str, int]` ID of the Tweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Result Tweet obect or `None`. """ data = kwargs data['id'] = id res = self.request('GET', 'statuses/show.json', params=data) if not res: raise NoneResponseException() return Tweet(res, self) def statuses_lookup(self, ids: List[str], raise_on_none: bool = False, **kwargs) -> Dict[str, Tweet]: """ Get details about up to 100 tweets. The returned dictionary keys represent the original requested IDs of the Tweets and are paired with the corresponding Tweet object, if found. Defaultly, the value can be `None` if no Tweet was found matching the given ID. **Parameters** - `ids: list` List of tweet IDs to be fetched. - `raise_on_none: boolean` Raise an `NoneResponseException` exception if a Tweet could not be fetched for a given ID. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[[int, str], Tweet]` Tweet IDs as keys paired with the corresponding result Tweet object, which can be `None`. """ ln = len(ids) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException('index must be in range [1, 100]') data = kwargs data['id'] = ','.join([str(id) for id in ids]) data['map'] = True res = self.request('GET', 'statuses/lookup.json', params=data) if not res or 'id' not in res: raise NoneResponseException() tweets = {} for tid, obj in res.get('id').items(): if not obj and raise_on_none: raise NoneResponseException() tweets[tid] = Tweet(obj, self) if obj else None return tweets def statuses_retweet(self, id: [str, int], **kwargs) -> Tweet: """ Retweet a Tweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to retweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Resulting Tweet object containing retweet data information. """ data = kwargs res = self.request('POST', 'statuses/retweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res, self) def statuses_unretweet(self, id: [str, int], **kwargs) -> Tweet: """ Revoke a retweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to unretweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Tweet object of the revoked retweet. """ data = kwargs res = self.request('POST', 'statuses/unretweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res) def statuses_retweets(self, id: [str, int], count: int = None, **kwargs) -> List[Tweet]: """ Returns a list of up to 100 of the most recent retweets of the specified Tweet by ID. **Parameters** - `id: [str, int]` The ID of the Tweet to get the list of retweets from. - `count: int` The ammount of retweets to be collected (in range of [1, 100]). *Default`: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` List of Tweet objects representing the retweets details. """ data = kwargs if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count res = self.request('GET', 'statuses/retweets/{}.json'.format(id), params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res] def statuses_retweets_of_me(self, count: int = None, since_id: [int, str] = None, max_id: [int, str] = None, include_entities: bool = True, include_user_entities: bool = True, **kwargs) -> List[Tweet]: """ Returns the most recent retweets of tweets authored by the authenticated user. This is a subset of the user timeline. **Parameters** - `count: int` Number of records to be retrieved in range of [1, 100]. If omitted, 20 will be assumed. *Default: `None`* - `since_id: [int, str]` Results only after the given tweet ID (which means more recent then the given tweet) *Default: `None`* - `max_id: [int, str]` Retuned results will be less than or equal the given tweet ID. *Default: `None`* - `include_entities: bool` Include tweet entities in result objects. *Default: `True`* - `include_user_entities: bool` Include user objects in result objects. *Default: `True`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` Result list of Tweet objects. """ data = kwargs data['include_entities'] = include_entities data['include_user_entities'] = include_user_entities if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count if since_id: data['since_id'] = since_id if max_id: data['max_id'] = max_id res = self.request('GET', 'statuses/retweets_of_mine', params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res] ################# # FAVORITES API # ################# def favorites_create(self, id: [str, int], **kwargs) -> Tweet: """ Favorite (like) a Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired Tweet to favorite/like. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The favorized/liked Tweets object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/create.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) def favorites_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Unfavorite (unlike) a liked Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired liked tweet to unfavorite/unlike. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The unfavorized/unliked Tweet object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/destroy.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self) ############# # USERS API # ############# def users_show(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> User: """ Fetches a single user by its ID or screen name (Twitter handle). **Parameters** - `id: [str, int]` ID of the desired user. *Default: `None`* - `screen_name: str` Screen name (handle) of the desired user. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `User` Fetched user object. """ if not id and not screen_name: raise ParameterNoneException() data = kwargs if id: data['user_id'] = id if screen_name: data['screen_name'] = screen_name res = self.request('GET', 'users/show.json', params=data) if not res: raise NoneResponseException() return User(res, self) def users_lookup(self, ids: List[str] = None, screen_names: List[str] = None, **kwargs) -> Dict[str, User]: """ Fetches up to 100 users by their ids OR screen names (Twitter handles). IDs and screen names can no be mixed. Screen names value list will be prefered. **Parameters** - `ids: List[str]` List of desired user IDs. *Default: `none`* - `screen_names: List[str]` List of desired user screen names (handles). *Default: `none`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[str, User]` A dict of user IDs and user names as keys linked to the corresponding user objects. So, for each user, there are two values in the dict. Firstly linked to an ID key and secondly linked to the users user name as key. """ if not ids and not screen_names: raise ParameterNoneException() data = kwargs ln = 0 if ids and len(ids) > 0: data['user_id'] = ','.join([str(id) for id in ids]) ln += len(ids) if screen_names and len(screen_names) > 0: data['screen_name'] = ','.join(screen_names) ln += len(screen_names) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException( 'ids + screen_names length must be in range [1, 100]') res = self.request('GET', 'users/lookup.json', params=data) if not res: return NoneResponseException() users = {} for r in res: user = User(r, self) users[user.id_str] = user users[user.username or user.screen_name] = user return users def followers_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all followers of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all followers of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('followers/ids.json', 'ids', params=params) def followers_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users following the target user. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all followers of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('followers/list.json', 'users', params=params) return [User(r, self) for r in results] def friends_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all friends (the user is following) of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all friends of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('friends/ids.json', 'ids', params=params) def friends_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users the target user is following (friends). **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all friends of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('friends/list.json', 'users', params=params) return [User(r, self) for r in results]
Ancestors (in MRO)
Class variables
var API_ROOT_URI
var API_UPLOAD_ROOT_URI
var API_VERSION
var UPLOAD_CHUNK_SIZE
Static methods
def __init__(
self, credentials)
Initialize self. See help(type(self)) for accurate signature.
def __init__(self, credentials: Credentials): self._session = requests.Session() self._credentials = credentials self._session = requests.Session() if all([self._credentials.access_token_key, self._credentials.access_token_secret, self._credentials.consumer_key, self._credentials.consumer_secret]): self._oauth = self._credentials.to_oauth1() else: self.obtain_user_context_token()
def cursor_request(
self, resource_path, expected_key, count=200, params={})
Issues cursored GET requests to the Twitter API follwoing the respond cursor for next requests returning the entire response objects as one array of objects.
Parameters
-
resource_path: str
Path to the requested resource (without root URI). Leading '/' will be cut off. -
expected_key: str
Key expected to be contained in the response grouping all response objects. -
count: int
Ammount of objects which will be requested at once. Must be in range of [1, 200]. -
params: dict
Parameters passed to the single GET requests.
Returns
List[object]
List of concat objects expected inexpected_key
.
def cursor_request(self, resource_path: str, expected_key: str, count: int = 200, params: dict = {}) -> List[object]: """ Issues cursored GET requests to the Twitter API follwoing the respond cursor for next requests returning the entire response objects as one array of objects. **Parameters** - `resource_path: str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `expected_key: str` Key expected to be contained in the response grouping all response objects. - `count: int` Ammount of objects which will be requested at once. Must be in range of [1, 200]. - `params: dict` Parameters passed to the single GET requests. **Returns** - `List[object]` List of concat objects expected in `expected_key`. """ results = [] cursor = -1 params['count'] = count if count > 200 or count < 1: raise ParameterOutOfBoundsException("must be in range of [1, 200]") while cursor is not 0: params['cursor'] = cursor res = self.request('GET', resource_path, params=params) data = res.get(expected_key) if data: results.extend(data) cursor = res.get('next_cursor') return results
def favorites_create(
self, id, **kwargs)
Favorite (like) a Tweet by its specified ID.
Parameters
-
id: [str, int]
The ID of the desired Tweet to favorite/like. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
The favorized/liked Tweets object.
def favorites_create(self, id: [str, int], **kwargs) -> Tweet: """ Favorite (like) a Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired Tweet to favorite/like. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The favorized/liked Tweets object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/create.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self)
def favorites_destroy(
self, id, **kwargs)
Unfavorite (unlike) a liked Tweet by its specified ID.
Parameters
-
id: [str, int]
The ID of the desired liked tweet to unfavorite/unlike. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
The unfavorized/unliked Tweet object.
def favorites_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Unfavorite (unlike) a liked Tweet by its specified ID. **Parameters** - `id: [str, int]` The ID of the desired liked tweet to unfavorite/unlike. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The unfavorized/unliked Tweet object. """ data = kwargs data['id'] = id res = self.request('POST', 'favorites/destroy.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self)
def followers_ids(
self, id=None, screen_name=None, **kwargs)
Returns a list of user IDs (as strings) of all followers of the user specified by its ID.
Parameters
-
id: [str, int]
ID of the user to get followers list from.
Default:None
-
screen_name: str
Screen name (handle) of the user to get followers list from.
Default:None
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[str]
List of IDs of all followers of the desired user.
def followers_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all followers of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all followers of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('followers/ids.json', 'ids', params=params)
def followers_list(
self, id=None, screen_name=None, **kwargs)
Returns a list of User objects of the users following the target user.
Parameters
-
id: [str, int]
ID of the user to get followers list from.
Default:None
-
screen_name: str
Screen name (handle) of the user to get followers list from.
Default:None
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[User]
List of User objects of all followers of the target user.
def followers_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users following the target user. **Parameters** - `id: [str, int]` ID of the user to get followers list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get followers list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all followers of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('followers/list.json', 'users', params=params) return [User(r, self) for r in results]
def friends_ids(
self, id=None, screen_name=None, **kwargs)
Returns a list of user IDs (as strings) of all friends (the user is following) of the user specified by its ID.
Parameters
-
id: [str, int]
ID of the user to get friends list from.
Default:None
-
screen_name: str
Screen name (handle) of the user to get friends list from.
Default:None
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[str]
List of IDs of all friends of the desired user.
def friends_ids(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[str]: """ Returns a list of user IDs (as strings) of all friends (the user is following) of the user specified by its ID. **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[str]` List of IDs of all friends of the desired user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs params['stringify_ids'] = True if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name return self.cursor_request('friends/ids.json', 'ids', params=params)
def friends_list(
self, id=None, screen_name=None, **kwargs)
Returns a list of User objects of the users the target user is following (friends).
Parameters
-
id: [str, int]
ID of the user to get friends list from.
Default:None
-
screen_name: str
Screen name (handle) of the user to get friends list from.
Default:None
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[User]
List of User objects of all friends of the target user.
def friends_list(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> List[User]: """ Returns a list of User objects of the users the target user is following (friends). **Parameters** - `id: [str, int]` ID of the user to get friends list from. *Default: `None`* - `screen_name: str` Screen name (handle) of the user to get friends list from. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[User]` List of User objects of all friends of the target user. """ if not id and not screen_name: raise ParameterNoneException() params = kwargs if id: params['user_id'] = id if screen_name: params['screen_name'] = screen_name results = self.cursor_request('friends/list.json', 'users', params=params) return [User(r, self) for r in results]
def obtain_user_context_token(
self)
Collect a user context bearer token from client_key and client_secret and sets it as OAuth2 authentication method for further requests.
def obtain_user_context_token(self): """ Collect a user context bearer token from client_key and client_secret and sets it as OAuth2 authentication method for further requests. """ key = urllib.parse.quote_plus(self._credentials.consumer_key) secret = urllib.parse.quote_plus(self._credentials.consumer_secret) basic_token = base64.b64encode( '{0}:{1}'.format(key, secret).encode('utf8')).decode('utf8') res = self._session.post( url='{0}/oauth2/token'.format(self.API_ROOT_URI), headers={ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Authorization': 'Basic {0}'.format(basic_token), }, data={ 'grant_type': 'client_credentials', }) if res.status_code == 429: raise RateLimitException() if res.status_code != 200: raise Exception('request failed with status code {0}'.format(res.status_code)) body = res.json() self._oauth = OAuth2(token=body)
def request(
self, method, resource_path, **kwargs)
Request the Twitter API with the defined authentication credentials. This method raises an exception on failed authentication or request.
Parameters
-
method : str
Request method. -
resource_path : str
Path to the requested resource (without root URI). Leading '/' will be cut off. -
**kwargs
Optional arguments passed to request.request()
Returns
Object
JSON-parsed response body.
def request(self, method: str, resource_path: str, **kwargs) -> object: """ Request the Twitter API with the defined authentication credentials. This method raises an exception on failed authentication or request. **Parameters** - `method : str` Request method. - `resource_path : str` Path to the requested resource (without root URI). Leading '/' will be cut off. - `**kwargs` Optional arguments passed to request.request() **Returns** - `Object` JSON-parsed response body. """ res = self._session.request( auth=self._oauth, method=method, url='{0}/{1}/{2}'.format(self.API_ROOT_URI, self.API_VERSION, (resource_path[1:] if resource_path.startswith('/') else resource_path)), **kwargs) if res.status_code == 429: raise RateLimitException() if not res.ok: raise Exception('request failed with status code {} and message: {}' .format(res.status_code, res.text)) return res.json()
def statuses_destroy(
self, id, **kwargs)
Delete a tweet.
Parameters
-
id: [str, int]
ID of the tweet to delete. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
The tweet ofbject which was deleted.
def statuses_destroy(self, id: [str, int], **kwargs) -> Tweet: """ Delete a tweet. **Parameters** - `id: [str, int]` ID of the tweet to delete. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` The tweet ofbject which was deleted. """ data = kwargs res = self.request('POST', 'statuses/destroy/{}.json'.format(id), data=data) if not res: raise NoneResponseException() return Tweet(res)
def statuses_lookup(
self, ids, raise_on_none=False, **kwargs)
Get details about up to 100 tweets. The returned
dictionary keys represent the original requested
IDs of the Tweets and are paired with the
corresponding Tweet object, if found. Defaultly,
the value can be None
if no Tweet was found
matching the given ID.
Parameters
-
ids: list
List of tweet IDs to be fetched. -
raise_on_none: boolean
Raise anNoneResponseException
exception if a Tweet could not be fetched for a given ID. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Dict[[int, str], Tweet]
Tweet IDs as keys paired with the corresponding result Tweet object, which can beNone
.
def statuses_lookup(self, ids: List[str], raise_on_none: bool = False, **kwargs) -> Dict[str, Tweet]: """ Get details about up to 100 tweets. The returned dictionary keys represent the original requested IDs of the Tweets and are paired with the corresponding Tweet object, if found. Defaultly, the value can be `None` if no Tweet was found matching the given ID. **Parameters** - `ids: list` List of tweet IDs to be fetched. - `raise_on_none: boolean` Raise an `NoneResponseException` exception if a Tweet could not be fetched for a given ID. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[[int, str], Tweet]` Tweet IDs as keys paired with the corresponding result Tweet object, which can be `None`. """ ln = len(ids) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException('index must be in range [1, 100]') data = kwargs data['id'] = ','.join([str(id) for id in ids]) data['map'] = True res = self.request('GET', 'statuses/lookup.json', params=data) if not res or 'id' not in res: raise NoneResponseException() tweets = {} for tid, obj in res.get('id').items(): if not obj and raise_on_none: raise NoneResponseException() tweets[tid] = Tweet(obj, self) if obj else None return tweets
def statuses_retweet(
self, id, **kwargs)
Retweet a Tweet by its ID.
Parameters
-
id: [str, int]
ID of the Tweet to retweet. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
Resulting Tweet object containing retweet data information.
def statuses_retweet(self, id: [str, int], **kwargs) -> Tweet: """ Retweet a Tweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to retweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Resulting Tweet object containing retweet data information. """ data = kwargs res = self.request('POST', 'statuses/retweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res, self)
def statuses_retweets(
self, id, count=None, **kwargs)
Returns a list of up to 100 of the most recent retweets of the specified Tweet by ID.
Parameters
-
id: [str, int]
The ID of the Tweet to get the list of retweets from. -
count: int
The ammount of retweets to be collected (in range of [1, 100]).
Default:
None` -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[Tweet]
List of Tweet objects representing the retweets details.
def statuses_retweets(self, id: [str, int], count: int = None, **kwargs) -> List[Tweet]: """ Returns a list of up to 100 of the most recent retweets of the specified Tweet by ID. **Parameters** - `id: [str, int]` The ID of the Tweet to get the list of retweets from. - `count: int` The ammount of retweets to be collected (in range of [1, 100]). *Default`: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` List of Tweet objects representing the retweets details. """ data = kwargs if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count res = self.request('GET', 'statuses/retweets/{}.json'.format(id), params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res]
def statuses_retweets_of_me(
self, count=None, since_id=None, max_id=None, include_entities=True, include_user_entities=True, **kwargs)
Returns the most recent retweets of tweets authored by the authenticated user. This is a subset of the user timeline.
Parameters
-
count: int
Number of records to be retrieved in range of [1, 100]. If omitted, 20 will be assumed.
Default:None
-
since_id: [int, str]
Results only after the given tweet ID (which means more recent then the given tweet)
Default:None
-
max_id: [int, str]
Retuned results will be less than or equal the given tweet ID.
Default:None
-
include_entities: bool
Include tweet entities in result objects.
Default:True
-
include_user_entities: bool
Include user objects in result objects.
Default:True
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
List[Tweet]
Result list of Tweet objects.
def statuses_retweets_of_me(self, count: int = None, since_id: [int, str] = None, max_id: [int, str] = None, include_entities: bool = True, include_user_entities: bool = True, **kwargs) -> List[Tweet]: """ Returns the most recent retweets of tweets authored by the authenticated user. This is a subset of the user timeline. **Parameters** - `count: int` Number of records to be retrieved in range of [1, 100]. If omitted, 20 will be assumed. *Default: `None`* - `since_id: [int, str]` Results only after the given tweet ID (which means more recent then the given tweet) *Default: `None`* - `max_id: [int, str]` Retuned results will be less than or equal the given tweet ID. *Default: `None`* - `include_entities: bool` Include tweet entities in result objects. *Default: `True`* - `include_user_entities: bool` Include user objects in result objects. *Default: `True`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `List[Tweet]` Result list of Tweet objects. """ data = kwargs data['include_entities'] = include_entities data['include_user_entities'] = include_user_entities if count: if count < 1 or count > 100: raise ParameterOutOfBoundsException('count must be in range of [1, 100]') data['count'] = count if since_id: data['since_id'] = since_id if max_id: data['max_id'] = max_id res = self.request('GET', 'statuses/retweets_of_mine', params=data) if not res: raise NoneResponseException() return [Tweet(r, self) for r in res]
def statuses_show(
self, id, **kwargs)
Fetches a single tweet by its ID. The tweets author
user object will be in the response Tweet object.
If there was no tweet found by the specified ID, the
result will be None
.
Parameters
-
id: [str, int]
ID of the Tweet. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
Result Tweet obect orNone
.
def statuses_show(self, id: [str, int], **kwargs) -> Tweet: """ Fetches a single tweet by its ID. The tweets author user object will be in the response Tweet object. If there was no tweet found by the specified ID, the result will be `None`. **Parameters** - `id: [str, int]` ID of the Tweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Result Tweet obect or `None`. """ data = kwargs data['id'] = id res = self.request('GET', 'statuses/show.json', params=data) if not res: raise NoneResponseException() return Tweet(res, self)
def statuses_unretweet(
self, id, **kwargs)
Revoke a retweet by its ID.
Parameters
-
id: [str, int]
ID of the Tweet to unretweet. -
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Tweet
Tweet object of the revoked retweet.
def statuses_unretweet(self, id: [str, int], **kwargs) -> Tweet: """ Revoke a retweet by its ID. **Parameters** - `id: [str, int]` ID of the Tweet to unretweet. - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Tweet` Tweet object of the revoked retweet. """ data = kwargs res = self.request('POST', 'statuses/unretweet/{}.json'.format(id), params=data) if not res: return NoneResponseException() return Tweet(res)
def statuses_update(
self, status, media=None, **kwargs)
Create a Tweet with specified content.
Parameters
-
status : str
The status text. This can not be None, but empty ('') if you do not want to have text content in your tweet. -
img : str
Image media identifier. This can be either a local file location or an online file linked by a HHTP(S) URL. -
**kwargs
Additional keyword arguments which will be directly passed to the request arguments. You should only use valid arguments supported by the POST statuses/update endpoint: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update
Returns
Tweet
The result Tweet object.
def statuses_update(self, status: str, media: [list, str] = None, **kwargs) -> Tweet: """ Create a Tweet with specified content. **Parameters** - `status : str` The status text. This can not be None, but empty ('') if you do not want to have text content in your tweet. - `img : str` Image media identifier. This can be either a local file location or an online file linked by a HHTP(S) URL. - `**kwargs` Additional keyword arguments which will be directly passed to the request arguments. You should only use valid arguments supported by the POST statuses/update endpoint: https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/post-statuses-update **Returns** - `Tweet` The result Tweet object. """ data = kwargs data['status'] = status if media is not None: if type(media) is not list: media = [media] media_objs = [] for media_obj in self.upload_attachments(media): media_objs.append(media_obj) data['media_ids'] = ','.join([m.id_str for m in media_objs]) res = self.request('POST', 'statuses/update.json', data=data) if not res: raise NoneResponseException() return Tweet(res, self)
def upload_attachments(
self, media, close_after=False)
Upload a list of media using chunked upload. The list of media can only contain either 4 photos, 1 gif or 1 video. Everything else will raise an exception.
Parameters
-
media: list
List of media objects as path to a local file, as URI to an online file which will be downloaded and atatched then or an existing FileInfo object. -
close_after: bool
Wether to close each opened file handler after uploading or not. Default:False
Returns
Iterator[Media]
Iterator of uploaded Media objects.
def upload_attachments(self, media: list, close_after: bool = False) -> Iterator[Media]: """ Upload a list of media using chunked upload. The list of media can only contain either 4 photos, 1 gif or 1 video. Everything else will raise an exception. **Parameters** - `media: list` List of media objects as path to a local file, as URI to an online file which will be downloaded and atatched then or an existing FileInfo object. - `close_after: bool` Wether to close each opened file handler after uploading or not. *Default: `False`* **Returns** - `Iterator[Media]` Iterator of uploaded Media objects. """ max_attachable = None files = [] for m in media: file_info = m if type(m) == FileInfo else utils.try_get_file(m) i = utils.check_upload_compatibility(file_info) files.append(file_info) if not max_attachable: max_attachable = i if max_attachable and max_attachable > len(media): raise Exception('you can only attach up to {} files using this attachment type.' .format(max_attachable)) for f in files: yield self.upload_file_cunked(f, close_after=close_after)
def upload_file_cunked(
self, file_info, close_after=False)
Try to grab the FileInfo of the specified media file, checks if it can be uploaded to twitter and then tries to upload the file via the chunked twitter media upload endpoint.
Parameters
-
media : str
Either a local file location or a HTTP(S) URL to an online file resource. -
close_after: bool
Wether the file info reader should be closed after upload or not. Default:False
Returns
Media
Result media object.
def upload_file_cunked(self, file_info: utils.FileInfo, close_after: bool = False) -> Media: """ Try to grab the FileInfo of the specified media file, checks if it can be uploaded to twitter and then tries to upload the file via the chunked twitter media upload endpoint. **Parameters** - `media : str` Either a local file location or a HTTP(S) URL to an online file resource. - `close_after: bool` Wether the file info reader should be closed after upload or not. *Default: `False`* **Returns** - `Media` Result media object. """ # --- INIT ------------------------------------------------------------ res = self.upload_media_request( command='INIT', params={ 'total_bytes': file_info.size, 'media_type': file_info.mime_type, }) res_data = res.json() if 'media_id_string' not in res_data: raise Exception('"media_id_string" not contained in response body') media_id = res_data['media_id'] # --- APPEND ---------------------------------------------------------- boundary_uuid = uuid.uuid4().hex boundary = '--{0}'.format(boundary_uuid).encode('utf8') for chunk in utils.chunk_file(file_info, self.UPLOAD_CHUNK_SIZE): body_data = ( # COMMAND boundary, b'Content-Disposition: form-data; name="command"', b'', b'APPEND', # MEDIA_ID boundary, b'Content-Disposition: form-data; name="media_id"', b'', str(media_id).encode('utf8'), # MEDIA boundary, 'Content-Disposition: form-data; name="media"; filename="{0!r}"' .format(file_info.file_name).encode('utf8'), b'Content-Type: application/octet-stream', b'', chunk.data, # SEGMENT_INDEX boundary, b'Content-Disposition: form-data; name="segment_index"', b'', str(chunk.index).encode('utf8'), boundary + b'--' ) body = b'\r\n'.join(body_data) self.upload_media_request( headers={ 'Content-Type': 'multipart/form-data; boundary={0}' .format(boundary_uuid), 'Content-Length': str(len(body)), }, raw=body) # --- FINALIZE -------------------------------------------------------- res = self.upload_media_request( command='FINALIZE', params={ 'media_id': str(media_id), }) if close_after: file_info.close() if res == None: raise NoneResponseException() return Media(res.json())
def upload_media_request(
self, command=None, params={}, raw=None, **kwargs)
Wrap a request to initiate, append or finalize a chunked media upload.
Parameters
-
command : str
Must be 'INIT', 'APPEND' or 'FINALIZE'. If 'raw' is set, this must not be passed. -
params : dict
Request body parameters. If 'raw' is set, this must not be passed. -
raw
Raw data used as reuqest body. This option overwrites 'command' and 'params'. -
**kwargs:
Additional arguments which will be passed to the request method.
Returns
object
Resulting FINALIZE response object.
def upload_media_request(self, command=None, params={}, raw=None, **kwargs) -> object: """ Wrap a request to initiate, append or finalize a chunked media upload. **Parameters** - `command : str` Must be 'INIT', 'APPEND' or 'FINALIZE'. If 'raw' is set, this must not be passed. - `params : dict` Request body parameters. If 'raw' is set, this must not be passed. - `raw` Raw data used as reuqest body. This option overwrites 'command' and 'params'. - `**kwargs:` Additional arguments which will be passed to the request method. **Returns** - `object` Resulting FINALIZE response object. """ if raw is None: params['command'] = command params = utils.sort_dict_alphabetically(params) res = self._session.post( auth=self._oauth, url='{0}/media/upload.json'.format(self.API_UPLOAD_ROOT_URI), data=(params if raw is None else raw), **kwargs) if res.status_code == 429: raise RateLimitException() if res.status_code < 200 or res.status_code >= 300: raise Exception('request failed with status code {0}'.format(res.status_code)) return res
def users_lookup(
self, ids=None, screen_names=None, **kwargs)
Fetches up to 100 users by their ids OR screen names (Twitter handles). IDs and screen names can no be mixed. Screen names value list will be prefered.
Parameters
-
ids: List[str]
List of desired user IDs.
Default:none
-
screen_names: List[str]
List of desired user screen names (handles).
Default:none
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
Dict[str, User]
A dict of user IDs and user names as keys linked to the corresponding user objects. So, for each user, there are two values in the dict. Firstly linked to an ID key and secondly linked to the users user name as key.
def users_lookup(self, ids: List[str] = None, screen_names: List[str] = None, **kwargs) -> Dict[str, User]: """ Fetches up to 100 users by their ids OR screen names (Twitter handles). IDs and screen names can no be mixed. Screen names value list will be prefered. **Parameters** - `ids: List[str]` List of desired user IDs. *Default: `none`* - `screen_names: List[str]` List of desired user screen names (handles). *Default: `none`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `Dict[str, User]` A dict of user IDs and user names as keys linked to the corresponding user objects. So, for each user, there are two values in the dict. Firstly linked to an ID key and secondly linked to the users user name as key. """ if not ids and not screen_names: raise ParameterNoneException() data = kwargs ln = 0 if ids and len(ids) > 0: data['user_id'] = ','.join([str(id) for id in ids]) ln += len(ids) if screen_names and len(screen_names) > 0: data['screen_name'] = ','.join(screen_names) ln += len(screen_names) if ln < 1 or ln > 100: raise ParameterOutOfBoundsException( 'ids + screen_names length must be in range [1, 100]') res = self.request('GET', 'users/lookup.json', params=data) if not res: return NoneResponseException() users = {} for r in res: user = User(r, self) users[user.id_str] = user users[user.username or user.screen_name] = user return users
def users_show(
self, id=None, screen_name=None, **kwargs)
Fetches a single user by its ID or screen name (Twitter handle).
Parameters
-
id: [str, int]
ID of the desired user.
Default:None
-
screen_name: str
Screen name (handle) of the desired user.
Default:None
-
**kwargs:
Additional agruments passed directly to the request parameters.
Returns
User
Fetched user object.
def users_show(self, id: [str, int] = None, screen_name: str = None, **kwargs) -> User: """ Fetches a single user by its ID or screen name (Twitter handle). **Parameters** - `id: [str, int]` ID of the desired user. *Default: `None`* - `screen_name: str` Screen name (handle) of the desired user. *Default: `None`* - `**kwargs:` Additional agruments passed directly to the request parameters. **Returns** - `User` Fetched user object. """ if not id and not screen_name: raise ParameterNoneException() data = kwargs if id: data['user_id'] = id if screen_name: data['screen_name'] = screen_name res = self.request('GET', 'users/show.json', params=data) if not res: raise NoneResponseException() return User(res, self)