Skip to content
This repository was archived by the owner on Aug 7, 2024. It is now read-only.

Commit 0212d81

Browse files
authored
Merge pull request #1 from bear/master
Pull origin repositories
2 parents b5fd51d + c736c47 commit 0212d81

File tree

10 files changed

+93
-45
lines changed

10 files changed

+93
-45
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Now it's a full-on open source project with many contributors over time:
2121
* Pradeep Nayak,
2222
* Ian Ozsvald,
2323
* Nicolas Perriault,
24+
* Trevor Prater,
2425
* Glen Tregoning,
2526
* Lars Weiler,
2627
* Sebastian Wiesinger,

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
See the messy details at https://github.com/bear/python-twitter/pull/251
77

88
Pull Requests
9+
#525 Adds support for 280 character limit for tweets by trevorprater
910
#267 initialize Api.__auth fixes #119 by rbpasker
1011
#266 Add full_text and page options in GetDirectMessages function by mistersalmon
1112
#264 Updates Media object with new methods, adds id param, adds tests by jeremylow
@@ -24,6 +25,9 @@
2425

2526
Probably a whole lot that I missed - ugh!
2627

28+
2017-11-11
29+
Added support for 280 character limit
30+
2731
2015-10-05
2832
Added who to api.GetSearch
2933

doc/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
Changelog
22
---------
3+
Version 3.3.1
4+
=============
5+
6+
* Adds support for 280 character limit.
7+
38

49
Version 3.3
510
=============

doc/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@
5757
# built documents.
5858
#
5959
# The short X.Y version.
60-
version = '3.2'
60+
version = '3.3'
6161
# The full version, including alpha/beta/rc tags.
62-
release = '3.2.1'
62+
release = '3.3.1'
6363

6464
# The language for content autogenerated by Sphinx. Refer to documentation
6565
# for a list of supported languages.

examples/tweet.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
__author__ = '[email protected]'
66

7+
from __future__ import print_function
8+
79
try:
810
import configparser
911
except ImportError as _:
@@ -15,6 +17,7 @@
1517
import twitter
1618

1719

20+
1821
USAGE = '''Usage: tweet [options] message
1922
2023
This script posts a message to Twitter.
@@ -52,7 +55,7 @@
5255

5356

5457
def PrintUsageAndExit():
55-
print USAGE
58+
print(USAGE)
5659
sys.exit(2)
5760

5861

@@ -143,11 +146,11 @@ def main():
143146
try:
144147
status = api.PostUpdate(message)
145148
except UnicodeDecodeError:
146-
print "Your message could not be encoded. Perhaps it contains non-ASCII characters? "
147-
print "Try explicitly specifying the encoding with the --encoding flag"
149+
print("Your message could not be encoded. Perhaps it contains non-ASCII characters? ")
150+
print("Try explicitly specifying the encoding with the --encoding flag")
148151
sys.exit(2)
149-
print "%s just posted: %s" % (status.user.name, status.text)
150152

153+
print("{0} just posted: {1}".format(status.user.name, status.text))
151154

152155
if __name__ == "__main__":
153156
main()

tests/test_models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,7 @@ def test_user_status(self):
190190
self.fail(e)
191191
self.assertTrue(user_status.AsJsonString())
192192
self.assertTrue(user_status.AsDict())
193+
194+
self.assertTrue(user_status.connections['blocking'])
195+
self.assertTrue(user_status.connections['muting'])
196+
self.assertFalse(user_status.connections['followed_by'])

tests/test_tweet_length.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,12 @@ def test_split_tweets(self):
6363
tweets = self.api._TweetTextWrap(test_tweet)
6464
self.assertEqual(
6565
tweets[0],
66-
"Anatole went out of the room and returned a few minutes later wearing a fur coat girt with a silver belt, and a sable cap jauntily set on")
66+
"Anatole went out of the room and returned a few minutes later wearing a fur coat girt with a silver belt, and a sable cap jauntily set on one side and very becoming to his handsome face. Having looked in a mirror, and standing before Dolokhov in the same pose he had assumed")
6767
self.assertEqual(
6868
tweets[1],
69-
"one side and very becoming to his handsome face. Having looked in a mirror, and standing before Dolokhov in the same pose he had assumed")
70-
self.assertEqual(
71-
tweets[2],
7269
"before it, he lifted a glass of wine.")
7370

74-
test_tweet = "t.co went t.co of t.co room t.co returned t.co few minutes later"
71+
test_tweet = "t.co went t.co of t.co room t.co returned t.co few minutes later and then t.co went to t.co restroom and t.co was sad because t.co did not have any t.co toilet paper"
7572
tweets = self.api._TweetTextWrap(test_tweet)
76-
self.assertEqual(tweets[0], 't.co went t.co of t.co room t.co returned')
77-
self.assertEqual(tweets[1], 't.co few minutes later')
73+
self.assertEqual(tweets[0], 't.co went t.co of t.co room t.co returned t.co few minutes later and then t.co went to t.co restroom and t.co was sad because')
74+
self.assertEqual(tweets[1], 't.co did not have any t.co toilet paper')

twitter/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
__email__ = '[email protected]'
2424
__copyright__ = 'Copyright (c) 2007-2016 The Python-Twitter Developers'
2525
__license__ = 'Apache License 2.0'
26-
__version__ = '3.3'
26+
__version__ = '3.3.1'
2727
__url__ = 'https://github.com/bear/python-twitter'
2828
__download_url__ = 'https://pypi.python.org/pypi/python-twitter'
2929
__description__ = 'A Python wrapper around the Twitter API'

twitter/api.py

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
if sys.version_info > (3,):
7373
long = int
7474

75-
CHARACTER_LIMIT = 140
75+
CHARACTER_LIMIT = 280
7676

7777
# A singleton representing a lazily instantiated FileCache.
7878
DEFAULT_CACHE = object()
@@ -229,6 +229,8 @@ def __init__(self,
229229
self._use_gzip = use_gzip_compression
230230
self._debugHTTP = debugHTTP
231231
self._shortlink_size = 19
232+
if timeout and timeout < 30:
233+
warn("Warning: The Twitter streaming API sends 30s keepalives, the given timeout is shorter!")
232234
self._timeout = timeout
233235
self.__auth = None
234236

@@ -274,9 +276,12 @@ def __init__(self,
274276

275277
if debugHTTP:
276278
import logging
277-
import http.client
279+
try:
280+
import http.client as http_client # python3
281+
except ImportError:
282+
import httplib as http_client # python2
278283

279-
http.client.HTTPConnection.debuglevel = 1
284+
http_client.HTTPConnection.debuglevel = 1
280285

281286
logging.basicConfig() # you need to initialize logging, otherwise you will not see anything from requests
282287
logging.getLogger().setLevel(logging.DEBUG)
@@ -976,8 +981,8 @@ def PostUpdate(self,
976981
977982
Args:
978983
status (str):
979-
The message text to be posted. Must be less than or equal to 140
980-
characters.
984+
The message text to be posted. Must be less than or equal to
985+
CHARACTER_LIMIT characters.
981986
media (int, str, fp, optional):
982987
A URL, a local file, or a file-like object (something with a
983988
read() method), or a list of any combination of the above.
@@ -1029,8 +1034,8 @@ def PostUpdate(self,
10291034
otherwise the payload will contain the full user data item.
10301035
verify_status_length (bool, optional):
10311036
If True, api throws a hard error that the status is over
1032-
140 characters. If False, Api will attempt to post the
1033-
status.
1037+
CHARACTER_LIMIT characters. If False, Api will attempt to post
1038+
the status.
10341039
Returns:
10351040
(twitter.Status) A twitter.Status instance representing the
10361041
message posted.
@@ -1042,8 +1047,8 @@ def PostUpdate(self,
10421047
else:
10431048
u_status = str(status, self._input_encoding)
10441049

1045-
if verify_status_length and calc_expected_status_length(u_status) > 140:
1046-
raise TwitterError("Text must be less than or equal to 140 characters.")
1050+
if verify_status_length and calc_expected_status_length(u_status) > CHARACTER_LIMIT:
1051+
raise TwitterError("Text must be less than or equal to CHARACTER_LIMIT characters.")
10471052

10481053
if auto_populate_reply_metadata and not in_reply_to_status_id:
10491054
raise TwitterError("If auto_populate_reply_metadata is True, you must set in_reply_to_status_id")
@@ -1514,7 +1519,7 @@ def PostMultipleMedia(self, status, media, possibly_sensitive=None,
15141519

15151520
def _TweetTextWrap(self,
15161521
status,
1517-
char_lim=140):
1522+
char_lim=CHARACTER_LIMIT):
15181523

15191524
if not self._config:
15201525
self.GetHelpConfiguration()
@@ -1525,7 +1530,7 @@ def _TweetTextWrap(self,
15251530
words = re.split(r'\s', status)
15261531

15271532
if len(words) == 1 and not is_url(words[0]):
1528-
if len(words[0]) > 140:
1533+
if len(words[0]) > CHARACTER_LIMIT:
15291534
raise TwitterError({"message": "Unable to split status into tweetable parts. Word was: {0}/{1}".format(len(words[0]), char_lim)})
15301535
else:
15311536
tweets.append(words[0])
@@ -1541,7 +1546,7 @@ def _TweetTextWrap(self,
15411546
else:
15421547
new_len += len(word) + 1
15431548

1544-
if new_len > 140:
1549+
if new_len > CHARACTER_LIMIT:
15451550
tweets.append(' '.join(line))
15461551
line = [word]
15471552
line_length = new_len - line_length
@@ -1559,12 +1564,12 @@ def PostUpdates(self,
15591564
"""Post one or more twitter status messages from the authenticated user.
15601565
15611566
Unlike api.PostUpdate, this method will post multiple status updates
1562-
if the message is longer than 140 characters.
1567+
if the message is longer than CHARACTER_LIMIT characters.
15631568
15641569
Args:
15651570
status:
15661571
The message text to be posted.
1567-
May be longer than 140 characters.
1572+
May be longer than CHARACTER_LIMIT characters.
15681573
continuation:
15691574
The character string, if any, to be appended to all but the
15701575
last message. Note that Twitter strips trailing '...' strings
@@ -2978,7 +2983,7 @@ def GetDirectMessages(self,
29782983
objects. [Optional]
29792984
full_text:
29802985
When set to True full message will be included in the returned message
2981-
object if message length is bigger than 140 characters. [Optional]
2986+
object if message length is bigger than CHARACTER_LIMIT characters. [Optional]
29822987
page:
29832988
If you want more than 200 messages, you can use this and get 20 messages
29842989
each time. You must recall it and increment the page value until it
@@ -4692,7 +4697,9 @@ def GetUserStream(self,
46924697
delimited=None,
46934698
stall_warnings=None,
46944699
stringify_friend_ids=False,
4695-
filter_level=None):
4700+
filter_level=None,
4701+
session=None,
4702+
include_keepalive=False):
46964703
"""Returns the data from the user stream.
46974704
46984705
Args:
@@ -4740,11 +4747,20 @@ def GetUserStream(self,
47404747
if filter_level is not None:
47414748
data['filter_level'] = filter_level
47424749

4743-
resp = self._RequestStream(url, 'POST', data=data)
4750+
resp = self._RequestStream(url, 'POST', data=data, session=session)
4751+
# The Twitter streaming API sends keep-alive newlines every 30s if there has not been other
4752+
# traffic, and specifies that streams should only be reset after three keep-alive ticks.
4753+
#
4754+
# The original implementation of this API didn't expose keep-alive signals to the user,
4755+
# making it difficult to determine whether the connection should be hung up or not.
4756+
#
4757+
# https://dev.twitter.com/streaming/overview/connecting
47444758
for line in resp.iter_lines():
47454759
if line:
47464760
data = self._ParseAndCheckTwitter(line.decode('utf-8'))
47474761
yield data
4762+
elif include_keepalive:
4763+
yield None
47484764

47494765
def VerifyCredentials(self, include_entities=None, skip_status=None, include_email=None):
47504766
"""Returns a twitter.User instance if the authenticating user is valid.
@@ -5073,7 +5089,7 @@ def _RequestUrl(self, url, verb, data=None, json=None, enforce_auth=True):
50735089

50745090
return resp
50755091

5076-
def _RequestStream(self, url, verb, data=None):
5092+
def _RequestStream(self, url, verb, data=None, session=None):
50775093
"""Request a stream of data.
50785094
50795095
Args:
@@ -5087,19 +5103,21 @@ def _RequestStream(self, url, verb, data=None):
50875103
Returns:
50885104
A twitter stream.
50895105
"""
5106+
session = session or requests.Session()
5107+
50905108
if verb == 'POST':
50915109
try:
5092-
return requests.post(url, data=data, stream=True,
5093-
auth=self.__auth,
5094-
timeout=self._timeout,
5095-
proxies=self.proxies)
5110+
return session.post(url, data=data, stream=True,
5111+
auth=self.__auth,
5112+
timeout=self._timeout,
5113+
proxies=self.proxies)
50965114
except requests.RequestException as e:
50975115
raise TwitterError(str(e))
50985116
if verb == 'GET':
50995117
url = self._BuildUrl(url, extra_params=data)
51005118
try:
5101-
return requests.get(url, stream=True, auth=self.__auth,
5102-
timeout=self._timeout, proxies=self.proxies)
5119+
return session.get(url, stream=True, auth=self.__auth,
5120+
timeout=self._timeout, proxies=self.proxies)
51035121
except requests.RequestException as e:
51045122
raise TwitterError(str(e))
51055123
return 0 # if not a POST or GET request

twitter/models.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,12 @@ class UserStatus(TwitterModel):
282282
""" A class representing the UserStatus structure. This is an abbreviated
283283
form of the twitter.User object. """
284284

285-
connections = {'following': False,
286-
'followed_by': False,
287-
'following_received': False,
288-
'following_requested': False,
289-
'blocking': False,
290-
'muting': False}
285+
_connections = {'following': False,
286+
'followed_by': False,
287+
'following_received': False,
288+
'following_requested': False,
289+
'blocking': False,
290+
'muting': False}
291291

292292
def __init__(self, **kwargs):
293293
self.param_defaults = {
@@ -307,10 +307,19 @@ def __init__(self, **kwargs):
307307
setattr(self, param, kwargs.get(param, default))
308308

309309
if 'connections' in kwargs:
310-
for param in self.connections:
310+
for param in self._connections:
311311
if param in kwargs['connections']:
312312
setattr(self, param, True)
313313

314+
@property
315+
def connections(self):
316+
return {'following': self.following,
317+
'followed_by': self.followed_by,
318+
'following_received': self.following_received,
319+
'following_requested': self.following_requested,
320+
'blocking': self.blocking,
321+
'muting': self.muting}
322+
314323
def __repr__(self):
315324
connections = [param for param in self.connections if getattr(self, param)]
316325
return "UserStatus(ID={uid}, ScreenName={sn}, Connections=[{conn}])".format(
@@ -337,19 +346,24 @@ def __init__(self, **kwargs):
337346
'friends_count': None,
338347
'geo_enabled': None,
339348
'id': None,
349+
'id_str': None,
340350
'lang': None,
341351
'listed_count': None,
342352
'location': None,
343353
'name': None,
344354
'notifications': None,
345355
'profile_background_color': None,
346356
'profile_background_image_url': None,
357+
'profile_background_image_url_https': None,
347358
'profile_background_tile': None,
348359
'profile_banner_url': None,
349360
'profile_image_url': None,
361+
'profile_image_url_https': None,
350362
'profile_link_color': None,
363+
'profile_sidebar_border_color': None,
351364
'profile_sidebar_fill_color': None,
352365
'profile_text_color': None,
366+
'profile_use_background_image': None,
353367
'protected': None,
354368
'screen_name': None,
355369
'status': None,
@@ -358,6 +372,8 @@ def __init__(self, **kwargs):
358372
'url': None,
359373
'utc_offset': None,
360374
'verified': None,
375+
'withheld_in_countries': None,
376+
'withheld_scope': None,
361377
}
362378

363379
for (param, default) in self.param_defaults.items():

0 commit comments

Comments
 (0)