Skip to content

Commit 5e81719

Browse files
2.1.0 Release
* Removal of oauth2 lib, `requests` has fully taken over. :) * FIXED: Obtaining auth url with specified callback was broke.. wouldn't give you auth url if you specified a callback url * Updated requests to pass the headers that are passed in the init, so User-Agent is once again `Twython Python Twitter Library v2.1.0` :thumbsup: :) * Catching exception when Stream API doesn't return valid JSON to parse * Removed `DELETE` method. As of the Spring 2012 clean up, Twitter no longer supports this method * Updated `post` internal func to take files as kwarg * `params - params or {}` only needs to be done in `_request`, just a lot of redundant code on my part, sorry ;P * Removed `bulkUserLookup`, there is no need for this to be a special case, anyone can pass a string of username or user ids and chances are if they're reading the docs and using this library they'll understand how to use `lookupUser()` in `twitter_endpoints.py` passing params provided in the Twitter docs * Changed internal `oauth_secret` variable to be more consistent with the keyword arg in the init `oauth_token_secret`
1 parent f2cd0d5 commit 5e81719

File tree

1 file changed

+30
-104
lines changed

1 file changed

+30
-104
lines changed

twython/twython.py

Lines changed: 30 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
"""
1010

1111
__author__ = "Ryan McGrath <[email protected]>"
12-
__version__ = "2.0.0"
12+
__version__ = "2.1.0"
1313

1414
import urllib
1515
import re
16-
import time
1716

1817
import requests
1918
from requests.auth import OAuth1
20-
import oauth2 as oauth
2119

2220
try:
2321
from urlparse import parse_qsl
@@ -109,8 +107,8 @@ def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_
109107
110108
:param app_key: (optional) Your applications key
111109
:param app_secret: (optional) Your applications secret key
112-
:param oauth_token: (optional) Used with oauth_secret to make authenticated calls
113-
:param oauth_secret: (optional) Used with oauth_token to make authenticated calls
110+
:param oauth_token: (optional) Used with oauth_token_secret to make authenticated calls
111+
:param oauth_token_secret: (optional) Used with oauth_token to make authenticated calls
114112
:param headers: (optional) Custom headers to send along with the request
115113
:param callback_url: (optional) If set, will overwrite the callback url set in your application
116114
"""
@@ -135,9 +133,9 @@ def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_
135133
if oauth_token is not None:
136134
self.oauth_token = u'%s' % oauth_token
137135

138-
self.oauth_secret = None
136+
self.oauth_token_secret = None
139137
if oauth_token_secret is not None:
140-
self.oauth_secret = u'%s' % oauth_token_secret
138+
self.oauth_token_secret = u'%s' % oauth_token_secret
141139

142140
self.callback_url = callback_url
143141

@@ -152,9 +150,9 @@ def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_
152150
self.auth = OAuth1(self.app_key, self.app_secret,
153151
signature_type='auth_header')
154152

155-
if self.oauth_token is not None and self.oauth_secret is not None:
153+
if self.oauth_token is not None and self.oauth_token_secret is not None:
156154
self.auth = OAuth1(self.app_key, self.app_secret,
157-
self.oauth_token, self.oauth_secret,
155+
self.oauth_token, self.oauth_token_secret,
158156
signature_type='auth_header')
159157

160158
# Filter down through the possibilities here - if they have a token, if they're first stage, etc.
@@ -182,27 +180,29 @@ def _constructFunc(self, api_call, **kwargs):
182180
)
183181

184182
method = fn['method'].lower()
185-
if not method in ('get', 'post', 'delete'):
186-
raise TwythonError('Method must be of GET, POST or DELETE')
183+
if not method in ('get', 'post'):
184+
raise TwythonError('Method must be of GET or POST')
187185

188186
content = self._request(url, method=method, params=kwargs)
189187

190188
return content
191189

192-
def _request(self, url, method='GET', params=None, api_call=None):
190+
def _request(self, url, method='GET', params=None, files=None, api_call=None):
193191
'''Internal response generator, no sense in repeating the same
194192
code twice, right? ;)
195193
'''
196194
myargs = {}
197195
method = method.lower()
198196

197+
params = params or {}
198+
199199
if method == 'get':
200200
url = '%s?%s' % (url, urllib.urlencode(params))
201201
else:
202202
myargs = params
203203

204204
func = getattr(self.client, method)
205-
response = func(url, data=myargs, auth=self.auth)
205+
response = func(url, data=myargs, files=files, headers=self.headers, auth=self.auth)
206206
content = response.content.decode('utf-8')
207207

208208
# create stash for last function intel
@@ -247,31 +247,23 @@ def _request(self, url, method='GET', params=None, api_call=None):
247247
we haven't gotten around to putting it in Twython yet. :)
248248
'''
249249

250-
def request(self, endpoint, method='GET', params=None, version=1):
251-
params = params or {}
252-
250+
def request(self, endpoint, method='GET', params=None, files=None, version=1):
253251
# In case they want to pass a full Twitter URL
254-
# i.e. http://search.twitter.com/
252+
# i.e. https://search.twitter.com/
255253
if endpoint.startswith('http://') or endpoint.startswith('https://'):
256254
url = endpoint
257255
else:
258256
url = '%s/%s.json' % (self.api_url % version, endpoint)
259257

260-
content = self._request(url, method=method, params=params, api_call=url)
258+
content = self._request(url, method=method, params=params, files=files, api_call=url)
261259

262260
return content
263261

264262
def get(self, endpoint, params=None, version=1):
265-
params = params or {}
266263
return self.request(endpoint, params=params, version=version)
267264

268-
def post(self, endpoint, params=None, version=1):
269-
params = params or {}
270-
return self.request(endpoint, 'POST', params=params, version=version)
271-
272-
def delete(self, endpoint, params=None, version=1):
273-
params = params or {}
274-
return self.request(endpoint, 'DELETE', params=params, version=version)
265+
def post(self, endpoint, params=None, files=None, version=1):
266+
return self.request(endpoint, 'POST', params=params, files=files, version=version)
275267

276268
# End Dynamic Request Methods
277269

@@ -297,16 +289,12 @@ def get_lastfunction_header(self, header):
297289
def get_authentication_tokens(self):
298290
"""Returns an authorization URL for a user to hit.
299291
"""
300-
callback_url = self.callback_url
301-
302292
request_args = {}
303-
if callback_url:
304-
request_args['oauth_callback'] = callback_url
305-
306-
method = 'get'
293+
if self.callback_url:
294+
request_args['oauth_callback'] = self.callback_url
307295

308-
func = getattr(self.client, method)
309-
response = func(self.request_token_url, data=request_args, auth=self.auth)
296+
req_url = self.request_token_url + '?' + urllib.urlencode(request_args)
297+
response = self.client.get(req_url, headers=self.headers, auth=self.auth)
310298

311299
if response.status_code != 200:
312300
raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content))
@@ -322,8 +310,8 @@ def get_authentication_tokens(self):
322310
}
323311

324312
# Use old-style callback argument if server didn't accept new-style
325-
if callback_url and not oauth_callback_confirmed:
326-
auth_url_params['oauth_callback'] = callback_url
313+
if self.callback_url and not oauth_callback_confirmed:
314+
auth_url_params['oauth_callback'] = self.callback_url
327315

328316
request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params)
329317

@@ -332,7 +320,7 @@ def get_authentication_tokens(self):
332320
def get_authorized_tokens(self):
333321
"""Returns authorized tokens after they go through the auth_url phase.
334322
"""
335-
response = self.client.get(self.access_token_url, auth=self.auth)
323+
response = self.client.get(self.access_token_url, headers=self.headers, auth=self.auth)
336324
authorized_tokens = dict(parse_qsl(response.content))
337325
if not authorized_tokens:
338326
raise TwythonError('Unable to decode authorized tokens.')
@@ -371,34 +359,6 @@ def shortenURL(url_to_shorten, shortener='http://is.gd/api.php'):
371359
def constructApiURL(base_url, params):
372360
return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), urllib.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()])
373361

374-
def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs):
375-
""" A method to do bulk user lookups against the Twitter API.
376-
377-
Documentation: https://dev.twitter.com/docs/api/1/get/users/lookup
378-
379-
:ids or screen_names: (required)
380-
:param ids: (optional) A list of integers of Twitter User IDs
381-
:param screen_names: (optional) A list of strings of Twitter Screen Names
382-
383-
:param include_entities: (optional) When set to either true, t or 1,
384-
each tweet will include a node called
385-
"entities,". This node offers a variety of
386-
metadata about the tweet in a discreet structure
387-
388-
e.g x.bulkUserLookup(screen_names=['ryanmcgrath', 'mikehelmick'],
389-
include_entities=1)
390-
"""
391-
if ids is None and screen_names is None:
392-
raise TwythonError('Please supply either a list of ids or \
393-
screen_names for this method.')
394-
395-
if ids is not None:
396-
kwargs['user_id'] = ','.join(map(str, ids))
397-
if screen_names is not None:
398-
kwargs['screen_name'] = ','.join(screen_names)
399-
400-
return self.get('users/lookup', params=kwargs, version=version)
401-
402362
def search(self, **kwargs):
403363
""" Returns tweets that match a specified query.
404364
@@ -512,44 +472,7 @@ def updateStatusWithMedia(self, file_, version=1, **params):
512472
**params)
513473

514474
def _media_update(self, url, file_, params=None):
515-
params = params or {}
516-
oauth_params = {
517-
'oauth_timestamp': int(time.time()),
518-
}
519-
520-
#create a fake request with your upload url and parameters
521-
faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params)
522-
523-
#sign the fake request.
524-
signature_method = oauth.SignatureMethod_HMAC_SHA1()
525-
526-
class dotdict(dict):
527-
"""
528-
This is a helper func. because python-oauth2 wants a
529-
dict in dot notation.
530-
"""
531-
532-
def __getattr__(self, attr):
533-
return self.get(attr, None)
534-
__setattr__ = dict.__setitem__
535-
__delattr__ = dict.__delitem__
536-
537-
consumer = {
538-
'key': self.app_key,
539-
'secret': self.app_secret
540-
}
541-
token = {
542-
'key': self.oauth_token,
543-
'secret': self.oauth_secret
544-
}
545-
546-
faux_req.sign_request(signature_method, dotdict(consumer), dotdict(token))
547-
548-
#create a dict out of the fake request signed params
549-
self.headers.update(faux_req.to_header())
550-
551-
req = requests.post(url, data=params, files=file_, headers=self.headers)
552-
return req.content
475+
return self.post(url, params=params, files=file_)
553476

554477
def getProfileImageUrl(self, username, size='normal', version=1):
555478
"""Gets the URL for the user's profile image.
@@ -624,7 +547,10 @@ def stream(data, callback):
624547

625548
for line in stream.iter_lines():
626549
if line:
627-
callback(simplejson.loads(line))
550+
try:
551+
callback(simplejson.loads(line))
552+
except ValueError:
553+
raise TwythonError('Response was not valid JSON, unable to decode.')
628554

629555
@staticmethod
630556
def unicode2utf8(text):

0 commit comments

Comments
 (0)