Update python-twitter to 3.4.1

This commit is contained in:
JonnyWong16 2018-05-23 17:12:19 -07:00
parent 8e4aba7ed4
commit f743a817ba
8 changed files with 1520 additions and 993 deletions

View file

@ -23,7 +23,7 @@ __author__ = 'The Python-Twitter Developers'
__email__ = 'python-twitter@googlegroups.com' __email__ = 'python-twitter@googlegroups.com'
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers' __copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
__license__ = 'Apache License 2.0' __license__ = 'Apache License 2.0'
__version__ = '3.0rc1' __version__ = '3.4.1'
__url__ = 'https://github.com/bear/python-twitter' __url__ = 'https://github.com/bear/python-twitter'
__download_url__ = 'https://pypi.python.org/pypi/python-twitter' __download_url__ = 'https://pypi.python.org/pypi/python-twitter'
__description__ = 'A Python wrapper around the Twitter API' __description__ = 'A Python wrapper around the Twitter API'

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python
import errno import errno
import os import os
import re
import tempfile import tempfile
from hashlib import md5 from hashlib import md5
@ -47,7 +46,7 @@ class _FileCache(object):
path = self._GetPath(key) path = self._GetPath(key)
if not path.startswith(self._root_directory): if not path.startswith(self._root_directory):
raise _FileCacheError('%s does not appear to live under %s' % raise _FileCacheError('%s does not appear to live under %s' %
(path, self._root_directory )) (path, self._root_directory))
if os.path.exists(path): if os.path.exists(path):
os.remove(path) os.remove(path)
@ -101,61 +100,3 @@ class _FileCache(object):
def _GetPrefix(self, hashed_key): def _GetPrefix(self, hashed_key):
return os.path.sep.join(hashed_key[0:_FileCache.DEPTH]) return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
class ParseTweet(object):
# compile once on import
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
"HASHTAG": r"(#[\w\d]+)", "URL": r"([http://]?[a-zA-Z\d\/]+[\.]+[a-zA-Z\d\/\.]+)"}
regexp = dict((key, re.compile(value)) for key, value in list(regexp.items()))
def __init__(self, timeline_owner, tweet):
""" timeline_owner : twitter handle of user account. tweet - 140 chars from feed; object does all computation on construction
properties:
RT, MT - boolean
URLs - list of URL
Hashtags - list of tags
"""
self.Owner = timeline_owner
self.tweet = tweet
self.UserHandles = ParseTweet.getUserHandles(tweet)
self.Hashtags = ParseTweet.getHashtags(tweet)
self.URLs = ParseTweet.getURLs(tweet)
self.RT = ParseTweet.getAttributeRT(tweet)
self.MT = ParseTweet.getAttributeMT(tweet)
# additional intelligence
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet?
self.Owner = self.UserHandles[0]
return
def __str__(self):
""" for display method """
return "owner %s, urls: %d, hashtags %d, user_handles %d, len_tweet %d, RT = %s, MT = %s" % (
self.Owner, len(self.URLs), len(self.Hashtags), len(self.UserHandles),
len(self.tweet), self.RT, self.MT)
@staticmethod
def getAttributeRT(tweet):
""" see if tweet is a RT """
return re.search(ParseTweet.regexp["RT"], tweet.strip()) is not None
@staticmethod
def getAttributeMT(tweet):
""" see if tweet is a MT """
return re.search(ParseTweet.regexp["MT"], tweet.strip()) is not None
@staticmethod
def getUserHandles(tweet):
""" given a tweet we try and extract all user handles in order of occurrence"""
return re.findall(ParseTweet.regexp["ALNUM"], tweet)
@staticmethod
def getHashtags(tweet):
""" return all hashtags"""
return re.findall(ParseTweet.regexp["HASHTAG"], tweet)
@staticmethod
def getURLs(tweet):
""" URL : [http://]?[\w\.?/]+"""
return re.findall(ParseTweet.regexp["URL"], tweet)

File diff suppressed because it is too large Load diff

View file

@ -8,3 +8,18 @@ class TwitterError(Exception):
def message(self): def message(self):
'''Returns the first argument used to construct this error.''' '''Returns the first argument used to construct this error.'''
return self.args[0] return self.args[0]
class PythonTwitterDeprecationWarning(DeprecationWarning):
"""Base class for python-twitter deprecation warnings"""
pass
class PythonTwitterDeprecationWarning330(PythonTwitterDeprecationWarning):
"""Warning for features to be removed in version 3.3.0"""
pass
class PythonTwitterDeprecationWarning340(PythonTwitterDeprecationWarning):
"""Warning for features to be removed in version 3.4.0"""
pass

View file

@ -28,6 +28,13 @@ class TwitterModel(object):
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self):
if hasattr(self, 'id'):
return hash(self.id)
else:
raise TypeError('unhashable type: {} (no id attribute)'
.format(type(self)))
def AsJsonString(self): def AsJsonString(self):
""" Returns the TwitterModel as a JSON string based on key/value """ Returns the TwitterModel as a JSON string based on key/value
pairs returned from the AsDict() method. """ pairs returned from the AsDict() method. """
@ -78,11 +85,14 @@ class TwitterModel(object):
""" """
json_data = data.copy()
if kwargs: if kwargs:
for key, val in kwargs.items(): for key, val in kwargs.items():
data[key] = val json_data[key] = val
return cls(**data) c = cls(**json_data)
c._json = data
return c
class Media(TwitterModel): class Media(TwitterModel):
@ -93,11 +103,14 @@ class Media(TwitterModel):
self.param_defaults = { self.param_defaults = {
'display_url': None, 'display_url': None,
'expanded_url': None, 'expanded_url': None,
'ext_alt_text': None,
'id': None, 'id': None,
'media_url': None, 'media_url': None,
'media_url_https': None, 'media_url_https': None,
'sizes': None,
'type': None, 'type': None,
'url': None, 'url': None,
'video_info': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@ -172,8 +185,10 @@ class DirectMessage(TwitterModel):
self.param_defaults = { self.param_defaults = {
'created_at': None, 'created_at': None,
'id': None, 'id': None,
'recipient': None,
'recipient_id': None, 'recipient_id': None,
'recipient_screen_name': None, 'recipient_screen_name': None,
'sender': None,
'sender_id': None, 'sender_id': None,
'sender_screen_name': None, 'sender_screen_name': None,
'text': None, 'text': None,
@ -181,6 +196,10 @@ class DirectMessage(TwitterModel):
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if 'sender' in kwargs:
self.sender = User.NewFromJsonDict(kwargs.get('sender', None))
if 'recipient' in kwargs:
self.recipient = User.NewFromJsonDict(kwargs.get('recipient', None))
def __repr__(self): def __repr__(self):
if self.text and len(self.text) > 140: if self.text and len(self.text) > 140:
@ -206,7 +225,7 @@ class Trend(TwitterModel):
'query': None, 'query': None,
'timestamp': None, 'timestamp': None,
'url': None, 'url': None,
'volume': None, 'tweet_volume': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@ -218,6 +237,10 @@ class Trend(TwitterModel):
self.timestamp, self.timestamp,
self.url) self.url)
@property
def volume(self):
return self.tweet_volume
class Hashtag(TwitterModel): class Hashtag(TwitterModel):
@ -259,12 +282,12 @@ class UserStatus(TwitterModel):
""" A class representing the UserStatus structure. This is an abbreviated """ A class representing the UserStatus structure. This is an abbreviated
form of the twitter.User object. """ form of the twitter.User object. """
connections = {'following': False, _connections = {'following': False,
'followed_by': False, 'followed_by': False,
'following_received': False, 'following_received': False,
'following_requested': False, 'following_requested': False,
'blocking': False, 'blocking': False,
'muting': False} 'muting': False}
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.param_defaults = { self.param_defaults = {
@ -284,10 +307,19 @@ class UserStatus(TwitterModel):
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if 'connections' in kwargs: if 'connections' in kwargs:
for param in self.connections: for param in self._connections:
if param in kwargs['connections']: if param in kwargs['connections']:
setattr(self, param, True) setattr(self, param, True)
@property
def connections(self):
return {'following': self.following,
'followed_by': self.followed_by,
'following_received': self.following_received,
'following_requested': self.following_requested,
'blocking': self.blocking,
'muting': self.muting}
def __repr__(self): def __repr__(self):
connections = [param for param in self.connections if getattr(self, param)] connections = [param for param in self.connections if getattr(self, param)]
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format( return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
@ -307,11 +339,14 @@ class User(TwitterModel):
'default_profile': None, 'default_profile': None,
'default_profile_image': None, 'default_profile_image': None,
'description': None, 'description': None,
'email': None,
'favourites_count': None, 'favourites_count': None,
'followers_count': None, 'followers_count': None,
'following': None,
'friends_count': None, 'friends_count': None,
'geo_enabled': None, 'geo_enabled': None,
'id': None, 'id': None,
'id_str': None,
'lang': None, 'lang': None,
'listed_count': None, 'listed_count': None,
'location': None, 'location': None,
@ -319,12 +354,16 @@ class User(TwitterModel):
'notifications': None, 'notifications': None,
'profile_background_color': None, 'profile_background_color': None,
'profile_background_image_url': None, 'profile_background_image_url': None,
'profile_background_image_url_https': None,
'profile_background_tile': None, 'profile_background_tile': None,
'profile_banner_url': None, 'profile_banner_url': None,
'profile_image_url': None, 'profile_image_url': None,
'profile_image_url_https': None,
'profile_link_color': None, 'profile_link_color': None,
'profile_sidebar_border_color': None,
'profile_sidebar_fill_color': None, 'profile_sidebar_fill_color': None,
'profile_text_color': None, 'profile_text_color': None,
'profile_use_background_image': None,
'protected': None, 'protected': None,
'screen_name': None, 'screen_name': None,
'status': None, 'status': None,
@ -333,6 +372,8 @@ class User(TwitterModel):
'url': None, 'url': None,
'utc_offset': None, 'utc_offset': None,
'verified': None, 'verified': None,
'withheld_in_countries': None,
'withheld_scope': None,
} }
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
@ -365,6 +406,7 @@ class Status(TwitterModel):
'current_user_retweet': None, 'current_user_retweet': None,
'favorite_count': None, 'favorite_count': None,
'favorited': None, 'favorited': None,
'full_text': None,
'geo': None, 'geo': None,
'hashtags': None, 'hashtags': None,
'id': None, 'id': None,
@ -377,6 +419,9 @@ class Status(TwitterModel):
'media': None, 'media': None,
'place': None, 'place': None,
'possibly_sensitive': None, 'possibly_sensitive': None,
'quoted_status': None,
'quoted_status_id': None,
'quoted_status_id_str': None,
'retweet_count': None, 'retweet_count': None,
'retweeted': None, 'retweeted': None,
'retweeted_status': None, 'retweeted_status': None,
@ -395,6 +440,11 @@ class Status(TwitterModel):
for (param, default) in self.param_defaults.items(): for (param, default) in self.param_defaults.items():
setattr(self, param, kwargs.get(param, default)) setattr(self, param, kwargs.get(param, default))
if kwargs.get('full_text', None):
self.tweet_mode = 'extended'
else:
self.tweet_mode = 'compatibility'
@property @property
def created_at_in_seconds(self): def created_at_in_seconds(self):
""" Get the time this status message was posted, in seconds since """ Get the time this status message was posted, in seconds since
@ -414,17 +464,21 @@ class Status(TwitterModel):
string: A string representation of this twitter.Status instance with string: A string representation of this twitter.Status instance with
the ID of status, username and datetime. the ID of status, username and datetime.
""" """
if self.tweet_mode == 'extended':
text = self.full_text
else:
text = self.text
if self.user: if self.user:
return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format( return "Status(ID={0}, ScreenName={1}, Created={2}, Text={3!r})".format(
self.id, self.id,
self.user.screen_name, self.user.screen_name,
self.created_at, self.created_at,
self.text) text)
else: else:
return u"Status(ID={0}, Created={1}, Text={2!r})".format( return u"Status(ID={0}, Created={1}, Text={2!r})".format(
self.id, self.id,
self.created_at, self.created_at,
self.text) text)
@classmethod @classmethod
def NewFromJsonDict(cls, data, **kwargs): def NewFromJsonDict(cls, data, **kwargs):
@ -439,17 +493,25 @@ class Status(TwitterModel):
current_user_retweet = None current_user_retweet = None
hashtags = None hashtags = None
media = None media = None
quoted_status = None
retweeted_status = None retweeted_status = None
urls = None urls = None
user = None user = None
user_mentions = None user_mentions = None
# for loading extended tweets from the streaming API.
if 'extended_tweet' in data:
for k, v in data['extended_tweet'].items():
data[k] = v
if 'user' in data: if 'user' in data:
user = User.NewFromJsonDict(data['user']) user = User.NewFromJsonDict(data['user'])
if 'retweeted_status' in data: if 'retweeted_status' in data:
retweeted_status = Status.NewFromJsonDict(data['retweeted_status']) retweeted_status = Status.NewFromJsonDict(data['retweeted_status'])
if 'current_user_retweet' in data: if 'current_user_retweet' in data:
current_user_retweet = data['current_user_retweet']['id'] current_user_retweet = data['current_user_retweet']['id']
if 'quoted_status' in data:
quoted_status = Status.NewFromJsonDict(data.get('quoted_status'))
if 'entities' in data: if 'entities' in data:
if 'urls' in data['entities']: if 'urls' in data['entities']:
@ -470,6 +532,7 @@ class Status(TwitterModel):
current_user_retweet=current_user_retweet, current_user_retweet=current_user_retweet,
hashtags=hashtags, hashtags=hashtags,
media=media, media=media,
quoted_status=quoted_status,
retweeted_status=retweeted_status, retweeted_status=retweeted_status,
urls=urls, urls=urls,
user=user, user=user,

View file

@ -2,6 +2,7 @@
import re import re
class Emoticons: class Emoticons:
POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *", POSITIVE = ["*O", "*-*", "*O*", "*o*", "* *",
":P", ":D", ":d", ":p", ":P", ":D", ":d", ":p",
@ -27,6 +28,7 @@ class Emoticons:
"[:", ";]" "[:", ";]"
] ]
class ParseTweet(object): class ParseTweet(object):
# compile once on import # compile once on import
regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)", regexp = {"RT": "^RT", "MT": r"^MT", "ALNUM": r"(@[a-zA-Z0-9_]+)",
@ -51,7 +53,7 @@ class ParseTweet(object):
self.Emoticon = ParseTweet.getAttributeEmoticon(tweet) self.Emoticon = ParseTweet.getAttributeEmoticon(tweet)
# additional intelligence # additional intelligence
if ( self.RT and len(self.UserHandles) > 0 ): # change the owner of tweet? if (self.RT and len(self.UserHandles) > 0): # change the owner of tweet?
self.Owner = self.UserHandles[0] self.Owner = self.UserHandles[0]
return return
@ -66,10 +68,10 @@ class ParseTweet(object):
emoji = list() emoji = list()
for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()): for tok in re.split(ParseTweet.regexp["SPACES"], tweet.strip()):
if tok in Emoticons.POSITIVE: if tok in Emoticons.POSITIVE:
emoji.append( tok ) emoji.append(tok)
continue continue
if tok in Emoticons.NEGATIVE: if tok in Emoticons.NEGATIVE:
emoji.append( tok ) emoji.append(tok)
return emoji return emoji
@staticmethod @staticmethod

View file

@ -97,6 +97,7 @@ class RateLimit(object):
and a dictionary of limit, remaining, and reset will be returned. and a dictionary of limit, remaining, and reset will be returned.
""" """
self.__dict__['resources'] = {}
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@staticmethod @staticmethod
@ -117,10 +118,12 @@ class RateLimit(object):
for non_std_endpoint in NON_STANDARD_ENDPOINTS: for non_std_endpoint in NON_STANDARD_ENDPOINTS:
if re.match(non_std_endpoint.regex, resource): if re.match(non_std_endpoint.regex, resource):
return non_std_endpoint.resource return non_std_endpoint.resource
else: return resource
return resource
def set_unknown_limit(self, url, limit, remaining, reset): def set_unknown_limit(self, url, limit, remaining, reset):
return self.set_limit(url, limit, remaining, reset)
def set_limit(self, url, limit, remaining, reset):
""" If a resource family is unknown, add it to the object's """ If a resource family is unknown, add it to the object's
dictionary. This is to deal with new endpoints being added to dictionary. This is to deal with new endpoints being added to
the API, but not necessarily to the information returned by the API, but not necessarily to the information returned by
@ -146,13 +149,18 @@ class RateLimit(object):
""" """
endpoint = self.url_to_resource(url) endpoint = self.url_to_resource(url)
resource_family = endpoint.split('/')[1] resource_family = endpoint.split('/')[1]
self.__dict__['resources'].update( new_endpoint = {endpoint: {
{resource_family: { "limit": enf_type('limit', int, limit),
endpoint: { "remaining": enf_type('remaining', int, remaining),
"limit": limit, "reset": enf_type('reset', int, reset)
"remaining": remaining, }}
"reset": reset
}}}) if not self.resources.get(resource_family, None):
self.resources[resource_family] = {}
self.__dict__['resources'][resource_family].update(new_endpoint)
return self.get_limit(url)
def get_limit(self, url): def get_limit(self, url):
""" Gets a EndpointRateLimit object for the given url. """ Gets a EndpointRateLimit object for the given url.
@ -181,35 +189,3 @@ class RateLimit(object):
return EndpointRateLimit(family_rates['limit'], return EndpointRateLimit(family_rates['limit'],
family_rates['remaining'], family_rates['remaining'],
family_rates['reset']) family_rates['reset'])
def set_limit(self, url, limit, remaining, reset):
""" Set an endpoint's rate limits. The data used for each of the
args should come from Twitter's ``x-rate-limit`` headers.
Args:
url (str):
URL of the endpoint being fetched.
limit (int):
Max number of times a user or app can hit the endpoint
before being rate limited.
remaining (int):
Number of times a user or app can access the endpoint
before being rate limited.
reset (int):
Epoch time at which the rate limit window will reset.
"""
endpoint = self.url_to_resource(url)
resource_family = endpoint.split('/')[1]
try:
family_rates = self.resources.get(resource_family).get(endpoint)
except AttributeError:
self.set_unknown_limit(url, limit, remaining, reset)
family_rates = self.resources.get(resource_family).get(endpoint)
family_rates['limit'] = enf_type('limit', int, limit)
family_rates['remaining'] = enf_type('remaining', int, remaining)
family_rates['reset'] = enf_type('reset', int, reset)
return EndpointRateLimit(family_rates['limit'],
family_rates['remaining'],
family_rates['reset'])

View file

@ -1,13 +1,33 @@
# encoding: utf-8 # encoding: utf-8
from __future__ import unicode_literals
import mimetypes import mimetypes
import os import os
import re import re
import sys
from tempfile import NamedTemporaryFile
from unicodedata import normalize
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
import requests import requests
from tempfile import NamedTemporaryFile
from twitter import TwitterError from twitter import TwitterError
import twitter
if sys.version_info < (3,):
range = xrange
if sys.version_info > (3,):
unicode = str
CHAR_RANGES = [
range(0, 4351),
range(8192, 8205),
range(8208, 8223),
range(8242, 8247)]
TLDS = [ TLDS = [
"ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar",
@ -138,7 +158,14 @@ TLDS = [
"淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团", "淡马锡", "游戏", "点看", "移动", "组织机构", "网址", "网店", "网络", "谷歌", "集团",
"飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"] "飞利浦", "餐厅", "닷넷", "닷컴", "삼성", "onion"]
URL_REGEXP = re.compile(r'(?i)((?:https?://|www\\.)*(?:[\w+-_]+[.])(?:' + r'\b|'.join(TLDS) + r'\b|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))+(?:[:\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*)', re.UNICODE) URL_REGEXP = re.compile((
r'('
r'^(?!(https?://|www\.)?\.|ftps?://|([0-9]+\.){{1,3}}\d+)' # exclude urls that start with "."
r'(?:https?://|www\.)*^(?!.*@)(?:[\w+-_]+[.])' # beginning of url
r'(?:{0}\b' # all tlds
r'(?:[:0-9]))' # port numbers & close off TLDs
r'(?:[\w+\/]?[a-z0-9!\*\'\(\);:&=\+\$/%#\[\]\-_\.,~?])*' # path/query params
r')').format(r'\b|'.join(TLDS)), re.U | re.I | re.X)
def calc_expected_status_length(status, short_url_length=23): def calc_expected_status_length(status, short_url_length=23):
@ -153,12 +180,19 @@ def calc_expected_status_length(status, short_url_length=23):
Expected length of the status message as an integer. Expected length of the status message as an integer.
""" """
replaced_chars = 0 status_length = 0
status_length = len(status) if isinstance(status, bytes):
match = re.findall(URL_REGEXP, status) status = unicode(status)
if len(match) >= 1: for word in re.split(r'\s', status):
replaced_chars = len(''.join(match)) if is_url(word):
status_length = status_length - replaced_chars + (short_url_length * len(match)) status_length += short_url_length
else:
for character in word:
if any([ord(normalize("NFC", character)) in char_range for char_range in CHAR_RANGES]):
status_length += 1
else:
status_length += 2
status_length += len(re.findall(r'\s', status))
return status_length return status_length
@ -171,16 +205,14 @@ def is_url(text):
Returns: Returns:
Boolean of whether the text should be treated as a URL or not. Boolean of whether the text should be treated as a URL or not.
""" """
if re.findall(URL_REGEXP, text): return bool(re.findall(URL_REGEXP, text))
return True
else:
return False
def http_to_file(http): def http_to_file(http):
data_file = NamedTemporaryFile() data_file = NamedTemporaryFile()
req = requests.get(http, stream=True) req = requests.get(http, stream=True)
data_file.write(req.raw.data) for chunk in req.iter_content(chunk_size=1024 * 1024):
data_file.write(chunk)
return data_file return data_file
@ -200,7 +232,8 @@ def parse_media_file(passed_media):
'image/gif', 'image/gif',
'image/bmp', 'image/bmp',
'image/webp'] 'image/webp']
video_formats = ['video/mp4'] video_formats = ['video/mp4',
'video/quicktime']
# If passed_media is a string, check if it points to a URL, otherwise, # If passed_media is a string, check if it points to a URL, otherwise,
# it should point to local file. Create a reference to a file obj for # it should point to local file. Create a reference to a file obj for
@ -208,7 +241,7 @@ def parse_media_file(passed_media):
if not hasattr(passed_media, 'read'): if not hasattr(passed_media, 'read'):
if passed_media.startswith('http'): if passed_media.startswith('http'):
data_file = http_to_file(passed_media) data_file = http_to_file(passed_media)
filename = os.path.basename(passed_media) filename = os.path.basename(urlparse(passed_media).path)
else: else:
data_file = open(os.path.realpath(passed_media), 'rb') data_file = open(os.path.realpath(passed_media), 'rb')
filename = os.path.basename(passed_media) filename = os.path.basename(passed_media)
@ -216,8 +249,8 @@ def parse_media_file(passed_media):
# Otherwise, if a file object was passed in the first place, # Otherwise, if a file object was passed in the first place,
# create the standard reference to media_file (i.e., rename it to fp). # create the standard reference to media_file (i.e., rename it to fp).
else: else:
if passed_media.mode != 'rb': if passed_media.mode not in ['rb', 'rb+', 'w+b']:
raise TwitterError({'message': 'File mode must be "rb".'}) raise TwitterError('File mode must be "rb" or "rb+"')
filename = os.path.basename(passed_media.name) filename = os.path.basename(passed_media.name)
data_file = passed_media data_file = passed_media
@ -226,16 +259,17 @@ def parse_media_file(passed_media):
try: try:
data_file.seek(0) data_file.seek(0)
except: except Exception as e:
pass pass
media_type = mimetypes.guess_type(os.path.basename(filename))[0] media_type = mimetypes.guess_type(os.path.basename(filename))[0]
if media_type in img_formats and file_size > 5 * 1048576: if media_type is not None:
raise TwitterError({'message': 'Images must be less than 5MB.'}) if media_type in img_formats and file_size > 5 * 1048576:
elif media_type in video_formats and file_size > 15 * 1048576: raise TwitterError({'message': 'Images must be less than 5MB.'})
raise TwitterError({'message': 'Videos must be less than 15MB.'}) elif media_type in video_formats and file_size > 15 * 1048576:
elif media_type not in img_formats and media_type not in video_formats: raise TwitterError({'message': 'Videos must be less than 15MB.'})
raise TwitterError({'message': 'Media type could not be determined.'}) elif media_type not in img_formats and media_type not in video_formats:
raise TwitterError({'message': 'Media type could not be determined.'})
return data_file, filename, file_size, media_type return data_file, filename, file_size, media_type
@ -263,3 +297,18 @@ def enf_type(field, _type, val):
raise TwitterError({ raise TwitterError({
'message': '"{0}" must be type {1}'.format(field, _type.__name__) 'message': '"{0}" must be type {1}'.format(field, _type.__name__)
}) })
def parse_arg_list(args, attr):
out = []
if isinstance(args, (str, unicode)):
out.append(args)
elif isinstance(args, twitter.User):
out.append(getattr(args, attr))
elif isinstance(args, (list, tuple)):
for item in args:
if isinstance(item, (str, unicode)):
out.append(item)
elif isinstance(item, twitter.User):
out.append(getattr(item, attr))
return ",".join([str(item) for item in out])