mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-11 07:46:07 -07:00
Update python-twitter to 3.4.1
This commit is contained in:
parent
8e4aba7ed4
commit
f743a817ba
8 changed files with 1520 additions and 993 deletions
|
@ -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'
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
2183
lib/twitter/api.py
2183
lib/twitter/api.py
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue