Merge pull request #65 from michaelhelmick/implement_requests

Implement requests (BETA, report bugs if you find them please)
This commit is contained in:
Ryan McGrath 2012-03-18 06:56:35 -07:00
commit 55b6396a60
3 changed files with 457 additions and 423 deletions

View file

@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import sys, os
from setuptools import setup from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
@ -8,31 +7,31 @@ __author__ = 'Ryan McGrath <ryan@venodesigns.net>'
__version__ = '1.4.6' __version__ = '1.4.6'
setup( setup(
# Basic package information. # Basic package information.
name = 'twython', name='twython',
version = __version__, version=__version__,
packages = find_packages(), packages=find_packages(),
# Packaging options. # Packaging options.
include_package_data = True, include_package_data=True,
# Package dependencies. # Package dependencies.
install_requires = ['simplejson', 'oauth2', 'httplib2', 'requests'], install_requires=['simplejson', 'oauth2', 'requests', 'requests-oauth'],
# Metadata for PyPI. # Metadata for PyPI.
author = 'Ryan McGrath', author='Ryan McGrath',
author_email = 'ryan@venodesigns.net', author_email='ryan@venodesigns.net',
license = 'MIT License', license='MIT License',
url = 'http://github.com/ryanmcgrath/twython/tree/master', url='http://github.com/ryanmcgrath/twython/tree/master',
keywords = 'twitter search api tweet twython', keywords='twitter search api tweet twython',
description = 'An easy (and up to date) way to access Twitter data with Python.', description='An easy (and up to date) way to access Twitter data with Python.',
long_description = open('README.markdown').read(), long_description=open('README.markdown').read(),
classifiers = [ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Communications :: Chat', 'Topic :: Communications :: Chat',
'Topic :: Internet' 'Topic :: Internet'
] ]
) )

View file

@ -1,330 +1,330 @@
""" """
A huge map of every Twitter API endpoint to a function definition in Twython. A huge map of every Twitter API endpoint to a function definition in Twython.
Parameters that need to be embedded in the URL are treated with mustaches, e.g: Parameters that need to be embedded in the URL are treated with mustaches, e.g:
{{version}}, etc {{version}}, etc
When creating new endpoint definitions, keep in mind that the name of the mustache When creating new endpoint definitions, keep in mind that the name of the mustache
will be replaced with the keyword that gets passed in to the function at call time. will be replaced with the keyword that gets passed in to the function at call time.
i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
with 47, instead of defaulting to 1 (said defaulting takes place at conversion time). with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).
""" """
# Base Twitter API url, no need to repeat this junk... # Base Twitter API url, no need to repeat this junk...
base_url = 'http://api.twitter.com/{{version}}' base_url = 'http://api.twitter.com/{{version}}'
api_table = { api_table = {
'getRateLimitStatus': { 'getRateLimitStatus': {
'url': '/account/rate_limit_status.json', 'url': '/account/rate_limit_status.json',
'method': 'GET', 'method': 'GET',
}, },
'verifyCredentials': { 'verifyCredentials': {
'url': '/account/verify_credentials.json', 'url': '/account/verify_credentials.json',
'method': 'GET', 'method': 'GET',
}, },
'endSession' : { 'endSession': {
'url': '/account/end_session.json', 'url': '/account/end_session.json',
'method': 'POST', 'method': 'POST',
}, },
# Timeline methods # Timeline methods
'getPublicTimeline': { 'getPublicTimeline': {
'url': '/statuses/public_timeline.json', 'url': '/statuses/public_timeline.json',
'method': 'GET', 'method': 'GET',
}, },
'getHomeTimeline': { 'getHomeTimeline': {
'url': '/statuses/home_timeline.json', 'url': '/statuses/home_timeline.json',
'method': 'GET', 'method': 'GET',
}, },
'getUserTimeline': { 'getUserTimeline': {
'url': '/statuses/user_timeline.json', 'url': '/statuses/user_timeline.json',
'method': 'GET', 'method': 'GET',
}, },
'getFriendsTimeline': { 'getFriendsTimeline': {
'url': '/statuses/friends_timeline.json', 'url': '/statuses/friends_timeline.json',
'method': 'GET', 'method': 'GET',
}, },
# Interfacing with friends/followers # Interfacing with friends/followers
'getUserMentions': { 'getUserMentions': {
'url': '/statuses/mentions.json', 'url': '/statuses/mentions.json',
'method': 'GET', 'method': 'GET',
}, },
'getFriendsStatus': { 'getFriendsStatus': {
'url': '/statuses/friends.json', 'url': '/statuses/friends.json',
'method': 'GET', 'method': 'GET',
}, },
'getFollowersStatus': { 'getFollowersStatus': {
'url': '/statuses/followers.json', 'url': '/statuses/followers.json',
'method': 'GET', 'method': 'GET',
}, },
'createFriendship': { 'createFriendship': {
'url': '/friendships/create.json', 'url': '/friendships/create.json',
'method': 'POST', 'method': 'POST',
}, },
'destroyFriendship': { 'destroyFriendship': {
'url': '/friendships/destroy.json', 'url': '/friendships/destroy.json',
'method': 'POST', 'method': 'POST',
}, },
'getFriendsIDs': { 'getFriendsIDs': {
'url': '/friends/ids.json', 'url': '/friends/ids.json',
'method': 'GET', 'method': 'GET',
}, },
'getFollowersIDs': { 'getFollowersIDs': {
'url': '/followers/ids.json', 'url': '/followers/ids.json',
'method': 'GET', 'method': 'GET',
}, },
'getIncomingFriendshipIDs': { 'getIncomingFriendshipIDs': {
'url': '/friendships/incoming.json', 'url': '/friendships/incoming.json',
'method': 'GET', 'method': 'GET',
}, },
'getOutgoingFriendshipIDs': { 'getOutgoingFriendshipIDs': {
'url': '/friendships/outgoing.json', 'url': '/friendships/outgoing.json',
'method': 'GET', 'method': 'GET',
}, },
# Retweets # Retweets
'reTweet': { 'reTweet': {
'url': '/statuses/retweet/{{id}}.json', 'url': '/statuses/retweet/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
'getRetweets': { 'getRetweets': {
'url': '/statuses/retweets/{{id}}.json', 'url': '/statuses/retweets/{{id}}.json',
'method': 'GET', 'method': 'GET',
}, },
'retweetedOfMe': { 'retweetedOfMe': {
'url': '/statuses/retweets_of_me.json', 'url': '/statuses/retweets_of_me.json',
'method': 'GET', 'method': 'GET',
}, },
'retweetedByMe': { 'retweetedByMe': {
'url': '/statuses/retweeted_by_me.json', 'url': '/statuses/retweeted_by_me.json',
'method': 'GET', 'method': 'GET',
}, },
'retweetedToMe': { 'retweetedToMe': {
'url': '/statuses/retweeted_to_me.json', 'url': '/statuses/retweeted_to_me.json',
'method': 'GET', 'method': 'GET',
}, },
# User methods # User methods
'showUser': { 'showUser': {
'url': '/users/show.json', 'url': '/users/show.json',
'method': 'GET', 'method': 'GET',
}, },
'searchUsers': { 'searchUsers': {
'url': '/users/search.json', 'url': '/users/search.json',
'method': 'GET', 'method': 'GET',
}, },
'lookupUser': { 'lookupUser': {
'url': '/users/lookup.json', 'url': '/users/lookup.json',
'method': 'GET', 'method': 'GET',
}, },
# Status methods - showing, updating, destroying, etc. # Status methods - showing, updating, destroying, etc.
'showStatus': { 'showStatus': {
'url': '/statuses/show/{{id}}.json', 'url': '/statuses/show/{{id}}.json',
'method': 'GET', 'method': 'GET',
}, },
'updateStatus': { 'updateStatus': {
'url': '/statuses/update.json', 'url': '/statuses/update.json',
'method': 'POST', 'method': 'POST',
}, },
'destroyStatus': { 'destroyStatus': {
'url': '/statuses/destroy/{{id}}.json', 'url': '/statuses/destroy/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
# Direct Messages - getting, sending, effing, etc. # Direct Messages - getting, sending, effing, etc.
'getDirectMessages': { 'getDirectMessages': {
'url': '/direct_messages.json', 'url': '/direct_messages.json',
'method': 'GET', 'method': 'GET',
}, },
'getSentMessages': { 'getSentMessages': {
'url': '/direct_messages/sent.json', 'url': '/direct_messages/sent.json',
'method': 'GET', 'method': 'GET',
}, },
'sendDirectMessage': { 'sendDirectMessage': {
'url': '/direct_messages/new.json', 'url': '/direct_messages/new.json',
'method': 'POST', 'method': 'POST',
}, },
'destroyDirectMessage': { 'destroyDirectMessage': {
'url': '/direct_messages/destroy/{{id}}.json', 'url': '/direct_messages/destroy/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
# Friendship methods # Friendship methods
'checkIfFriendshipExists': { 'checkIfFriendshipExists': {
'url': '/friendships/exists.json', 'url': '/friendships/exists.json',
'method': 'GET', 'method': 'GET',
}, },
'showFriendship': { 'showFriendship': {
'url': '/friendships/show.json', 'url': '/friendships/show.json',
'method': 'GET', 'method': 'GET',
}, },
# Profile methods # Profile methods
'updateProfile': { 'updateProfile': {
'url': '/account/update_profile.json', 'url': '/account/update_profile.json',
'method': 'POST', 'method': 'POST',
}, },
'updateProfileColors': { 'updateProfileColors': {
'url': '/account/update_profile_colors.json', 'url': '/account/update_profile_colors.json',
'method': 'POST', 'method': 'POST',
}, },
# Favorites methods # Favorites methods
'getFavorites': { 'getFavorites': {
'url': '/favorites.json', 'url': '/favorites.json',
'method': 'GET', 'method': 'GET',
}, },
'createFavorite': { 'createFavorite': {
'url': '/favorites/create/{{id}}.json', 'url': '/favorites/create/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
'destroyFavorite': { 'destroyFavorite': {
'url': '/favorites/destroy/{{id}}.json', 'url': '/favorites/destroy/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
# Blocking methods # Blocking methods
'createBlock': { 'createBlock': {
'url': '/blocks/create/{{id}}.json', 'url': '/blocks/create/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
'destroyBlock': { 'destroyBlock': {
'url': '/blocks/destroy/{{id}}.json', 'url': '/blocks/destroy/{{id}}.json',
'method': 'POST', 'method': 'POST',
}, },
'getBlocking': { 'getBlocking': {
'url': '/blocks/blocking.json', 'url': '/blocks/blocking.json',
'method': 'GET', 'method': 'GET',
}, },
'getBlockedIDs': { 'getBlockedIDs': {
'url': '/blocks/blocking/ids.json', 'url': '/blocks/blocking/ids.json',
'method': 'GET', 'method': 'GET',
}, },
'checkIfBlockExists': { 'checkIfBlockExists': {
'url': '/blocks/exists.json', 'url': '/blocks/exists.json',
'method': 'GET', 'method': 'GET',
}, },
# Trending methods # Trending methods
'getCurrentTrends': { 'getCurrentTrends': {
'url': '/trends/current.json', 'url': '/trends/current.json',
'method': 'GET', 'method': 'GET',
}, },
'getDailyTrends': { 'getDailyTrends': {
'url': '/trends/daily.json', 'url': '/trends/daily.json',
'method': 'GET', 'method': 'GET',
}, },
'getWeeklyTrends': { 'getWeeklyTrends': {
'url': '/trends/weekly.json', 'url': '/trends/weekly.json',
'method': 'GET', 'method': 'GET',
}, },
'availableTrends': { 'availableTrends': {
'url': '/trends/available.json', 'url': '/trends/available.json',
'method': 'GET', 'method': 'GET',
}, },
'trendsByLocation': { 'trendsByLocation': {
'url': '/trends/{{woeid}}.json', 'url': '/trends/{{woeid}}.json',
'method': 'GET', 'method': 'GET',
}, },
# Saved Searches # Saved Searches
'getSavedSearches': { 'getSavedSearches': {
'url': '/saved_searches.json', 'url': '/saved_searches.json',
'method': 'GET', 'method': 'GET',
}, },
'showSavedSearch': { 'showSavedSearch': {
'url': '/saved_searches/show/{{id}}.json', 'url': '/saved_searches/show/{{id}}.json',
'method': 'GET', 'method': 'GET',
}, },
'createSavedSearch': { 'createSavedSearch': {
'url': '/saved_searches/create.json', 'url': '/saved_searches/create.json',
'method': 'GET', 'method': 'GET',
}, },
'destroySavedSearch': { 'destroySavedSearch': {
'url': '/saved_searches/destroy/{{id}}.json', 'url': '/saved_searches/destroy/{{id}}.json',
'method': 'GET', 'method': 'GET',
}, },
# List API methods/endpoints. Fairly exhaustive and annoying in general. ;P # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P
'createList': { 'createList': {
'url': '/{{username}}/lists.json', 'url': '/{{username}}/lists.json',
'method': 'POST', 'method': 'POST',
}, },
'updateList': { 'updateList': {
'url': '/{{username}}/lists/{{list_id}}.json', 'url': '/{{username}}/lists/{{list_id}}.json',
'method': 'POST', 'method': 'POST',
}, },
'showLists': { 'showLists': {
'url': '/{{username}}/lists.json', 'url': '/{{username}}/lists.json',
'method': 'GET', 'method': 'GET',
}, },
'getListMemberships': { 'getListMemberships': {
'url': '/{{username}}/lists/memberships.json', 'url': '/{{username}}/lists/memberships.json',
'method': 'GET', 'method': 'GET',
}, },
'getListSubscriptions': { 'getListSubscriptions': {
'url': '/{{username}}/lists/subscriptions.json', 'url': '/{{username}}/lists/subscriptions.json',
'method': 'GET', 'method': 'GET',
}, },
'deleteList': { 'deleteList': {
'url': '/{{username}}/lists/{{list_id}}.json', 'url': '/{{username}}/lists/{{list_id}}.json',
'method': 'DELETE', 'method': 'DELETE',
}, },
'getListTimeline': { 'getListTimeline': {
'url': '/{{username}}/lists/{{list_id}}/statuses.json', 'url': '/{{username}}/lists/{{list_id}}/statuses.json',
'method': 'GET', 'method': 'GET',
}, },
'getSpecificList': { 'getSpecificList': {
'url': '/{{username}}/lists/{{list_id}}/statuses.json', 'url': '/{{username}}/lists/{{list_id}}/statuses.json',
'method': 'GET', 'method': 'GET',
}, },
'addListMember': { 'addListMember': {
'url': '/{{username}}/{{list_id}}/members.json', 'url': '/{{username}}/{{list_id}}/members.json',
'method': 'POST', 'method': 'POST',
}, },
'getListMembers': { 'getListMembers': {
'url': '/{{username}}/{{list_id}}/members.json', 'url': '/{{username}}/{{list_id}}/members.json',
'method': 'GET', 'method': 'GET',
}, },
'deleteListMember': { 'deleteListMember': {
'url': '/{{username}}/{{list_id}}/members.json', 'url': '/{{username}}/{{list_id}}/members.json',
'method': 'DELETE', 'method': 'DELETE',
}, },
'getListSubscribers': { 'getListSubscribers': {
'url': '/{{username}}/{{list_id}}/subscribers.json', 'url': '/{{username}}/{{list_id}}/subscribers.json',
'method': 'GET', 'method': 'GET',
}, },
'subscribeToList': { 'subscribeToList': {
'url': '/{{username}}/{{list_id}}/subscribers.json', 'url': '/{{username}}/{{list_id}}/subscribers.json',
'method': 'POST', 'method': 'POST',
}, },
'unsubscribeFromList': { 'unsubscribeFromList': {
'url': '/{{username}}/{{list_id}}/subscribers.json', 'url': '/{{username}}/{{list_id}}/subscribers.json',
'method': 'DELETE', 'method': 'DELETE',
}, },
# The one-offs # The one-offs
'notificationFollow': { 'notificationFollow': {
'url': '/notifications/follow/follow.json', 'url': '/notifications/follow/follow.json',
'method': 'POST', 'method': 'POST',
}, },
'notificationLeave': { 'notificationLeave': {
'url': '/notifications/leave/leave.json', 'url': '/notifications/leave/leave.json',
'method': 'POST', 'method': 'POST',
}, },
'updateDeliveryService': { 'updateDeliveryService': {
'url': '/account/update_delivery_device.json', 'url': '/account/update_delivery_device.json',
'method': 'POST', 'method': 'POST',
}, },
'reportSpam': { 'reportSpam': {
'url': '/report_spam.json', 'url': '/report_spam.json',
'method': 'POST', 'method': 'POST',
}, },
} }

View file

@ -12,13 +12,13 @@ __author__ = "Ryan McGrath <ryan@venodesigns.net>"
__version__ = "1.4.6" __version__ = "1.4.6"
import urllib import urllib
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:
@ -30,7 +30,6 @@ except ImportError:
# table is a file with a dictionary of every API endpoint that Twython supports. # table is a file with a dictionary of every API endpoint that Twython supports.
from twitter_endpoints import base_url, api_table from twitter_endpoints import base_url, api_table
from urllib2 import HTTPError
# There are some special setups (like, oh, a Django application) where # There are some special setups (like, oh, a Django application) where
# simplejson exists behind the scenes anyway. Past Python 2.6, this should # simplejson exists behind the scenes anyway. Past Python 2.6, this should
@ -124,7 +123,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 +140,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 +159,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 +192,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 +218,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 +252,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.
@ -263,22 +270,6 @@ class Twython(object):
def constructApiURL(base_url, params): def constructApiURL(base_url, params):
return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), urllib.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), urllib.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()])
@staticmethod
def shortenURL(url_to_shorten, shortener="http://is.gd/api.php", query="longurl"):
"""shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl")
Shortens url specified by url_to_shorten.
Parameters:
url_to_shorten - URL to shorten.
shortener - In case you want to use a url shortening service other than is.gd.
"""
try:
content = urllib2.urlopen(shortener + "?" + urllib.urlencode({query: Twython.unicode2utf8(url_to_shorten)})).read()
return content
except HTTPError, e:
raise TwythonError("shortenURL() failed with a %s error code." % e.code)
def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs): def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs):
""" bulkUserLookup(self, ids = None, screen_names = None, version = 1, **kwargs) """ bulkUserLookup(self, ids = None, screen_names = None, version = 1, **kwargs)
@ -294,9 +285,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 +302,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 +333,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 +379,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 +398,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 +439,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 +476,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 +519,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):