Twython using requests/requests-oauth

This commit is contained in:
Michael Helmick 2012-03-08 12:20:04 -05:00
parent e3d9ed656b
commit 8630dc3f03

View file

@ -13,12 +13,13 @@ __version__ = "1.4.6"
import urllib import urllib
import urllib2 import urllib2
import httplib2
import re import re
import inspect import inspect
import time import time
import requests import requests
from requests.exceptions import RequestException
from oauth_hook import OAuthHook
import oauth2 as oauth import oauth2 as oauth
try: try:
@ -124,7 +125,7 @@ class TwythonAuthError(TwythonError):
class Twython(object): class Twython(object):
def __init__(self, twitter_token=None, twitter_secret=None, oauth_token=None, oauth_token_secret=None, \ def __init__(self, twitter_token=None, twitter_secret=None, oauth_token=None, oauth_token_secret=None, \
headers=None, callback_url=None, client_args=None): headers=None, callback_url=None):
"""setup(self, oauth_token = None, headers = None) """setup(self, oauth_token = None, headers = None)
Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below).
@ -141,11 +142,16 @@ class Twython(object):
** Note: versioning is not currently used by search.twitter functions; ** Note: versioning is not currently used by search.twitter functions;
when Twitter moves their junk, it'll be supported. when Twitter moves their junk, it'll be supported.
""" """
OAuthHook.consumer_key = twitter_token
OAuthHook.consumer_secret = twitter_secret
# Needed for hitting that there API. # Needed for hitting that there API.
self.request_token_url = 'http://twitter.com/oauth/request_token' self.request_token_url = 'http://twitter.com/oauth/request_token'
self.access_token_url = 'http://twitter.com/oauth/access_token' self.access_token_url = 'http://twitter.com/oauth/access_token'
self.authorize_url = 'http://twitter.com/oauth/authorize' self.authorize_url = 'http://twitter.com/oauth/authorize'
self.authenticate_url = 'http://twitter.com/oauth/authenticate' self.authenticate_url = 'http://twitter.com/oauth/authenticate'
self.twitter_token = twitter_token self.twitter_token = twitter_token
self.twitter_secret = twitter_secret self.twitter_secret = twitter_secret
self.oauth_token = oauth_token self.oauth_token = oauth_token
@ -155,29 +161,23 @@ class Twython(object):
# If there's headers, set them, otherwise be an embarassing parent for their own good. # If there's headers, set them, otherwise be an embarassing parent for their own good.
self.headers = headers self.headers = headers
if self.headers is None: if self.headers is None:
self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} self.headers = {'User-agent': 'Twython Python Twitter Library v1.4.6'}
self.consumer = None self.client = None
self.token = None
client_args = client_args or {}
if self.twitter_token is not None and self.twitter_secret is not None: if self.twitter_token is not None and self.twitter_secret is not None:
self.consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) self.client = requests.session(hooks={'pre_request': OAuthHook()})
if self.oauth_token is not None and self.oauth_secret is not None: if self.oauth_token is not None and self.oauth_secret is not None:
self.token = oauth.Token(oauth_token, oauth_token_secret) self.oauth_hook = OAuthHook(self.oauth_token, self.oauth_secret)
self.client = requests.session(hooks={'pre_request': self.oauth_hook})
# Filter down through the possibilities here - if they have a token, if they're first stage, etc. # Filter down through the possibilities here - if they have a token, if they're first stage, etc.
if self.consumer is not None and self.token is not None: if self.client is None:
self.client = oauth.Client(self.consumer, self.token, **client_args)
elif self.consumer is not None:
self.client = oauth.Client(self.consumer, **client_args)
else:
# If they don't do authentication, but still want to request unprotected resources, we need an opener. # If they don't do authentication, but still want to request unprotected resources, we need an opener.
self.client = httplib2.Http(**client_args) self.client = requests.session()
# register available funcs to allow listing name when debugging.
# register available funcs to allow listing name when debugging.
def setFunc(key): def setFunc(key):
return lambda **kwargs: self._constructFunc(key, **kwargs) return lambda **kwargs: self._constructFunc(key, **kwargs)
for key in api_table.keys(): for key in api_table.keys():
@ -194,17 +194,19 @@ class Twython(object):
base_url + fn['url'] base_url + fn['url']
) )
# Then open and load that shiiit, yo. TODO: check HTTP method method = fn['method'].lower()
# and junk, handle errors/authentication if not method in ('get', 'post', 'delete'):
if fn['method'] == 'POST': raise TwythonError('Method must be of GET, POST or DELETE')
myargs = urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items()))
resp, content = self.client.request(base, fn['method'], myargs, headers=self.headers)
else:
myargs = ["%s=%s" % (key, value) for (key, value) in kwargs.iteritems()]
url = "%s?%s" % (base, "&".join(myargs))
resp, content = self.client.request(url, fn['method'], headers=self.headers)
return simplejson.loads(content.decode('utf-8')) if method == 'get':
myargs = ['%s=%s' % (key, value) for (key, value) in kwargs.iteritems()]
else:
myargs = kwargs
func = getattr(self.client, method)
response = func(base, data=myargs)
return simplejson.loads(response.content.decode('utf-8'))
def get_authentication_tokens(self): def get_authentication_tokens(self):
""" """
@ -218,12 +220,14 @@ class Twython(object):
if OAUTH_LIB_SUPPORTS_CALLBACK: if OAUTH_LIB_SUPPORTS_CALLBACK:
request_args['callback_url'] = callback_url request_args['callback_url'] = callback_url
resp, content = self.client.request(self.request_token_url, "GET", **request_args) response = self.client.get(self.request_token_url, **request_args)
if resp['status'] != '200': if response.status_code != 200:
raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content))
request_tokens = dict(parse_qsl(content)) request_tokens = dict(parse_qsl(response.content))
if not request_tokens:
raise TwythonError('Unable to decode request tokens.')
oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true'
@ -250,8 +254,13 @@ class Twython(object):
Returns authorized tokens after they go through the auth_url phase. Returns authorized tokens after they go through the auth_url phase.
""" """
resp, content = self.client.request(self.access_token_url, "GET")
return dict(parse_qsl(content)) response = self.client.get(self.access_token_url)
authorized_tokens = dict(parse_qsl(response.content))
if not authorized_tokens:
raise TwythonError('Unable to decode authorized tokens.')
return authorized_tokens
# ------------------------------------------------------------------------------------------------------------------------ # ------------------------------------------------------------------------------------------------------------------------
# The following methods are all different in some manner or require special attention with regards to the Twitter API. # The following methods are all different in some manner or require special attention with regards to the Twitter API.
@ -294,9 +303,9 @@ class Twython(object):
lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs)
try: try:
resp, content = self.client.request(lookupURL, "POST", headers=self.headers) response = self.client.post(lookupURL, headers=self.headers)
return simplejson.loads(content.decode('utf-8')) return simplejson.loads(response.content.decode('utf-8'))
except HTTPError, e: except RequestException, e:
raise TwythonError("bulkUserLookup() failed with a %s error code." % e.code, e.code) raise TwythonError("bulkUserLookup() failed with a %s error code." % e.code, e.code)
def search(self, **kwargs): def search(self, **kwargs):
@ -311,17 +320,17 @@ class Twython(object):
""" """
searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs)
try: try:
resp, content = self.client.request(searchURL, "GET", headers=self.headers) response = self.client.get(searchURL, headers=self.headers)
if int(resp.status) == 420: if response.status_code == 420:
retry_wait_seconds = resp['retry-after'] retry_wait_seconds = response.headers.get('retry-after')
raise TwythonRateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." % raise TwythonRateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." %
retry_wait_seconds, retry_wait_seconds,
retry_wait_seconds, retry_wait_seconds,
resp.status) response.status_code)
return simplejson.loads(content.decode('utf-8')) return simplejson.loads(response.content.decode('utf-8'))
except HTTPError, e: except RequestException, e:
raise TwythonError("getSearchTimeline() failed with a %s error code." % e.code, e.code) raise TwythonError("getSearchTimeline() failed with a %s error code." % e.code, e.code)
def searchTwitter(self, **kwargs): def searchTwitter(self, **kwargs):
@ -342,9 +351,9 @@ class Twython(object):
""" """
searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs)
try: try:
resp, content = self.client.request(searchURL, "GET", headers=self.headers) response = self.client.get(searchURL, headers=self.headers)
data = simplejson.loads(content.decode('utf-8')) data = simplejson.loads(response.content.decode('utf-8'))
except HTTPError, e: except RequestException, e:
raise TwythonError("searchGen() failed with a %s error code." % e.code, e.code) raise TwythonError("searchGen() failed with a %s error code." % e.code, e.code)
if not data['results']: if not data['results']:
@ -388,9 +397,9 @@ class Twython(object):
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
""" """
try: try:
resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, id), headers=self.headers) response = self.client.get("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, id), headers=self.headers)
return simplejson.loads(content.decode('utf-8')) return simplejson.loads(response.content.decode('utf-8'))
except HTTPError, e: except RequestException, e:
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
def isListSubscriber(self, username, list_id, id, version=1): def isListSubscriber(self, username, list_id, id, version=1):
@ -407,9 +416,9 @@ class Twython(object):
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
""" """
try: try:
resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, id), headers=self.headers) response = self.client.get("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, id), headers=self.headers)
return simplejson.loads(content.decode('utf-8')) return simplejson.loads(response.content.decode('utf-8'))
except HTTPError, e: except RequestException, e:
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
# The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set.
@ -448,10 +457,35 @@ class Twython(object):
""" """
return self._media_update('https://upload.twitter.com/%d/statuses/update_with_media.json' % version, {'media': (file_, open(file_, 'rb'))}, **params) return self._media_update('https://upload.twitter.com/%d/statuses/update_with_media.json' % version, {'media': (file_, open(file_, 'rb'))}, **params)
def _media_update(self, url, file_, params={}): def _media_update(self, url, file_, params=None):
params = params or {}
'''
***
Techincally, this code will work one day. :P
I think @kennethreitz is working with somebody to
get actual OAuth stuff implemented into `requests`
Until then we will have to use `request-oauth` and
currently the code below should work, but doesn't.
See this gist (https://gist.github.com/2002119)
request-oauth is missing oauth_body_hash from the
header.. that MIGHT be why it's not working..
I haven't debugged enough.
- Mike Helmick
***
self.oauth_hook.header_auth = True
self.client = requests.session(hooks={'pre_request': self.oauth_hook})
print self.oauth_hook
response = self.client.post(url, data=params, files=file_, headers=self.headers)
print response.headers
return response.content
'''
oauth_params = { oauth_params = {
'oauth_version': "1.0", 'oauth_consumer_key': self.oauth_hook.consumer_key,
'oauth_nonce': oauth.generate_nonce(length=41), 'oauth_token': self.oauth_token,
'oauth_timestamp': int(time.time()), 'oauth_timestamp': int(time.time()),
} }
@ -460,7 +494,28 @@ class Twython(object):
#sign the fake request. #sign the fake request.
signature_method = oauth.SignatureMethod_HMAC_SHA1() signature_method = oauth.SignatureMethod_HMAC_SHA1()
faux_req.sign_request(signature_method, self.consumer, self.token)
class dotdict(dict):
"""
This is a helper func. because python-oauth2 wants a
dict in dot notation.
"""
def __getattr__(self, attr):
return self.get(attr, None)
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
consumer = {
'key': self.oauth_hook.consumer_key,
'secret': self.oauth_hook.consumer_secret
}
token = {
'key': self.oauth_token,
'secret': self.oauth_secret
}
faux_req.sign_request(signature_method, dotdict(consumer), dotdict(token))
#create a dict out of the fake request signed params #create a dict out of the fake request signed params
self.headers.update(faux_req.to_header()) self.headers.update(faux_req.to_header())
@ -482,16 +537,14 @@ class Twython(object):
if size: if size:
url = self.constructApiURL(url, {'size': size}) url = self.constructApiURL(url, {'size': size})
client = httplib2.Http() #client.follow_redirects = False
client.follow_redirects = False response = self.client.get(url, allow_redirects=False)
resp, content = client.request(url, 'GET') image_url = response.headers.get('location')
if resp.status in (301, 302, 303, 307): if response.status_code in (301, 302, 303, 307) and image_url is not None:
return resp['location'] return image_url
elif resp.status == 200:
return simplejson.loads(content.decode('utf-8'))
raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) raise TwythonError("getProfileImageUrl() failed with a %d error code." % response.status_code, response.status_code)
@staticmethod @staticmethod
def unicode2utf8(text): def unicode2utf8(text):