From 3f26325ddb0a6cc13a9c7b253207836f8c44a814 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 10 Apr 2012 14:24:50 -0400 Subject: [PATCH 1/5] Updating methods to use internal get/post methods, updating methods using deprecated Twitter endpoints, updating some documentation on methods to start using rst format, plotting demise of deprecated Twython exceptiosn and methods >:) * Updated all methods to use the internal get/post methods * isListMember was using the deprecated *GET :user/:list_id/members/:id* Twitter endpoint * isListMember was also using a deprecated method * Changed documentation on methods, the first line should be what the method does (docstring) * Started to change documentation for methods to use rst (restructed text) -- What PyPi supports as well as Sphinx generator and others instead of Markdown * Planning to get rid of Exceptions - TwythonAPILimit, APILimit, AuthError and Methods - searchTwitter(), searchTwitterGen() --- twython/twython.py | 305 ++++++++++++++++++++++++++------------------- 1 file changed, 176 insertions(+), 129 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index c4b425c..99f4e8f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -34,9 +34,7 @@ from twitter_endpoints import base_url, api_table, twitter_http_status_codes # 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 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. + # If they have the library, chances are they're gonna want to use that. import simplejson except ImportError: try: @@ -61,7 +59,7 @@ class TwythonError(AttributeError): from twython import TwythonError, TwythonAPILimit, TwythonAuthError """ - def __init__(self, msg, error_code=None): + def __init__(self, msg, error_code=None, retry_after=None): self.msg = msg if error_code is not None and error_code in twitter_http_status_codes: @@ -71,12 +69,43 @@ class TwythonError(AttributeError): self.msg) if error_code == 400 or error_code == 420: - raise TwythonAPILimit(self.msg) + raise TwythonRateLimitError(self.msg, retry_after=retry_after) def __str__(self): return repr(self.msg) +class TwythonAuthError(TwythonError): + """ + Raised when you try to access a protected resource and it fails due to some issue with + your authentication. + """ + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return repr(self.msg) + + +class TwythonRateLimitError(TwythonError): + """ + Raised when you've hit a rate limit. retry_wait_seconds is the number of seconds to + wait before trying again. + """ + def __init__(self, msg, error_code, retry_after=None): + retry_after = int(retry_after) + self.msg = '%s (Retry after %s seconds)' % (msg, retry_after) + TwythonError.__init__(self, msg, error_code) + + def __str__(self): + return repr(self.msg) + + +''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' +''' REMOVE THE FOLLOWING IN TWYTHON 2.0 ''' +''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' + + class TwythonAPILimit(TwythonError): """ Raised when you've hit an API limit. Try to avoid these, read the API @@ -105,31 +134,6 @@ class APILimit(TwythonError): return repr(self.msg) -class TwythonRateLimitError(TwythonError): - """ - Raised when you've hit a rate limit. retry_wait_seconds is the number of seconds to - wait before trying again. - """ - def __init__(self, msg, retry_wait_seconds, error_code): - self.retry_wait_seconds = int(retry_wait_seconds) - TwythonError.__init__(self, msg, error_code) - - def __str__(self): - return repr(self.msg) - - -class TwythonAuthError(TwythonError): - """ - Raised when you try to access a protected resource and it fails due to some issue with - your authentication. - """ - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return repr(self.msg) - - class AuthError(TwythonError): """ Raised when you try to access a protected resource and it fails due to some issue with @@ -141,6 +145,9 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) +''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' +''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' + class Twython(object): def __init__(self, twitter_token=None, twitter_secret=None, oauth_token=None, oauth_token_secret=None, \ @@ -267,9 +274,11 @@ class Twython(object): if content.get('error') is not None: error_msg = content['error'] - self._last_call = error_msg + self._last_call['api_error'] = error_msg - raise TwythonError(error_msg, error_code=response.status_code) + raise TwythonError(error_msg, + error_code=response.status_code, + retry_after=response.headers.get('retry-after')) return content @@ -412,77 +421,83 @@ class Twython(object): return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), urllib.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs): - """ bulkUserLookup(self, ids = None, screen_names = None, version = 1, **kwargs) + """ A method to do bulk user lookups against the Twitter API. - A method to do bulk user lookups against the Twitter API. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that - contain their respective data sets. + Documentation: https://dev.twitter.com/docs/api/1/get/users/lookup - Statuses for the users in question will be returned inline if they exist. Requires authentication! + :ids or screen_names: (required) + :param ids: (optional) A list of integers of Twitter User IDs + :param screen_names: (optional) A list of strings of Twitter Screen Names + + :param include_entities: (optional) When set to either true, t or 1, + each tweet will include a node called + "entities,". This node offers a variety of + metadata about the tweet in a discreet structure + + e.g x.bulkUserLookup(screen_names=['ryanmcgrath', 'mikehelmick'], + include_entities=1) """ - if ids: + if ids is None and screen_names is None: + raise TwythonError('Please supply either a list of ids or \ + screen_names for this method.') + + if ids is not None: kwargs['user_id'] = ','.join(map(str, ids)) - if screen_names: + if screen_names is not None: kwargs['screen_name'] = ','.join(screen_names) - lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) - try: - response = self.client.post(lookupURL, headers=self.headers) - return simplejson.loads(response.content.decode('utf-8')) - except RequestException, e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % e.code, e.code) + return self.get('users/lookup', params=kwargs, version=version) def search(self, **kwargs): - """search(search_query, **kwargs) + """ Returns tweets that match a specified query. - Returns tweets that match a specified query. + Documentation: https://dev.twitter.com/ - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + :param q: (required) The query you want to search Twitter for - e.g x.search(q = "jjndf", page = '2') + :param geocode: (optional) Returns tweets by users located within + a given radius of the given latitude/longitude. + The parameter value is specified by + "latitude,longitude,radius", where radius units + must be specified as either "mi" (miles) or + "km" (kilometers). + Example Values: 37.781157,-122.398720,1mi + :param lang: (optional) Restricts tweets to the given language, + given by an ISO 639-1 code. + :param locale: (optional) Specify the language of the query you + are sending. Only ``ja`` is currently effective. + :param page: (optional) The page number (starting at 1) to return + Max ~1500 results + :param result_type: (optional) Default ``mixed`` + mixed: Include both popular and real time + results in the response. + recent: return only the most recent results in + the response + popular: return only the most popular results + in the response. + + e.g x.search(q='jjndf', page='2') """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) - try: - response = self.client.get(searchURL, headers=self.headers) - - if response.status_code == 420: - retry_wait_seconds = response.headers.get('retry-after') - raise TwythonRateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." % - retry_wait_seconds, - retry_wait_seconds, - response.status_code) - - return simplejson.loads(response.content.decode('utf-8')) - except RequestException, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % e.code, e.code) - - def searchTwitter(self, **kwargs): - """use search() ,this is a fall back method to support searchTwitter() - """ - return self.search(**kwargs) + return self.get('http://search.twitter.com/search.json', params=kwargs) def searchGen(self, search_query, **kwargs): - """searchGen(search_query, **kwargs) + """ Returns a generator of tweets that match a specified query. - Returns a generator of tweets that match a specified query. + Documentation: https://dev.twitter.com/doc/get/search. - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + See Twython.search() for acceptable parameters - e.g x.searchGen("python", page="2") or - x.searchGen(search_query = "python", page = "2") + e.g search = x.searchGen('python') + for result in search: + print result """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) - try: - response = self.client.get(searchURL, headers=self.headers) - data = simplejson.loads(response.content.decode('utf-8')) - except RequestException, e: - raise TwythonError("searchGen() failed with a %s error code." % e.code, e.code) + kwargs['q'] = search_query + content = self.get('http://search.twitter.com/search.json', params=kwargs) - if not data['results']: + if not content['results']: raise StopIteration - for tweet in data['results']: + for tweet in content['results']: yield tweet if 'page' not in kwargs: @@ -493,58 +508,70 @@ class Twython(object): kwargs['page'] += 1 kwargs['page'] = str(kwargs['page']) except TypeError: - raise TwythonError("searchGen() exited because page takes str") - except e: - raise TwythonError("searchGen() failed with %s error code" % \ - e.code, e.code) + raise TwythonError("searchGen() exited because page takes type str") for tweet in self.searchGen(search_query, **kwargs): yield tweet - def searchTwitterGen(self, search_query, **kwargs): - """use searchGen(), this is a fallback method to support - searchTwitterGen()""" - return self.searchGen(search_query, **kwargs) + def isListMember(self, list_id, id, username, version=1, **kwargs): + """ Check if a specified user (username) is a member of the list in question (list_id). - def isListMember(self, list_id, id, username, version=1): - """ isListMember(self, list_id, id, version) + Documentation: https://dev.twitter.com/docs/api/1/get/lists/members/show - Check if a specified user (id) is a member of the list in question (list_id). + **Note: This method may not work for private/protected lists, + unless you're authenticated and have access to those lists. - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + :param list_id: (required) The numerical id of the list. + :param username: (required) The screen name for whom to return results for + :param version: (optional) Currently, default (only effective value) is 1 + :param id: (deprecated) This value is no longer needed. - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - username - User who owns the list you're checking against (username) - 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. + e.g. + **Note: currently TwythonError is not descriptive enough + to handle specific errors, those errors will be + included in the library soon enough + try: + x.isListMember(53131724, None, 'ryanmcgrath') + except TwythonError: + print 'User is not a member' """ - try: - 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(response.content.decode('utf-8')) - except RequestException, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + kwargs['list_id'] = list_id + kwargs['screen_name'] = username + return self.get('lists/members/show', params=kwargs) - def isListSubscriber(self, username, list_id, id, version=1): - """ isListSubscriber(self, list_id, id, version) + def isListSubscriber(self, username, list_id, id, version=1, **kwargs): + """ Check if a specified user (username) is a subscriber of the list in question (list_id). - Check if a specified user (id) is a subscriber of the list in question (list_id). + Documentation: https://dev.twitter.com/docs/api/1/get/lists/subscribers/show - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + **Note: This method may not work for private/protected lists, + unless you're authenticated and have access to those lists. - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - username - Required. The username of the owner of the list that you're seeing if someone is subscribed to. - 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. + :param list_id: (required) The numerical id of the list. + :param username: (required) The screen name for whom to return results for + :param version: (optional) Currently, default (only effective value) is 1 + :param id: (deprecated) This value is no longer needed. + + e.g. + **Note: currently TwythonError is not descriptive enough + to handle specific errors, those errors will be + included in the library soon enough + try: + x.isListSubscriber('ryanmcgrath', 53131724, None) + except TwythonError: + print 'User is not a member' + + The above throws a TwythonError, the following returns data about + the user since they follow the specific list: + + x.isListSubscriber('icelsius', 53131724, None) """ - try: - 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(response.content.decode('utf-8')) - except RequestException, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + kwargs['list_id'] = list_id + kwargs['screen_name'] = username + return self.get('lists/subscribers/show', params=kwargs) - # 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. def updateProfileBackgroundImage(self, file_, tile=True, version=1): """ updateProfileBackgroundImage(filename, tile=True) @@ -555,9 +582,10 @@ class Twython(object): tile - Optional (defaults to True). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. 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. """ - return self._media_update('http://api.twitter.com/%d/account/update_profile_background_image.json' % version, { - 'image': (file_, open(file_, 'rb')) - }, params={'tile': tile}) + url = 'http://api.twitter.com/%d/account/update_profile_background_image.json' % version + return self._media_update(url, + {'image': (file_, open(file_, 'rb'))}, + params={'tile': tile}) def updateProfileImage(self, file_, version=1): """ updateProfileImage(filename) @@ -568,9 +596,9 @@ class Twython(object): image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down. 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. """ - return self._media_update('http://api.twitter.com/%d/account/update_profile_image.json' % version, { - 'image': (file_, open(file_, 'rb')) - }) + url = 'http://api.twitter.com/%d/account/update_profile_image.json' % version + return self._media_update(url, + {'image': (file_, open(file_, 'rb'))}) # statuses/update_with_media def updateStatusWithMedia(self, file_, version=1, **params): @@ -582,9 +610,10 @@ class Twython(object): image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down. 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. """ - return self._media_update('https://upload.twitter.com/%d/statuses/update_with_media.json' % version, { - 'media': (file_, open(file_, 'rb')) - }, **params) + url = 'https://upload.twitter.com/%d/statuses/update_with_media.json' % version + return self._media_update(url, + {'media': (file_, open(file_, 'rb'))}, + **params) def _media_update(self, url, file_, params=None): params = params or {} @@ -662,7 +691,9 @@ class Twython(object): size - Optional. Image size. Valid options include 'normal', 'mini' and 'bigger'. Defaults to 'normal' if not given. 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. """ - url = "http://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) + url = "http://api.twitter.com/%s/users/profile_image/%s.json" % \ + (version, username) + if size: url = self.constructApiURL(url, {'size': size}) @@ -731,3 +762,19 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) + + ''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' + ''' REMOVE THE FOLLOWING IN TWYTHON 2.0 ''' + ''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' + + def searchTwitter(self, **kwargs): + """use search() ,this is a fall back method to support searchTwitter() + """ + return self.search(**kwargs) + + def searchTwitterGen(self, search_query, **kwargs): + """use searchGen(), this is a fallback method to support + searchTwitterGen()""" + return self.searchGen(search_query, **kwargs) + + ''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' From a42570b68546973c4186c5eb898f635f04936f52 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Apr 2012 11:44:27 -0400 Subject: [PATCH 2/5] Trying to make this merge-able. * We don't need RequestException anymore. * I changed TwythonError to raise TwythonRateLimitError instead of TwythonAPIError since TwythonRateLimitError is more verbose and in the belief we should deprecate TwythonAPILimit and ultimately remove it in 2.0 * And I updated the version to 1.7.0 -- I feel like development as far as versioning seems like it's going fast, but versioning is versioning and I'm following Twitter's rhythm of versioning .., minor changing when minor features or significant fixes have been added. In this case, TwythonRateLimitError should start being caught in place of TwythonAPILimit --- setup.py | 2 +- twython/twython.py | 45 +++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index bc36455..acbbe3e 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.6.0' +__version__ = '1.7.0' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 99f4e8f..b08f376 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,14 +9,13 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.6.0" +__version__ = "1.7.0" import urllib import re import time import requests -from requests.exceptions import RequestException from oauth_hook import OAuthHook import oauth2 as oauth @@ -61,6 +60,7 @@ class TwythonError(AttributeError): """ def __init__(self, msg, error_code=None, retry_after=None): self.msg = msg + self.error_code = error_code if error_code is not None and error_code in twitter_http_status_codes: self.msg = '%s: %s -- %s' % \ @@ -69,28 +69,29 @@ class TwythonError(AttributeError): self.msg) if error_code == 400 or error_code == 420: - raise TwythonRateLimitError(self.msg, retry_after=retry_after) + raise TwythonRateLimitError(self.msg, + error_code, + retry_after=retry_after) def __str__(self): return repr(self.msg) class TwythonAuthError(TwythonError): + """ Raised when you try to access a protected resource and it fails due to + some issue with your authentication. """ - Raised when you try to access a protected resource and it fails due to some issue with - your authentication. - """ - def __init__(self, msg): + def __init__(self, msg, error_code=None): self.msg = msg + self.error_code = error_code def __str__(self): return repr(self.msg) class TwythonRateLimitError(TwythonError): - """ - Raised when you've hit a rate limit. retry_wait_seconds is the number of seconds to - wait before trying again. + """ Raised when you've hit a rate limit. + retry_wait_seconds is the number of seconds to wait before trying again. """ def __init__(self, msg, error_code, retry_after=None): retry_after = int(retry_after) @@ -107,40 +108,40 @@ class TwythonRateLimitError(TwythonError): class TwythonAPILimit(TwythonError): - """ - Raised when you've hit an API limit. Try to avoid these, read the API + """ Raised when you've hit an API limit. Try to avoid these, read the API docs if you're running into issues here, Twython does not concern itself with this matter beyond telling you that you've done goofed. """ - def __init__(self, msg): - self.msg = msg + def __init__(self, msg, error_code=None): + self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch on TwythonRateLimitLimit instead!' % msg + self.error_code = error_code def __str__(self): return repr(self.msg) class APILimit(TwythonError): - """ - Raised when you've hit an API limit. Try to avoid these, read the API + """ Raised when you've hit an API limit. Try to avoid these, read the API docs if you're running into issues here, Twython does not concern itself with this matter beyond telling you that you've done goofed. DEPRECATED, import and catch TwythonAPILimit instead. """ - def __init__(self, msg): - self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch on TwythonAPILimit instead!' % msg + def __init__(self, msg, error_code=None): + self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch on TwythonRateLimitLimit instead!' % msg + self.error_code = error_code def __str__(self): return repr(self.msg) class AuthError(TwythonError): - """ - Raised when you try to access a protected resource and it fails due to some issue with + """ Raised when you try to access a protected resource and it fails due to some issue with your authentication. """ - def __init__(self, msg): + def __init__(self, msg, error_code=None): self.msg = '%s\n Notice: AuthError is deprecated and soon to be removed, catch on TwythonAuthError instead!' % msg + self.error_code = error_code def __str__(self): return repr(self.msg) @@ -414,7 +415,7 @@ class Twython(object): if request.status_code in [301, 201, 200]: return request.text else: - raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code) + raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code , request.status_code ) @staticmethod def constructApiURL(base_url, params): From ac18837ed66e5baeb862e4e9e877fe18686de10b Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 19 Apr 2012 19:31:07 -0400 Subject: [PATCH 3/5] Should be good for auto-merge * Fixed a typo - 'startwith' replaced with 'startswith' * Got rid of constructApiUrl, it's no longer needed, self.request() does it internally * A bunch of odds and ends to get this to auto-merge finally?! :D --- README.markdown | 1 + README.txt | 10 +++++----- twython/twython.py | 41 +++++++++++++++++++---------------------- twython3k/twython.py | 22 +++++++++++----------- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/README.markdown b/README.markdown index 629ca67..ccb9f04 100644 --- a/README.markdown +++ b/README.markdown @@ -147,3 +147,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - **[Remy DeCausemaker (decause)](https://github.com/decause)**, PEP-8 contributions. - **[mckellister](https://github.com/mckellister)**, Fixes to `Exception`s raised by Twython (Rate Limits, etc). - **[tatz_tsuchiya](http://d.hatena.ne.jp/tatz_tsuchiya/20120115/1326623451)**, Fix for `lambda` scoping in key injection phase. +- **[Voulnet (Mohammed ALDOUB)](https://github.com/Voulnet)**, Fixes for `http`/`https` access endpoints diff --git a/README.txt b/README.txt index b281c3f..ccb9f04 100644 --- a/README.txt +++ b/README.txt @@ -57,10 +57,9 @@ Streaming API ---------------------------------------------------------------------------------------------------- Twython, as of v1.5.0, now includes an experimental **[Twitter Streaming API](https://dev.twitter.com/docs/streaming-api)** handler. Usage is as follows; it's designed to be open-ended enough that you can adapt it to higher-level (read: Twitter must give you access) -streams. This also exists in large part (read: pretty much in full) thanks to the excellent **[python-requests](http://docs.python-requests.org/en/latest/) library by +streams. This also exists in large part (read: pretty much in full) thanks to the excellent **[python-requests](http://docs.python-requests.org/en/latest/)** library by Kenneth Reitz. - -**Example Usage:** + ``` python import json from twython import Twython @@ -77,7 +76,7 @@ Twython.stream({ 'track': 'python' }, on_results) ``` - + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- @@ -147,4 +146,5 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - **[Mesar Hameed (mhameed)](https://github.com/mhameed)**, Commit to swap `__getattr__` trick for a more debuggable solution. - **[Remy DeCausemaker (decause)](https://github.com/decause)**, PEP-8 contributions. - **[mckellister](https://github.com/mckellister)**, Fixes to `Exception`s raised by Twython (Rate Limits, etc). -- **[tatz_tsuchiya](http://d.hatena.ne.jp/tatz_tsuchiya/20120115/1326623451), Fix for `lambda` scoping in key injection phase. +- **[tatz_tsuchiya](http://d.hatena.ne.jp/tatz_tsuchiya/20120115/1326623451)**, Fix for `lambda` scoping in key injection phase. +- **[Voulnet (Mohammed ALDOUB)](https://github.com/Voulnet)**, Fixes for `http`/`https` access endpoints diff --git a/twython/twython.py b/twython/twython.py index b08f376..98f7188 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -127,6 +127,7 @@ class APILimit(TwythonError): DEPRECATED, import and catch TwythonAPILimit instead. """ + def __init__(self, msg, error_code=None): self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch on TwythonRateLimitLimit instead!' % msg self.error_code = error_code @@ -139,6 +140,7 @@ class AuthError(TwythonError): """ Raised when you try to access a protected resource and it fails due to some issue with your authentication. """ + def __init__(self, msg, error_code=None): self.msg = '%s\n Notice: AuthError is deprecated and soon to be removed, catch on TwythonAuthError instead!' % msg self.error_code = error_code @@ -173,11 +175,11 @@ class Twython(object): OAuthHook.consumer_secret = twitter_secret # Needed for hitting that there API. - self.request_token_url = 'http://twitter.com/oauth/request_token' - 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/%s/' + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorize_url = 'https://twitter.com/oauth/authorize' + self.authenticate_url = 'https://twitter.com/oauth/authenticate' + self.api_url = 'https://api.twitter.com/%s/' self.twitter_token = twitter_token self.twitter_secret = twitter_secret @@ -232,8 +234,7 @@ class Twython(object): return content def _request(self, url, method='GET', params=None, api_call=None): - ''' - Internal response generator, not sense in repeating the same + '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' myargs = {} @@ -295,7 +296,7 @@ class Twython(object): # In case they want to pass a full Twitter URL # i.e. http://search.twitter.com/ - if endpoint.startswith('http://'): + if endpoint.startswith('http://') or endpoint.startswith('https://'): url = endpoint else: url = '%s%s.json' % (self.api_url % version, endpoint) @@ -415,11 +416,7 @@ class Twython(object): if request.status_code in [301, 201, 200]: return request.text else: - raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code , request.status_code ) - - @staticmethod - 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()]) + raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code, request.status_code) def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs): """ A method to do bulk user lookups against the Twitter API. @@ -479,6 +476,9 @@ class Twython(object): e.g x.search(q='jjndf', page='2') """ + if 'q' in kwargs: + kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(kwargs['q'])) + return self.get('http://search.twitter.com/search.json', params=kwargs) def searchGen(self, search_query, **kwargs): @@ -492,7 +492,7 @@ class Twython(object): for result in search: print result """ - kwargs['q'] = search_query + kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(search_query)) content = self.get('http://search.twitter.com/search.json', params=kwargs) if not content['results']: @@ -682,7 +682,7 @@ class Twython(object): req = requests.post(url, data=params, files=file_, headers=self.headers) return req.content - def getProfileImageUrl(self, username, size=None, version=1): + def getProfileImageUrl(self, username, size='normal', version=1): """ getProfileImageUrl(username) Gets the URL for the user's profile image. @@ -692,20 +692,17 @@ class Twython(object): size - Optional. Image size. Valid options include 'normal', 'mini' and 'bigger'. Defaults to 'normal' if not given. 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. """ - url = "http://api.twitter.com/%s/users/profile_image/%s.json" % \ - (version, username) - if size: - url = self.constructApiURL(url, {'size': size}) + endpoint = 'users/profile_image/%s' % username + url = self.api_url % version + endpoint + '?' + urllib.urlencode({'size': size}) - #client.follow_redirects = False response = self.client.get(url, allow_redirects=False) image_url = response.headers.get('location') if response.status_code in (301, 302, 303, 307) and image_url is not None: return image_url - - raise TwythonError("getProfileImageUrl() failed with a %d error code." % response.status_code, response.status_code) + else: + raise TwythonError('getProfileImageUrl() threw an error.', error_code=response.status_code) @staticmethod def stream(data, callback): diff --git a/twython3k/twython.py b/twython3k/twython.py index 6296e82..c8f03b9 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -144,10 +144,10 @@ class Twython(object): ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ # Needed for hitting that there API. - self.request_token_url = 'http://twitter.com/oauth/request_token' - 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.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorize_url = 'https://twitter.com/oauth/authorize' + self.authenticate_url = 'https://twitter.com/oauth/authenticate' self.twitter_token = twitter_token self.twitter_secret = twitter_secret self.oauth_token = oauth_token @@ -297,7 +297,7 @@ class Twython(object): if screen_names: kwargs['screen_name'] = ','.join(screen_names) - lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) + lookupURL = Twython.constructApiURL("https://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: resp, content = self.client.request(lookupURL, "POST", headers = self.headers) return simplejson.loads(content.decode('utf-8')) @@ -314,7 +314,7 @@ class Twython(object): e.g x.search(q = "jjndf", page = '2') """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + searchURL = Twython.constructApiURL("https://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) return simplejson.loads(content.decode('utf-8')) @@ -337,7 +337,7 @@ class Twython(object): e.g x.searchGen("python", page="2") or x.searchGen(search_query = "python", page = "2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) + searchURL = Twython.constructApiURL("https://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) data = simplejson.loads(content.decode('utf-8')) @@ -385,7 +385,7 @@ 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. """ try: - resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id)), headers = self.headers) + resp, content = self.client.request("https://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id)), headers = self.headers) return simplejson.loads(content.decode('utf-8')) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -404,7 +404,7 @@ 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. """ try: - resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)), headers = self.headers) + resp, content = self.client.request("https://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)), headers = self.headers) return simplejson.loads(content.decode('utf-8')) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -426,7 +426,7 @@ class Twython(object): fields = [] content_type, body = Twython.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) + r = urllib.request.Request("https://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return urllib.request.urlopen(r).read() except HTTPError as e: raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) @@ -445,7 +445,7 @@ class Twython(object): fields = [] content_type, body = Twython.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) + r = urllib.request.Request("https://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return urllib.request.urlopen(r).read() except HTTPError as e: raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) From 0ee5e5877e6ac7560601647234e92351be7dd810 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 8 May 2012 12:14:45 -0400 Subject: [PATCH 4/5] Cleaning up endpoints per Twitter Spring 2012 deprecations https://dev.twitter.com/docs/deprecations/spring-2012 --- twython/twitter_endpoints.py | 54 ++++++++++++++---------------------- twython/twython.py | 22 +++++++-------- 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 85da63a..7b1aae7 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -32,10 +32,6 @@ api_table = { }, # Timeline methods - 'getPublicTimeline': { - 'url': '/statuses/public_timeline.json', - 'method': 'GET', - }, 'getHomeTimeline': { 'url': '/statuses/home_timeline.json', 'method': 'GET', @@ -44,24 +40,12 @@ api_table = { 'url': '/statuses/user_timeline.json', 'method': 'GET', }, - 'getFriendsTimeline': { - 'url': '/statuses/friends_timeline.json', - 'method': 'GET', - }, # Interfacing with friends/followers 'getUserMentions': { 'url': '/statuses/mentions.json', 'method': 'GET', }, - 'getFriendsStatus': { - 'url': '/statuses/friends.json', - 'method': 'GET', - }, - 'getFollowersStatus': { - 'url': '/statuses/followers.json', - 'method': 'GET', - }, 'createFriendship': { 'url': '/friendships/create.json', 'method': 'POST', @@ -126,7 +110,7 @@ api_table = { # Status methods - showing, updating, destroying, etc. 'showStatus': { - 'url': '/statuses/show/{{id}}.json', + 'url': '/statuses/show.json', 'method': 'GET', }, 'updateStatus': { @@ -254,60 +238,64 @@ api_table = { # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P 'createList': { - 'url': '/{{username}}/lists.json', + 'url': '/lists/create.json', 'method': 'POST', }, 'updateList': { - 'url': '/{{username}}/lists/{{list_id}}.json', + 'url': '/lists/update.json', 'method': 'POST', }, 'showLists': { - 'url': '/{{username}}/lists.json', + 'url': '/lists.json', 'method': 'GET', }, 'getListMemberships': { - 'url': '/{{username}}/lists/memberships.json', + 'url': '/lists/memberships.json', 'method': 'GET', }, 'getListSubscriptions': { - 'url': '/{{username}}/lists/subscriptions.json', + 'url': '/lists/subscriptions.json', 'method': 'GET', }, 'deleteList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'DELETE', + 'url': '/lists/destroy.json', + 'method': 'POST', }, 'getListTimeline': { 'url': '/{{username}}/lists/{{list_id}}/statuses.json', 'method': 'GET', }, 'getSpecificList': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'url': '/lists/show.json', 'method': 'GET', }, + 'getListStatuses': { + 'url': '/lists/statuses.json', + 'method': 'GET' + }, 'addListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', + 'url': '/lists/members/create.json', 'method': 'POST', }, 'getListMembers': { - 'url': '/{{username}}/{{list_id}}/members.json', + 'url': '/lists/members.json', 'method': 'GET', }, 'deleteListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'DELETE', + 'url': '/lists/members/destroy.json', + 'method': 'POST', }, 'getListSubscribers': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'url': '/lists/subscribers.json', 'method': 'GET', }, 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'url': '/lists/subscribers/create.json', 'method': 'POST', }, 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'DELETE', + 'url': '/lists/subscribers/destroy.json', + 'method': 'POST', }, # The one-offs diff --git a/twython/twython.py b/twython/twython.py index 98f7188..4d308d5 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -68,7 +68,7 @@ class TwythonError(AttributeError): twitter_http_status_codes[error_code][1], self.msg) - if error_code == 400 or error_code == 420: + if error_code == 420: raise TwythonRateLimitError(self.msg, error_code, retry_after=retry_after) @@ -94,9 +94,9 @@ class TwythonRateLimitError(TwythonError): retry_wait_seconds is the number of seconds to wait before trying again. """ def __init__(self, msg, error_code, retry_after=None): - retry_after = int(retry_after) - self.msg = '%s (Retry after %s seconds)' % (msg, retry_after) - TwythonError.__init__(self, msg, error_code) + if isinstance(retry_after, int): + retry_after = int(retry_after) + self.msg = '%s (Retry after %s seconds)' % (msg, retry_after) def __str__(self): return repr(self.msg) @@ -175,11 +175,11 @@ class Twython(object): OAuthHook.consumer_secret = twitter_secret # Needed for hitting that there API. - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorize_url = 'https://twitter.com/oauth/authorize' - self.authenticate_url = 'https://twitter.com/oauth/authenticate' - self.api_url = 'https://api.twitter.com/%s/' + self.api_url = 'https://api.twitter.com/%s' + self.request_token_url = self.api_url % 'oauth/request_token' + self.access_token_url = self.api_url % 'oauth/access_token' + self.authorize_url = self.api_url % 'oauth/authorize' + self.authenticate_url = self.api_url % 'oauth/authenticate' self.twitter_token = twitter_token self.twitter_secret = twitter_secret @@ -299,7 +299,7 @@ class Twython(object): if endpoint.startswith('http://') or endpoint.startswith('https://'): url = endpoint else: - url = '%s%s.json' % (self.api_url % version, endpoint) + url = '%s/%s.json' % (self.api_url % version, endpoint) content = self._request(url, method=method, params=params, api_call=url) @@ -694,7 +694,7 @@ class Twython(object): """ endpoint = 'users/profile_image/%s' % username - url = self.api_url % version + endpoint + '?' + urllib.urlencode({'size': size}) + url = self.api_url % version + '/' + endpoint + '?' + urllib.urlencode({'size': size}) response = self.client.get(url, allow_redirects=False) image_url = response.headers.get('location') From 9fa9b525a1492570ba72d9864a59ce45cc9bf507 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 8 May 2012 12:29:57 -0400 Subject: [PATCH 5/5] Note when upgrading to 1.7.0 --- README.markdown | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index ccb9f04..8dc235e 100644 --- a/README.markdown +++ b/README.markdown @@ -40,6 +40,10 @@ Installing Twython is fairly easy. You can... cd twython sudo python setup.py install +Please note: +----------------------------------------------------------------------------------------------------- +As of Twython 1.7.0, we have change routes for functions to abide by the Twitter Spring 2012 clean up (https://dev.twitter.com/docs/deprecations/spring-2012). Please make changes to your code accordingly. + Example Use ----------------------------------------------------------------------------------------------------- ``` python @@ -75,8 +79,7 @@ Twython.stream({ 'password': 'your_password', 'track': 'python' }, on_results) -``` - +``` A note about the development of Twython (specifically, 1.3) ----------------------------------------------------------------------------------------------------