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

Commit c71c197

Browse files
author
Jonathan Schultz
committed
Merge branch 'master' of https://github.com/bear/python-twitter
2 parents e8313a9 + e02e9b8 commit c71c197

4 files changed

Lines changed: 84 additions & 73 deletions

File tree

examples/tweet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
__author__ = 'dewitt@google.com'
66

7-
import ConfigParser
7+
import configparser
88
import getopt
99
import os
1010
import sys

tests/test_api_30.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
from __future__ import unicode_literals, print_function
33

44
import json
5+
import os
56
import re
67
import sys
8+
from tempfile import NamedTemporaryFile
79
import unittest
10+
try:
11+
from unittest.mock import patch
12+
except ImportError:
13+
from mock import patch
814
import warnings
915

1016
import twitter
@@ -1713,3 +1719,16 @@ def test_UpdateBackgroundImage_deprecation(self):
17131719
with warnings.catch_warnings(record=True) as w:
17141720
resp = self.api.UpdateBackgroundImage(image='testdata/168NQ.jpg')
17151721
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
1722+
1723+
@responses.activate
1724+
@patch('twitter.api.Api.UploadMediaChunked')
1725+
def test_UploadSmallVideoUsesChunkedData(self, mocker):
1726+
responses.add(POST, DEFAULT_URL, body='{}')
1727+
video = NamedTemporaryFile(suffix='.mp4')
1728+
video.write(b'10' * 1024)
1729+
video.seek(0, 0)
1730+
1731+
resp = self.api.PostUpdate('test', media=video)
1732+
assert os.path.getsize(video.name) <= 1024 * 1024
1733+
assert isinstance(resp, twitter.Status)
1734+
assert twitter.api.Api.UploadMediaChunked.called

twitter/api.py

Lines changed: 61 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@
7070
)
7171

7272

73-
warnings.simplefilter('always', DeprecationWarning)
74-
7573
CHARACTER_LIMIT = 140
7674

7775
# A singleton representing a lazily instantiated FileCache.
@@ -158,7 +156,8 @@ def __init__(self,
158156
debugHTTP=False,
159157
timeout=None,
160158
sleep_on_rate_limit=False,
161-
tweet_mode='compat'):
159+
tweet_mode='compat',
160+
proxies=None):
162161
"""Instantiate a new twitter.Api object.
163162
164163
Args:
@@ -209,12 +208,15 @@ def __init__(self,
209208
tweet_mode (str, optional):
210209
Whether to use the new (as of Sept. 2016) extended tweet mode. See docs for
211210
details. Choices are ['compatibility', 'extended'].
211+
proxies (dict, optional):
212+
A dictionary of proxies for the request to pass through, if not specified
213+
allows requests lib to use environmental variables for proxy if any.
212214
"""
213215

214216
# check to see if the library is running on a Google App Engine instance
215217
# see GAE.rst for more information
216218
if os.environ:
217-
if 'Google App Engine' in os.environ.get('SERVER_SOFTWARE', ''):
219+
if 'APPENGINE_RUNTIME' in os.environ.keys():
218220
import requests_toolbelt.adapters.appengine # Adapter ensures requests use app engine's urlfetch
219221
requests_toolbelt.adapters.appengine.monkeypatch()
220222
cache = None # App Engine does not like this caching strategy, disable caching
@@ -235,6 +237,7 @@ def __init__(self,
235237
self.rate_limit = RateLimit()
236238
self.sleep_on_rate_limit = sleep_on_rate_limit
237239
self.tweet_mode = tweet_mode
240+
self.proxies = proxies
238241

239242
if base_url is None:
240243
self.base_url = 'https://api.twitter.com/1.1'
@@ -1055,6 +1058,7 @@ def PostUpdate(self,
10551058
parameters['attachment_url'] = attachment_url
10561059

10571060
if media:
1061+
chunked_types = ['video/mp4', 'video/quicktime', 'image/gif']
10581062
media_ids = []
10591063
if isinstance(media, int):
10601064
media_ids.append(media)
@@ -1070,9 +1074,8 @@ def PostUpdate(self,
10701074
_, _, file_size, media_type = parse_media_file(media_file)
10711075
if media_type == 'image/gif' or media_type == 'video/mp4':
10721076
raise TwitterError(
1073-
'You cannot post more than 1 GIF or 1 video in a '
1074-
'single status.')
1075-
if file_size > self.chunk_size:
1077+
'You cannot post more than 1 GIF or 1 video in a single status.')
1078+
if file_size > self.chunk_size or media_type in chunked_types:
10761079
media_id = self.UploadMediaChunked(
10771080
media=media_file,
10781081
additional_owners=media_additional_owners,
@@ -1084,13 +1087,11 @@ def PostUpdate(self,
10841087
media_category=media_category)
10851088
media_ids.append(media_id)
10861089
else:
1087-
_, _, file_size, _ = parse_media_file(media)
1088-
if file_size > self.chunk_size:
1089-
media_ids.append(
1090-
self.UploadMediaChunked(media, media_additional_owners))
1090+
_, _, file_size, media_type = parse_media_file(media)
1091+
if file_size > self.chunk_size or media_type in chunked_types:
1092+
media_ids.append(self.UploadMediaChunked(media, media_additional_owners))
10911093
else:
1092-
media_ids.append(
1093-
self.UploadMediaSimple(media, media_additional_owners))
1094+
media_ids.append(self.UploadMediaSimple(media, media_additional_owners))
10941095
parameters['media_ids'] = ','.join([str(mid) for mid in media_ids])
10951096

10961097
if latitude is not None and longitude is not None:
@@ -1292,7 +1293,7 @@ def _UploadMediaChunkedAppend(self,
12921293

12931294
try:
12941295
media_fp.close()
1295-
except:
1296+
except Exception as e:
12961297
pass
12971298

12981299
return True
@@ -1731,7 +1732,7 @@ def GetRetweeters(self,
17311732
"""
17321733
url = '%s/statuses/retweeters/ids.json' % (self.base_url)
17331734
parameters = {
1734-
'status_id': enf_type('status_id', int, status_id),
1735+
'id': enf_type('id', int, status_id),
17351736
'stringify_ids': enf_type('stringify_ids', bool, stringify_ids)
17361737
}
17371738

@@ -1741,7 +1742,7 @@ def GetRetweeters(self,
17411742
while True:
17421743
if cursor:
17431744
try:
1744-
parameters['count'] = int(cursor)
1745+
parameters['cursor'] = int(cursor)
17451746
except ValueError:
17461747
raise TwitterError({'message': "cursor must be an integer"})
17471748
resp = self._RequestUrl(url, 'GET', data=parameters)
@@ -2858,47 +2859,38 @@ def UsersLookup(self,
28582859
are queried is the union of all specified parameters.
28592860
28602861
Args:
2861-
user_id:
2862-
A list of user_ids to retrieve extended information. [Optional]
2863-
screen_name:
2864-
A list of screen_names to retrieve extended information. [Optional]
2865-
users:
2862+
user_id (int, list, optional):
2863+
A list of user_ids to retrieve extended information.
2864+
screen_name (str, optional):
2865+
A list of screen_names to retrieve extended information.
2866+
users (list, optional):
28662867
A list of twitter.User objects to retrieve extended information.
2867-
[Optional]
2868-
include_entities:
2868+
include_entities (bool, optional):
28692869
The entities node that may appear within embedded statuses will be
2870-
disincluded when set to False. [Optional]
2870+
excluded when set to False.
28712871
28722872
Returns:
28732873
A list of twitter.User objects for the requested users
28742874
"""
2875-
if not user_id and not screen_name and not users:
2876-
raise TwitterError({'message': "Specify at least one of user_id, screen_name, or users."})
2875+
if not any([user_id, screen_name, users]):
2876+
raise TwitterError("Specify at least one of user_id, screen_name, or users.")
28772877

28782878
url = '%s/users/lookup.json' % self.base_url
2879-
parameters = {}
2879+
parameters = {
2880+
'include_entities': include_entities
2881+
}
28802882
uids = list()
28812883
if user_id:
28822884
uids.extend(user_id)
28832885
if users:
28842886
uids.extend([u.id for u in users])
28852887
if len(uids):
2886-
parameters['user_id'] = ','.join(["%s" % u for u in uids])
2888+
parameters['user_id'] = ','.join([str(u) for u in uids])
28872889
if screen_name:
28882890
parameters['screen_name'] = ','.join(screen_name)
2889-
if not include_entities:
2890-
parameters['include_entities'] = 'false'
28912891

28922892
resp = self._RequestUrl(url, 'GET', data=parameters)
2893-
try:
2894-
data = self._ParseAndCheckTwitter(resp.content.decode('utf-8'))
2895-
except TwitterError as e:
2896-
_, e, _ = sys.exc_info()
2897-
t = e.args[0]
2898-
if len(t) == 1 and ('code' in t[0]) and (t[0]['code'] == 34):
2899-
data = []
2900-
else:
2901-
raise
2893+
data = self._ParseAndCheckTwitter(resp.content.decode('utf-8'))
29022894
return [User.NewFromJsonDict(u) for u in data]
29032895

29042896
def GetUser(self,
@@ -2908,29 +2900,27 @@ def GetUser(self,
29082900
"""Returns a single user.
29092901
29102902
Args:
2911-
user_id:
2912-
The id of the user to retrieve. [Optional]
2913-
screen_name:
2903+
user_id (int, optional):
2904+
The id of the user to retrieve.
2905+
screen_name (str, optional):
29142906
The screen name of the user for whom to return results for.
29152907
Either a user_id or screen_name is required for this method.
2916-
[Optional]
2917-
include_entities:
2908+
include_entities (bool, optional):
29182909
The entities node will be omitted when set to False.
2919-
[Optional]
29202910
29212911
Returns:
29222912
A twitter.User instance representing that user
29232913
"""
29242914
url = '%s/users/show.json' % (self.base_url)
2925-
parameters = {}
2915+
parameters = {
2916+
'include_entities': include_entities
2917+
}
29262918
if user_id:
29272919
parameters['user_id'] = user_id
29282920
elif screen_name:
29292921
parameters['screen_name'] = screen_name
29302922
else:
2931-
raise TwitterError({'message': "Specify at least one of user_id or screen_name."})
2932-
if not include_entities:
2933-
parameters['include_entities'] = 'false'
2923+
raise TwitterError("Specify at least one of user_id or screen_name.")
29342924

29352925
resp = self._RequestUrl(url, 'GET', data=parameters)
29362926
data = self._ParseAndCheckTwitter(resp.content.decode('utf-8'))
@@ -3621,8 +3611,8 @@ def CreateList(self, name, mode=None, description=None):
36213611
return List.NewFromJsonDict(data)
36223612

36233613
def DestroyList(self,
3624-
owner_screen_name=False,
3625-
owner_id=False,
3614+
owner_screen_name=None,
3615+
owner_id=None,
36263616
list_id=None,
36273617
slug=None):
36283618
"""Destroys the list identified by list_id or slug and one of
@@ -3660,8 +3650,8 @@ def DestroyList(self,
36603650
return List.NewFromJsonDict(data)
36613651

36623652
def CreateSubscription(self,
3663-
owner_screen_name=False,
3664-
owner_id=False,
3653+
owner_screen_name=None,
3654+
owner_id=None,
36653655
list_id=None,
36663656
slug=None):
36673657
"""Creates a subscription to a list by the authenticated user.
@@ -3697,8 +3687,8 @@ def CreateSubscription(self,
36973687
return User.NewFromJsonDict(data)
36983688

36993689
def DestroySubscription(self,
3700-
owner_screen_name=False,
3701-
owner_id=False,
3690+
owner_screen_name=None,
3691+
owner_id=None,
37023692
list_id=None,
37033693
slug=None):
37043694
"""Destroys the subscription to a list for the authenticated user.
@@ -3735,8 +3725,8 @@ def DestroySubscription(self,
37353725
return List.NewFromJsonDict(data)
37363726

37373727
def ShowSubscription(self,
3738-
owner_screen_name=False,
3739-
owner_id=False,
3728+
owner_screen_name=None,
3729+
owner_id=None,
37403730
list_id=None,
37413731
slug=None,
37423732
user_id=None,
@@ -4200,8 +4190,8 @@ def CreateListsMember(self,
42004190
def DestroyListsMember(self,
42014191
list_id=None,
42024192
slug=None,
4203-
owner_screen_name=False,
4204-
owner_id=False,
4193+
owner_screen_name=None,
4194+
owner_id=None,
42054195
user_id=None,
42064196
screen_name=None):
42074197
"""Destroys the subscription to a list for the authenticated user.
@@ -4219,10 +4209,10 @@ def DestroyListsMember(self,
42194209
owner_id (int, optional):
42204210
The user ID of the user who owns the list being requested by a slug.
42214211
user_id (int, optional):
4222-
The user_id or a list of user_id's to add to the list.
4212+
The user_id or a list of user_id's to remove from the list.
42234213
If not given, then screen_name is required.
42244214
screen_name (str, optional):
4225-
The screen_name or a list of Screen_name's to add to the list.
4215+
The screen_name or a list of Screen_name's to remove from the list.
42264216
If not given, then user_id is required.
42274217
42284218
Returns:
@@ -4916,7 +4906,8 @@ def _RequestChunkedUpload(self, url, headers, data):
49164906
headers=headers,
49174907
data=data,
49184908
auth=self.__auth,
4919-
timeout=self._timeout
4909+
timeout=self._timeout,
4910+
proxies=self.proxies
49204911
)
49214912
except requests.RequestException as e:
49224913
raise TwitterError(str(e))
@@ -4953,20 +4944,20 @@ def _RequestUrl(self, url, verb, data=None, json=None):
49534944
if data:
49544945
if 'media_ids' in data:
49554946
url = self._BuildUrl(url, extra_params={'media_ids': data['media_ids']})
4956-
resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout)
4947+
resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies)
49574948
elif 'media' in data:
4958-
resp = requests.post(url, files=data, auth=self.__auth, timeout=self._timeout)
4949+
resp = requests.post(url, files=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies)
49594950
else:
4960-
resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout)
4951+
resp = requests.post(url, data=data, auth=self.__auth, timeout=self._timeout, proxies=self.proxies)
49614952
elif json:
4962-
resp = requests.post(url, json=json, auth=self.__auth, timeout=self._timeout)
4953+
resp = requests.post(url, json=json, auth=self.__auth, timeout=self._timeout, proxies=self.proxies)
49634954
else:
49644955
resp = 0 # POST request, but without data or json
49654956

49664957
elif verb == 'GET':
49674958
data['tweet_mode'] = self.tweet_mode
49684959
url = self._BuildUrl(url, extra_params=data)
4969-
resp = requests.get(url, auth=self.__auth, timeout=self._timeout)
4960+
resp = requests.get(url, auth=self.__auth, timeout=self._timeout, proxies=self.proxies)
49704961

49714962
else:
49724963
resp = 0 # if not a POST or GET request
@@ -4998,14 +4989,15 @@ def _RequestStream(self, url, verb, data=None):
49984989
try:
49994990
return requests.post(url, data=data, stream=True,
50004991
auth=self.__auth,
5001-
timeout=self._timeout)
4992+
timeout=self._timeout,
4993+
proxies=self.proxies)
50024994
except requests.RequestException as e:
50034995
raise TwitterError(str(e))
50044996
if verb == 'GET':
50054997
url = self._BuildUrl(url, extra_params=data)
50064998
try:
50074999
return requests.get(url, stream=True, auth=self.__auth,
5008-
timeout=self._timeout)
5000+
timeout=self._timeout, proxies=self.proxies)
50095001
except requests.RequestException as e:
50105002
raise TwitterError(str(e))
50115003
return 0 # if not a POST or GET request

twitter/twitter_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ def parse_media_file(passed_media):
223223
# Otherwise, if a file object was passed in the first place,
224224
# create the standard reference to media_file (i.e., rename it to fp).
225225
else:
226-
if passed_media.mode != 'rb':
227-
raise TwitterError({'message': 'File mode must be "rb".'})
226+
if passed_media.mode not in ['rb', 'rb+', 'w+b']:
227+
raise TwitterError('File mode must be "rb" or "rb+"')
228228
filename = os.path.basename(passed_media.name)
229229
data_file = passed_media
230230

@@ -233,7 +233,7 @@ def parse_media_file(passed_media):
233233

234234
try:
235235
data_file.seek(0)
236-
except:
236+
except Exception as e:
237237
pass
238238

239239
media_type = mimetypes.guess_type(os.path.basename(filename))[0]

0 commit comments

Comments
 (0)