From 03f3a22480fcf275c8f85a785257e62c624bd75a Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 18:12:07 -0400 Subject: [PATCH 1/6] Dynamic Request Methods Just in case Twitter releases something in their API and a developer wants to implement it on their app, but we haven't gotten around to putting it in Twython yet. :) --- setup.py | 2 +- twython/twython.py | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 98c6925..bc36455 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.5.2' +__version__ = '1.6.0' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 4bd2fa7..a8a2660 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.5.2" +__version__ = "1.6.0" import urllib import re @@ -175,6 +175,7 @@ class Twython(object): self.access_token_url = 'http://twitter.com/oauth/access_token' self.authorize_url = 'http://twitter.com/oauth/authorize' self.authenticate_url = 'http://twitter.com/oauth/authenticate' + self.api_url = 'http://api.twitter.com/1/' self.twitter_token = twitter_token self.twitter_secret = twitter_secret @@ -221,17 +222,56 @@ class Twython(object): if not method in ('get', 'post', 'delete'): raise TwythonError('Method must be of GET, POST or DELETE') + response = self._request(url, method=method, params=kwargs) + + return simplejson.loads(response.content.decode('utf-8')) + + def _request(self, url, method='GET', params=None): + ''' + Internal response generator, not sense in repeating the same + code twice, right? ;) + ''' myargs = {} + method = method.lower() if method == 'get': - url = '%s?%s' % (url, urllib.urlencode(kwargs)) + url = '%s?%s' % (url, urllib.urlencode(params)) else: - myargs = kwargs + myargs = params func = getattr(self.client, method) response = func(url, data=myargs) + return response + + ''' + # Dynamic Request Methods + Just in case Twitter releases something in their API + and a developer wants to implement it on their app, but + we haven't gotten around to putting it in Twython yet. :) + ''' + + def request(self, endpoint, method='GET', params=None): + params = params or {} + url = '%s%s.json' % (self.api_url, endpoint) + + response = self._request(url, method=method, params=params) + return simplejson.loads(response.content.decode('utf-8')) + def get(self, endpoint, params=None): + params = params or {} + return self.request(endpoint, params=params) + + def post(self, endpoint, params=None): + params = params or {} + return self.request(endpoint, 'POST', params=params) + + def delete(self, endpoint, params=None): + params = params or {} + return self.request(endpoint, 'DELETE', params=params) + + # End Dynamic Request Methods + def get_authentication_tokens(self): """ get_auth_url(self) From cf38c7c3de42b5bd7b2a3e3eb348dc8dae2fa234 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 20:16:30 -0400 Subject: [PATCH 2/6] POSTing works again, somehow it broke... :/ --- twython/twython.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index a8a2660..a73c743 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -194,7 +194,7 @@ class Twython(object): self.client = requests.session(hooks={'pre_request': OAuthHook()}) if self.oauth_token is not None and self.oauth_secret is not None: - self.oauth_hook = OAuthHook(self.oauth_token, self.oauth_secret) + self.oauth_hook = OAuthHook(self.oauth_token, self.oauth_secret, header_auth=True) 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. @@ -678,3 +678,18 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) + + +if __name__ == '__main__': + t_token = 'tWcBBbw1RPw1xqByfmuacA' + t_secret = '8OUkoA2aXr2gTMI2gx7oDgw46UuG6ez8wIqV980m4' + f_oauth_secret = '66XY3rAamLbwWC0KNwUG9QxdsnfPNZBji2UKNhVh4' + f_oauth_token = '29251354-UCmNcr9y3lflHqN9Gvwc7A0JlH0H4FOhO0JgJxS7t' + + t = Twython(twitter_token=t_token, + twitter_secret=t_secret, + oauth_token=f_oauth_token, + oauth_token_secret=f_oauth_secret) + + user = t.post('statuses/update', params={'status': 'Testing Twython Library'}) + print user From 0fcd4202c8f4aecfddc7c6f9eff3f0ede083bde0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 20:18:12 -0400 Subject: [PATCH 3/6] Whoops.. didn't mean to give those out. Haha. --- twython/twython.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index a73c743..7e4728c 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -678,18 +678,3 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) - - -if __name__ == '__main__': - t_token = 'tWcBBbw1RPw1xqByfmuacA' - t_secret = '8OUkoA2aXr2gTMI2gx7oDgw46UuG6ez8wIqV980m4' - f_oauth_secret = '66XY3rAamLbwWC0KNwUG9QxdsnfPNZBji2UKNhVh4' - f_oauth_token = '29251354-UCmNcr9y3lflHqN9Gvwc7A0JlH0H4FOhO0JgJxS7t' - - t = Twython(twitter_token=t_token, - twitter_secret=t_secret, - oauth_token=f_oauth_token, - oauth_token_secret=f_oauth_secret) - - user = t.post('statuses/update', params={'status': 'Testing Twython Library'}) - print user From 703012ef2989e5c2d390e503a837d448b0812480 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 11:44:30 -0400 Subject: [PATCH 4/6] Use simplejson if they have it first, allow for version passing in generic requests, catch json decoding errors and status code errors * Changed the importing order for simplejson, if they have the library installed, chances are they're going to want to use that over Python json, json is slower than simplejson * Version passing is now avaliable * Catching json decode errors (ValueError) and Twitter Errors on `_request` method and returning content rather than the response object. --- twython/twython.py | 63 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 7e4728c..155759b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -35,12 +35,14 @@ from twitter_endpoints import base_url, api_table # simplejson exists behind the scenes anyway. Past Python 2.6, this should # never really cause any problems to begin with. try: - # Python 2.6 and up - import json as simplejson + # Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with) + # If they have simplejson, we should try and load that first, + # if they have the library, chances are they're gonna want to use that. + import simplejson except ImportError: try: - # Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with) - import simplejson + # Python 2.6 and up + import json as simplejson except ImportError: try: # This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there. @@ -175,7 +177,7 @@ class Twython(object): self.access_token_url = 'http://twitter.com/oauth/access_token' self.authorize_url = 'http://twitter.com/oauth/authorize' self.authenticate_url = 'http://twitter.com/oauth/authenticate' - self.api_url = 'http://api.twitter.com/1/' + self.api_url = 'http://api.twitter.com/%s/' self.twitter_token = twitter_token self.twitter_secret = twitter_secret @@ -222,9 +224,9 @@ class Twython(object): if not method in ('get', 'post', 'delete'): raise TwythonError('Method must be of GET, POST or DELETE') - response = self._request(url, method=method, params=kwargs) + content = self._request(url, method=method, params=kwargs) - return simplejson.loads(response.content.decode('utf-8')) + return content def _request(self, url, method='GET', params=None): ''' @@ -233,15 +235,34 @@ class Twython(object): ''' myargs = {} method = method.lower() + if method == 'get': url = '%s?%s' % (url, urllib.urlencode(params)) else: myargs = params + print url func = getattr(self.client, method) response = func(url, data=myargs) - return response + # Python 2.6 `json` will throw a ValueError if it + # can't load the string as valid JSON, + # `simplejson` will throw simplejson.decoder.JSONDecodeError + # But excepting just ValueError will work with both. o.O + try: + content = simplejson.loads(response.content.decode('utf-8')) + except ValueError: + raise TwythonError('Response was not valid JSON, unable to decode.') + + if response.status_code > 302: + # Just incase there is no error message, let's set a default + error_msg = 'An error occurred processing your request.' + if content.get('error') is not None: + error_msg = content['error'] + + raise TwythonError(error_msg, error_code=response.status_code) + + return content ''' # Dynamic Request Methods @@ -250,25 +271,31 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None): + def request(self, endpoint, method='GET', params=None, version=1): params = params or {} - url = '%s%s.json' % (self.api_url, endpoint) - response = self._request(url, method=method, params=params) + # In case they want to pass a full Twitter URL + # i.e. http://search.twitter.com/ + if endpoint.startswith('http://'): + url = endpoint + else: + url = '%s%s.json' % (self.api_url % version, endpoint) - return simplejson.loads(response.content.decode('utf-8')) + content = self._request(url, method=method, params=params) - def get(self, endpoint, params=None): + return content + + def get(self, endpoint, params=None, version=1): params = params or {} - return self.request(endpoint, params=params) + return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None): + def post(self, endpoint, params=None, version=1): params = params or {} - return self.request(endpoint, 'POST', params=params) + return self.request(endpoint, 'POST', params=params, version=version) - def delete(self, endpoint, params=None): + def delete(self, endpoint, params=None, version=1): params = params or {} - return self.request(endpoint, 'DELETE', params=params) + return self.request(endpoint, 'DELETE', params=params, version=version) # End Dynamic Request Methods From 7205aa402a96e4248aaef0d09af6b8b5d4f76cd2 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 14:19:46 -0400 Subject: [PATCH 5/6] Get rid of print --- twython/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 155759b..d3afb4a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -240,7 +240,6 @@ class Twython(object): url = '%s?%s' % (url, urllib.urlencode(params)) else: myargs = params - print url func = getattr(self.client, method) response = func(url, data=myargs) From 813626a9add1b166a760c73ecbf3ef3bb44e5f65 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 9 Apr 2012 10:59:13 -0400 Subject: [PATCH 6/6] Maybe the twitter_http_status_codes were a good idea. :P I still think it's weird to have them, but I'm not against giving the user more information. I put back in the twitter_http_status_codes variable, but I changed where the logic was being handled, instead of it happening the in _request, it will be asserted in Twython error if an error_code is passed AND the error_code is in twitter_http_status_codes --- twython/twitter_endpoints.py | 15 +++++++++++++++ twython/twython.py | 9 ++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index cf4690b..85da63a 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -328,3 +328,18 @@ api_table = { 'method': 'POST', }, } + +# from https://dev.twitter.com/docs/error-codes-responses +twitter_http_status_codes = { + 200: ('OK', 'Success!'), + 304: ('Not Modified', 'There was no new data to return.'), + 400: ('Bad Request', 'The request was invalid. An accompanying error message will explain why. This is the status code will be returned during rate limiting.'), + 401: ('Unauthorized', 'Authentication credentials were missing or incorrect.'), + 403: ('Forbidden', 'The request is understood, but it has been refused. An accompanying error message will explain why. This code is used when requests are being denied due to update limits.'), + 404: ('Not Found', 'The URI requested is invalid or the resource requested, such as a user, does not exists.'), + 406: ('Not Acceptable', 'Returned by the Search API when an invalid format is specified in the request.'), + 420: ('Enhance Your Calm', 'Returned by the Search and Trends API when you are being rate limited.'), + 500: ('Internal Server Error', 'Something is broken. Please post to the group so the Twitter team can investigate.'), + 502: ('Bad Gateway', 'Twitter is down or being upgraded.'), + 503: ('Service Unavailable', 'The Twitter servers are up, but overloaded with requests. Try again later.'), +} diff --git a/twython/twython.py b/twython/twython.py index 1e3a1b4..c4b425c 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -27,7 +27,7 @@ except ImportError: # Twython maps keyword based arguments to Twitter API endpoints. The endpoints # 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, twitter_http_status_codes # There are some special setups (like, oh, a Django application) where @@ -64,8 +64,11 @@ class TwythonError(AttributeError): def __init__(self, msg, error_code=None): self.msg = msg - if error_code is not None: - self.msg = self.msg + ' Please see https://dev.twitter.com/docs/error-codes-responses for additional information.' + if error_code is not None and error_code in twitter_http_status_codes: + self.msg = '%s: %s -- %s' % \ + (twitter_http_status_codes[error_code][0], + twitter_http_status_codes[error_code][1], + self.msg) if error_code == 400 or error_code == 420: raise TwythonAPILimit(self.msg)