From 624de61874458db6ba97434d2384977b74c6cc2b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 4 May 2009 03:09:19 -0400 Subject: [PATCH 001/687] Basic README stuff --- README | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..26f985e --- /dev/null +++ b/README @@ -0,0 +1,50 @@ +Tango - Easy Twitter utilities for Django-based applications +----------------------------------------------------------------------------------------------------- +As a Django user, I've often wanted a way to easily implement Twitter scraping calls in my applications. +I could write them from scratch using the already existing (and quite excellent) library called +"Python-Twitter" (http://code.google.com/p/python-twitter/)... + +However, Python-Twitter doesn't (currently) handle some things well, such as using Twitter's +Search API. I've found myself wanting a largely drop-in solution for this (and other) +problems... + +Thus, we now have Tango. + + +Installation +----------------------------------------------------------------------------------------------------- +You can install this like any other Django-app; just throw it in as a new app, register it in your +settings as an "Installed App", and sync the models. + +Tango requires (much like Python-Twitter, because they had the right idea :D) a library called +"simplejson" - Django should include this by default, but if you need the library for any reason, +you can grab it at the following link: + +http://pypi.python.org/pypi/simplejson + +Tango also works well as a standalone Python library, so feel free to use it however you like. + + +Example Use +----------------------------------------------------------------------------------------------------- +An extremely generic, and somewhat useless, demo is below. Instantiate your class by passing in a +Twitter username, then all functions will come off of that. Results are returned as a list. + +testList = tango("ryanmcgrath") +newTestList = testList.getSearchTimeline("b", "20") +for testTweet in newTestList: + print testTweet + + +Questions, Comments, etc? +----------------------------------------------------------------------------------------------------- +I want to note that Tango is *not* like other Twitter libraries - we don't handle authentication or +anything of the sort (yet); there are already battle-tested solutions out there for both Basic Auth and +OAuth. This isn't production ready quite yet. + +Tango will (hopefully) be compatible with Python 3; as it stands, I think it might be now, I've just +not had the time to check over it. + +My hope is that Tango is so plug-and-play that you'd never *have* to ask any questions, but if +you feel the need to contact me for this (or other) reasons, you can hit me up +at ryan@venodesigns.net. From 91fc174fdc3858317258051b3059104fb8a712ac Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 4 May 2009 03:09:46 -0400 Subject: [PATCH 002/687] Beginnings of Tango, an awesome Python Twitter library for Django --- tango.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tango.py diff --git a/tango.py b/tango.py new file mode 100644 index 0000000..c3d631e --- /dev/null +++ b/tango.py @@ -0,0 +1,33 @@ +""" + Django-Twitter (Tango) utility functions. Huzzah. +""" + +import simplejson, urllib, urllib2 + +class tango: + def __init__(self, twitter_user): + # Authenticate here? + self.twitter_user = twitter_user + + def getUserTimeline(self, optional_count): + userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" + ("" if optional_count is None else "?count=" + optional_count) + userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) + formattedTimeline = [] + for tweet in userTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline + + def getSearchTimeline(self, search_query, optional_page): + params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* + searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) + formattedTimeline = [] + for tweet in searchTimeline['results']: + formattedTimeline.append(tweet['text']) + return formattedTimeline + + def getTrendingTopics(self): + trendingTopicsURL = "http://search.twitter.com/trends.json" + trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) + pass # for now, coming soon + + From f2553f81dcb204733ea62cbdc8ce9ae3a0766bf6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 5 May 2009 02:19:39 -0400 Subject: [PATCH 003/687] Implemented a trending topics search function. Returns an array of dictionary items to loop through - fairly snazzy. --- tango.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tango.py b/tango.py index c3d631e..763049e 100644 --- a/tango.py +++ b/tango.py @@ -26,8 +26,10 @@ class tango: return formattedTimeline def getTrendingTopics(self): + # Returns an array of dictionary items containing the current trends trendingTopicsURL = "http://search.twitter.com/trends.json" trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) - pass # for now, coming soon - - + trendingTopicsArray = [] + for topic in trendingTopics['trends']: + trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) + return trendingTopicsArray From 5a7a0ebcc9e11d7b45376fa85a6094d4e8397fb4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 6 May 2009 02:20:50 -0400 Subject: [PATCH 004/687] Renaming a function to make room for other trending search stuff --- tango.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index 763049e..301dd02 100644 --- a/tango.py +++ b/tango.py @@ -9,7 +9,7 @@ class tango: # Authenticate here? self.twitter_user = twitter_user - def getUserTimeline(self, optional_count): + def getUserTimeline(self, count, page, since_id): userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" + ("" if optional_count is None else "?count=" + optional_count) userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) formattedTimeline = [] @@ -25,7 +25,7 @@ class tango: formattedTimeline.append(tweet['text']) return formattedTimeline - def getTrendingTopics(self): + def getCurrentTrends(self): # Returns an array of dictionary items containing the current trends trendingTopicsURL = "http://search.twitter.com/trends.json" trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) From 777ac8e6e48ea89ccba7975eb57f7b193255b976 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 7 May 2009 01:39:42 -0400 Subject: [PATCH 005/687] Updated tango.getUserTimeline() to take **kwargs, need to implement full API support for it --- tango.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index 301dd02..1c90a06 100644 --- a/tango.py +++ b/tango.py @@ -9,8 +9,11 @@ class tango: # Authenticate here? self.twitter_user = twitter_user - def getUserTimeline(self, count, page, since_id): - userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" + ("" if optional_count is None else "?count=" + optional_count) + def getUserTimeline(self, **kwargs): + # Needs full API support, when I'm not so damn tired. + userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" + if kwargs["count"] is not None: + userTimelineURL += "?count=" + kwargs["count"] userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) formattedTimeline = [] for tweet in userTimeline: From 0b29bfbbae0d3ab33306158ed2ce0cdcf2d0291a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 7 May 2009 21:52:17 -0400 Subject: [PATCH 006/687] Expanded getUserTimeline() API support --- tango.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tango.py b/tango.py index 1c90a06..2455754 100644 --- a/tango.py +++ b/tango.py @@ -14,6 +14,12 @@ class tango: userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" if kwargs["count"] is not None: userTimelineURL += "?count=" + kwargs["count"] + if kwargs["since_id"] is not None: + userTimelineURL += "?since_id=" + kwargs["since_id"] + if kwargs["max_id"] is not None: + userTimelineURL += "?max_id=" + kwargs["max_id"] + if kwargs["page"] is not None: + userTimelineURL += "?page=" + kwargs["page"] userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) formattedTimeline = [] for tweet in userTimeline: From 79f14afba263940a724e1addb9e6adb4783b1ce0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 8 May 2009 02:37:39 -0400 Subject: [PATCH 007/687] Added more API support to getUserTimeline(); fixed issues with Tango not properly importing, started basic Auth shell --- tango.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/tango.py b/tango.py index 2455754..84f5ec0 100644 --- a/tango.py +++ b/tango.py @@ -1,25 +1,37 @@ +#!/usr/bin/python + """ Django-Twitter (Tango) utility functions. Huzzah. """ -import simplejson, urllib, urllib2 +import simplejson, urllib, urllib2, base64 + +# Need to support URL shortening class tango: - def __init__(self, twitter_user): - # Authenticate here? - self.twitter_user = twitter_user + def __init__(self, authtype = None, username = None, password = None): + self.authtype = authtype + self.username = username + self.password = password + # Forthcoming auth work below, now requires base64 shiz + if self.authtype == "OAuth": + pass + elif self.authtype == "Basic": + print "Basic Auth" + else: + pass - def getUserTimeline(self, **kwargs): - # Needs full API support, when I'm not so damn tired. - userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.twitter_user + ".json" - if kwargs["count"] is not None: - userTimelineURL += "?count=" + kwargs["count"] - if kwargs["since_id"] is not None: - userTimelineURL += "?since_id=" + kwargs["since_id"] - if kwargs["max_id"] is not None: - userTimelineURL += "?max_id=" + kwargs["max_id"] - if kwargs["page"] is not None: - userTimelineURL += "?page=" + kwargs["page"] + def getUserTimeline(self, count = None, since_id = None, max_id = None, page = None): + # Fairly close to full API support, need a few other methods. + userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.username + ".json" + if count is not None: + userTimelineURL += "?count=" + count + if since_id is not None: + userTimelineURL += "?since_id=" + since_id + if max_id is not None: + userTimelineURL += "?max_id=" + max_id + if page is not None: + userTimelineURL += "?page=" + page userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) formattedTimeline = [] for tweet in userTimeline: From dda51e2f54cef225ea3402be61c62c0365104fb6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 12 May 2009 01:52:44 -0400 Subject: [PATCH 008/687] Abstracted out parameter passing so that it's nothing but **kwargs. Function kwargs are passed to a url-builder function and builds based off of that; should make life easier if Twitter decides to change their API parameters, because users can then change what they're passing without modifying the library. --- tango.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tango.py b/tango.py index 84f5ec0..153e44f 100644 --- a/tango.py +++ b/tango.py @@ -8,7 +8,7 @@ import simplejson, urllib, urllib2, base64 # Need to support URL shortening -class tango: +class setup: def __init__(self, authtype = None, username = None, password = None): self.authtype = authtype self.username = username @@ -21,17 +21,20 @@ class tango: else: pass - def getUserTimeline(self, count = None, since_id = None, max_id = None, page = None): - # Fairly close to full API support, need a few other methods. - userTimelineURL = "http://twitter.com/statuses/user_timeline/" + self.username + ".json" - if count is not None: - userTimelineURL += "?count=" + count - if since_id is not None: - userTimelineURL += "?since_id=" + since_id - if max_id is not None: - userTimelineURL += "?max_id=" + max_id - if page is not None: - userTimelineURL += "?page=" + page + def constructApiURL(self, base_url, params): + queryURL = base_url + questionMarkUsed = False + for param in params: + if params[param] is not None: + queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) + questionMarkUsed = True + return queryURL + + def getUserTimeline(self, **kwargs): + # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication + # By doing this with kwargs and constructing a url outside, we can stay somewhat agnostic of API changes - it's all + # based on what the user decides to pass. We just handle the heavy lifting! :D + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) formattedTimeline = [] for tweet in userTimeline: From f93d02dfb975ed8664ba3667b0727658ae10e5d7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 12 May 2009 01:56:24 -0400 Subject: [PATCH 009/687] Modifying example somewhat --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 26f985e..2b8c86d 100644 --- a/README +++ b/README @@ -30,7 +30,7 @@ Example Use An extremely generic, and somewhat useless, demo is below. Instantiate your class by passing in a Twitter username, then all functions will come off of that. Results are returned as a list. -testList = tango("ryanmcgrath") +testList = tango.setup(username="ryanmcgrath") newTestList = testList.getSearchTimeline("b", "20") for testTweet in newTestList: print testTweet From 1f544c6aaf701816c4eed7c24080b73f6ca20180 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 13 May 2009 03:25:57 -0400 Subject: [PATCH 010/687] Added a method to check against the public timeline --- tango.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tango.py b/tango.py index 153e44f..6e97c21 100644 --- a/tango.py +++ b/tango.py @@ -30,6 +30,13 @@ class setup: questionMarkUsed = True return queryURL + def getPublicTimeline(self): + publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + formattedTimeline = [] + for tweet in publicTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline + def getUserTimeline(self, **kwargs): # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication # By doing this with kwargs and constructing a url outside, we can stay somewhat agnostic of API changes - it's all @@ -40,7 +47,16 @@ class setup: for tweet in userTimeline: formattedTimeline.append(tweet['text']) return formattedTimeline + + def getUserMentions(self, **kwargs): + pass + def updateStatus(self, **kwargs): + pass + + def destroyStatus(self, **kwargs): + pass + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) From c47d6008a0e86af6f709578b6225dd0a209ff49c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 14 May 2009 01:38:37 -0400 Subject: [PATCH 011/687] Worked in some try/catch support for loading and hitting the API - needs to happen for other functions as well, once I have more time. --- tango.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tango.py b/tango.py index 6e97c21..cdaa788 100644 --- a/tango.py +++ b/tango.py @@ -31,11 +31,17 @@ class setup: return queryURL def getPublicTimeline(self): - publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - formattedTimeline = [] - for tweet in publicTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline + try: + publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + formattedTimeline = [] + for tweet in publicTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline + except IOError, e: + if hasattr(e, 'code'): + return "Loading API failed with HTTP Status Code " + e.code + else: + return "God help us all, Scotty, she's dead and we're not sure why." def getUserTimeline(self, **kwargs): # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication From 5debb9bca8c2393e1f9b329adfdec74d92db2fd2 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 15 May 2009 01:41:05 -0400 Subject: [PATCH 012/687] Got Basic Auth working through usage of httplib2 - Basic Auth is a horrible idea, and OAuth should be used, but it's still worth supporting for the time being. Why go and re-engineer my way around httplib when the work is already done? --- tango.py | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/tango.py b/tango.py index cdaa788..8093955 100644 --- a/tango.py +++ b/tango.py @@ -4,20 +4,24 @@ Django-Twitter (Tango) utility functions. Huzzah. """ -import simplejson, urllib, urllib2, base64 +import simplejson, httplib2, urllib, urllib2, base64 # Need to support URL shortening class setup: def __init__(self, authtype = None, username = None, password = None): self.authtype = authtype + self.authenticated = False self.username = username self.password = password - # Forthcoming auth work below, now requires base64 shiz - if self.authtype == "OAuth": - pass - elif self.authtype == "Basic": - print "Basic Auth" + self.http = httplib2.Http() # For Basic Auth... + # Forthcoming auth work below, now requires base64 shiz + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + pass + elif self.authtype == "Basic": + self.http.add_credentials(self.username, self.password) + self.authenticated = True else: pass @@ -55,13 +59,29 @@ class setup: return formattedTimeline def getUserMentions(self, **kwargs): - pass + if self.authenticated is True: + pass + else: + print "getUserMentions() requires you to be authenticated." + pass - def updateStatus(self, **kwargs): - pass + def updateStatus(self, status = None, in_reply_to_status_id = None): + if self.authenticated is True: + if self.authtype == "Basic": + self.http.request("http://twitter.com/statuses/update.json", "POST", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) + else: + print "Sorry, OAuth support is still forthcoming. Feel free to help out on this front!" + pass + else: + print "updateStatus() requires you to be authenticated." + pass def destroyStatus(self, **kwargs): - pass + if self.authenticated is True: + pass + else: + print "destroyStatus() requires you to be authenticated." + pass def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* From 4f70913ddbb45cf71f60904eb68487956ed935fb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 15 May 2009 18:18:23 -0400 Subject: [PATCH 013/687] Set OAuth as the default authtype --- tango.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/tango.py b/tango.py index 8093955..0685f77 100644 --- a/tango.py +++ b/tango.py @@ -4,18 +4,17 @@ Django-Twitter (Tango) utility functions. Huzzah. """ -import simplejson, httplib2, urllib, urllib2, base64 +import simplejson, httplib2, urllib, urllib2 # Need to support URL shortening class setup: - def __init__(self, authtype = None, username = None, password = None): + def __init__(self, authtype = "OAuth", username = None, password = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password self.http = httplib2.Http() # For Basic Auth... - # Forthcoming auth work below, now requires base64 shiz if self.username is not None and self.password is not None: if self.authtype == "OAuth": pass @@ -35,17 +34,11 @@ class setup: return queryURL def getPublicTimeline(self): - try: - publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - formattedTimeline = [] - for tweet in publicTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline - except IOError, e: - if hasattr(e, 'code'): - return "Loading API failed with HTTP Status Code " + e.code - else: - return "God help us all, Scotty, she's dead and we're not sure why." + publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + formattedTimeline = [] + for tweet in publicTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline def getUserTimeline(self, **kwargs): # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication @@ -60,7 +53,10 @@ class setup: def getUserMentions(self, **kwargs): if self.authenticated is True: - pass + if self.authtype is "Basic": + pass + else: + pass else: print "getUserMentions() requires you to be authenticated." pass From 9a13b0ae3f5c7579101384ea9526a846ff41bdc5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 17 May 2009 00:56:49 -0400 Subject: [PATCH 014/687] Added a destroy method, added library checks at top of file --- tango.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tango.py b/tango.py index 0685f77..a844468 100644 --- a/tango.py +++ b/tango.py @@ -4,16 +4,33 @@ Django-Twitter (Tango) utility functions. Huzzah. """ -import simplejson, httplib2, urllib, urllib2 +import urllib, urllib2 + +try: + import simplejson +except: + print "Tango requires the simplejson library to work. http://www.undefined.org/python/" + +try: + import httplib2 +except: + print "Tango requires httplib2 for authentication purposes. http://code.google.com/p/httplib2/" + +try: + import oauth +except: + print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" + # Need to support URL shortening class setup: - def __init__(self, authtype = "OAuth", username = None, password = None): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password + self.oauth_keys = oauth_keys self.http = httplib2.Http() # For Basic Auth... if self.username is not None and self.password is not None: if self.authtype == "OAuth": @@ -53,7 +70,7 @@ class setup: def getUserMentions(self, **kwargs): if self.authenticated is True: - if self.authtype is "Basic": + if self.authtype == "Basic": pass else: pass @@ -72,9 +89,9 @@ class setup: print "updateStatus() requires you to be authenticated." pass - def destroyStatus(self, **kwargs): + def destroyStatus(self, id): if self.authenticated is True: - pass + self.http.request("http://twitter.com/status/destroy/" + id + ".json", "POST") else: print "destroyStatus() requires you to be authenticated." pass From 10c32e427c23696c8be2f42c500e6f204668ae81 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 21 May 2009 03:17:47 -0400 Subject: [PATCH 015/687] Added a URL shortening method that hits the is.gd API, random comments sprinkled throughout - coming along nicely. Perhaps fallbacks should be provided for when the is.gd API limit is reached? --- tango.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tango.py b/tango.py index a844468..ff1b890 100644 --- a/tango.py +++ b/tango.py @@ -11,6 +11,7 @@ try: except: print "Tango requires the simplejson library to work. http://www.undefined.org/python/" +# Should really deprecate httplib2 at some point... try: import httplib2 except: @@ -41,6 +42,11 @@ class setup: else: pass + def shortenURL(self, url_to_shorten): + # Perhaps we should have fallbacks here in case the is.gd API limit gets hit? Maybe allow them to set the host? + shortURL = urllib2.urlopen("http://is.gd/api.php?" + urllib.urlencode({"longurl": url_to_shorten})).read() + return shortURL + def constructApiURL(self, base_url, params): queryURL = base_url questionMarkUsed = False From 1887280855264cd01761ebb717227f9c3649dddf Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 26 May 2009 01:29:03 -0400 Subject: [PATCH 016/687] Aiming to remove need for httplib2, but sadly this commit is broken. --- tango.py | 205 +++++++++++++++++++++++++++---------------------------- 1 file changed, 100 insertions(+), 105 deletions(-) diff --git a/tango.py b/tango.py index ff1b890..ea74d7f 100644 --- a/tango.py +++ b/tango.py @@ -7,114 +7,109 @@ import urllib, urllib2 try: - import simplejson + import simplejson except: - print "Tango requires the simplejson library to work. http://www.undefined.org/python/" - -# Should really deprecate httplib2 at some point... -try: - import httplib2 -except: - print "Tango requires httplib2 for authentication purposes. http://code.google.com/p/httplib2/" + print "Tango requires the simplejson library to work. http://www.undefined.org/python/" try: - import oauth + import oauth except: - print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" - - -# Need to support URL shortening + print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - self.http = httplib2.Http() # For Basic Auth... - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - pass - elif self.authtype == "Basic": - self.http.add_credentials(self.username, self.password) - self.authenticated = True - else: - pass - - def shortenURL(self, url_to_shorten): - # Perhaps we should have fallbacks here in case the is.gd API limit gets hit? Maybe allow them to set the host? - shortURL = urllib2.urlopen("http://is.gd/api.php?" + urllib.urlencode({"longurl": url_to_shorten})).read() - return shortURL - - def constructApiURL(self, base_url, params): - queryURL = base_url - questionMarkUsed = False - for param in params: - if params[param] is not None: - queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) - questionMarkUsed = True - return queryURL - - def getPublicTimeline(self): - publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - formattedTimeline = [] - for tweet in publicTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline - - def getUserTimeline(self, **kwargs): - # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication - # By doing this with kwargs and constructing a url outside, we can stay somewhat agnostic of API changes - it's all - # based on what the user decides to pass. We just handle the heavy lifting! :D - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) - formattedTimeline = [] - for tweet in userTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - if self.authtype == "Basic": - pass - else: - pass - else: - print "getUserMentions() requires you to be authenticated." - pass - - def updateStatus(self, status = None, in_reply_to_status_id = None): - if self.authenticated is True: - if self.authtype == "Basic": - self.http.request("http://twitter.com/statuses/update.json", "POST", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) - else: - print "Sorry, OAuth support is still forthcoming. Feel free to help out on this front!" - pass - else: - print "updateStatus() requires you to be authenticated." - pass - - def destroyStatus(self, id): - if self.authenticated is True: - self.http.request("http://twitter.com/status/destroy/" + id + ".json", "POST") - else: - print "destroyStatus() requires you to be authenticated." - pass - - def getSearchTimeline(self, search_query, optional_page): - params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* - searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) - formattedTimeline = [] - for tweet in searchTimeline['results']: - formattedTimeline.append(tweet['text']) - return formattedTimeline - - def getCurrentTrends(self): - # Returns an array of dictionary items containing the current trends - trendingTopicsURL = "http://search.twitter.com/trends.json" - trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) - trendingTopicsArray = [] - for topic in trendingTopics['trends']: - trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) - return trendingTopicsArray + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + self.opener = None + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + pass + elif self.authtype == "Basic": + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com/account/verify_credentials.json", self.username, self.password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + self.authenticated = True + else: + pass + + def shortenURL(self, url_to_shorten): + # Perhaps we should have fallbacks here in case the is.gd API limit gets hit? Maybe allow them to set the host? + shortURL = urllib2.urlopen("http://is.gd/api.php?" + urllib.urlencode({"longurl": url_to_shorten})).read() + return shortURL + + def constructApiURL(self, base_url, params): + queryURL = base_url + questionMarkUsed = False + for param in params: + if params[param] is not None: + queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) + questionMarkUsed = True + return queryURL + + def getPublicTimeline(self): + publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + formattedTimeline = [] + for tweet in publicTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline + + def getUserTimeline(self, **kwargs): + # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication + # By doing this with kwargs and constructing a url outside, we can stay somewhat agnostic of API changes - it's all + # based on what the user decides to pass. We just handle the heavy lifting! :D + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) + formattedTimeline = [] + for tweet in userTimeline: + formattedTimeline.append(tweet['text']) + return formattedTimeline + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + if self.authtype == "Basic": + pass + else: + pass + else: + print "getUserMentions() requires you to be authenticated." + pass + + def updateStatus(self, status, in_reply_to_status_id = ""): + if self.authenticated is True: + if self.authtype == "Basic": + self.opener.open("http://twitter.com/statuses/update.json" + urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) + print self.opener.open("http://twitter.com/statuses/update.json" + urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})).read() + else: + print "Sorry, OAuth support is still forthcoming. Feel free to help out on this front!" + pass + else: + print "updateStatus() requires you to be authenticated." + pass + + def destroyStatus(self, id): + if self.authenticated is True: + self.http.request("http://twitter.com/status/destroy/" + id + ".json", "POST") + else: + print "destroyStatus() requires you to be authenticated." + pass + + def getSearchTimeline(self, search_query, optional_page): + params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* + searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) + formattedTimeline = [] + for tweet in searchTimeline['results']: + formattedTimeline.append(tweet['text']) + return formattedTimeline + + def getCurrentTrends(self): + # Returns an array of dictionary items containing the current trends + trendingTopicsURL = "http://search.twitter.com/trends.json" + trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) + trendingTopicsArray = [] + for topic in trendingTopics['trends']: + trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) + return trendingTopicsArray From 60c6aae34612a37e31c8f8c20ef6346e221a69a1 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 26 May 2009 02:43:26 -0400 Subject: [PATCH 017/687] Finally removed the need for httplib2, Basic HTTP Auth is now working. --- tango.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tango.py b/tango.py index ea74d7f..1a99ed0 100644 --- a/tango.py +++ b/tango.py @@ -6,6 +6,8 @@ import urllib, urllib2 +from urllib2 import HTTPError + try: import simplejson except: @@ -23,13 +25,12 @@ class setup: self.username = username self.password = password self.oauth_keys = oauth_keys - self.opener = None if self.username is not None and self.password is not None: if self.authtype == "OAuth": pass elif self.authtype == "Basic": self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com/account/verify_credentials.json", self.username, self.password) + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) self.authenticated = True @@ -78,11 +79,14 @@ class setup: print "getUserMentions() requires you to be authenticated." pass - def updateStatus(self, status, in_reply_to_status_id = ""): + def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: if self.authtype == "Basic": - self.opener.open("http://twitter.com/statuses/update.json" + urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) - print self.opener.open("http://twitter.com/statuses/update.json" + urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})).read() + try: + self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) + except HTTPError, e: + print e.code + print e.headers else: print "Sorry, OAuth support is still forthcoming. Feel free to help out on this front!" pass From b6a03f918ce8c862d48407d149dd49c9b6918627 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 28 May 2009 01:58:56 -0400 Subject: [PATCH 018/687] Many, many changes. Broke out the url shortener so it's more plug and play; users can now throw in their own desired shortening service, and it should auto work. All timeline methods are now implemented, moving on to user methods next. Refactored some parts of the library that were becoming kludgy, and set an optional debug parameter that may be useful in the future. --- tango.py | 108 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/tango.py b/tango.py index 1a99ed0..0802d5e 100644 --- a/tango.py +++ b/tango.py @@ -1,7 +1,11 @@ #!/usr/bin/python """ - Django-Twitter (Tango) utility functions. Huzzah. + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + Questions, comments? ryan@venodesigns.net """ import urllib, urllib2 @@ -19,12 +23,13 @@ except: print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): self.authtype = authtype self.authenticated = False self.username = username self.password = password self.oauth_keys = oauth_keys + self.debug = debug if self.username is not None and self.password is not None: if self.authtype == "OAuth": pass @@ -37,9 +42,26 @@ class setup: else: pass - def shortenURL(self, url_to_shorten): - # Perhaps we should have fallbacks here in case the is.gd API limit gets hit? Maybe allow them to set the host? - shortURL = urllib2.urlopen("http://is.gd/api.php?" + urllib.urlencode({"longurl": url_to_shorten})).read() + def explainOAuthSupport(self): + print "Sorry, OAuth support is still forthcoming. Default back to Basic Authentication for now, or help out on this front!" + pass + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + shortURL = urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() return shortURL def constructApiURL(self, base_url, params): @@ -51,28 +73,45 @@ class setup: questionMarkUsed = True return queryURL + def createGenericTimeline(self, existingTimeline): + # Many of Twitter's API functions return the same style of data. This function just wraps it if we need it. + genericTimeline = [] + for tweet in existingTimeline: + genericTimeline.append({ + "created_at": tweet["created_at"], + "in_reply_to_screen_name": tweet["in_reply_to_screen_name"], + "in_reply_to_status_id": tweet["in_reply_to_status_id"], + "in_reply_to_user_id": tweet["in_reply_to_user_id"], + "id": tweet["id"], + "text": tweet["text"] + }) + return genericTimeline + def getPublicTimeline(self): - publicTimeline = simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - formattedTimeline = [] - for tweet in publicTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline + return self.createGenericTimeline(simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json"))) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return self.createGenericTimeline(simplejson.load(self.opener.open(friendsTimelineURL))) + else: + print "getFriendsTimeline() requires you to be authenticated." + pass def getUserTimeline(self, **kwargs): - # 99% API compliant, I think - need to figure out Gzip compression and auto-getting based on authentication - # By doing this with kwargs and constructing a url outside, we can stay somewhat agnostic of API changes - it's all - # based on what the user decides to pass. We just handle the heavy lifting! :D userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - userTimeline = simplejson.load(urllib2.urlopen(userTimelineURL)) - formattedTimeline = [] - for tweet in userTimeline: - formattedTimeline.append(tweet['text']) - return formattedTimeline + try: + return self.createGenericTimeline(simplejson.load(urllib2.urlopen(userTimelineURL))) + except: + print "Hmmm, that failed. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + pass def getUserMentions(self, **kwargs): if self.authenticated is True: if self.authtype == "Basic": - pass + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + mentionsFeed = simplejson.load(self.opener.open(mentionsFeedURL)) + return self.createGenericTimeline(mentionsFeed) else: pass else: @@ -88,26 +127,41 @@ class setup: print e.code print e.headers else: - print "Sorry, OAuth support is still forthcoming. Feel free to help out on this front!" - pass + self.explainOAuthSupport() else: print "updateStatus() requires you to be authenticated." pass def destroyStatus(self, id): if self.authenticated is True: - self.http.request("http://twitter.com/status/destroy/" + id + ".json", "POST") + if self.authtype == "Basic": + self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST") + else: + self.explainOAuthSupport() else: print "destroyStatus() requires you to be authenticated." pass def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* - searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) - formattedTimeline = [] - for tweet in searchTimeline['results']: - formattedTimeline.append(tweet['text']) - return formattedTimeline + try: + searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) + # This one is custom built because the search feed is a bit different than the other feeds. + genericTimeline = [] + for tweet in searchTimeline["results"]: + genericTimeline.append({ + "created_at": tweet["created_at"], + "from_user": tweet["from_user"], + "from_user_id": tweet["from_user_id"], + "profile_image_url": tweet["profile_image_url"], + "id": tweet["id"], + "text": tweet["text"], + "to_user_id": tweet["to_user_id"] + }) + return genericTimeline + except HTTPError, e: + print e.code + print e.headers def getCurrentTrends(self): # Returns an array of dictionary items containing the current trends From 3ca959fded20bc7a917f7ff203723c4440cb57f9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 29 May 2009 02:16:44 -0400 Subject: [PATCH 019/687] Added a rate limit checker, finished off user methods with a showStatus method, cleaned up some code relating to HTTP Error codes. --- tango.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/tango.py b/tango.py index 0802d5e..afc6eaf 100644 --- a/tango.py +++ b/tango.py @@ -86,7 +86,22 @@ class setup: "text": tweet["text"] }) return genericTimeline - + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + rate_limit = simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + rate_limit = simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "It seems that there's something wrong. Twitter gave you a " + e.code + " error code; are you doing something you shouldn't be?" + return {"remaining-hits": rate_limit["remaining-hits"], + "hourly-limit": rate_limit["hourly-limit"], + "reset-time": rate_limit["reset-time"], + "reset-time-in-seconds": rate_limit["reset-time-in-seconds"]} + def getPublicTimeline(self): return self.createGenericTimeline(simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json"))) @@ -102,8 +117,10 @@ class setup: userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) try: return self.createGenericTimeline(simplejson.load(urllib2.urlopen(userTimelineURL))) - except: - print "Hmmm, that failed. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + except HTTPError, e: + if self.debug is True: + print e.headers + print "Hmmm, failed with a " + e.code + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." pass def getUserMentions(self, **kwargs): @@ -118,6 +135,21 @@ class setup: print "getUserMentions() requires you to be authenticated." pass + def showStatus(self, id): + try: + tweet = simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) + return {"created_at": tweet["created_at"], + "id": tweet["id"], + "text": tweet["text"], + "in_reply_to_status_id": tweet["in_reply_to_status_id"], + "in_reply_to_user_id": tweet["in_reply_to_user_id"], + "in_reply_to_screen_name": tweet["in_reply_to_screen_name"]} + except HTTPError, e: + if self.debug is True: + print e.headers + print "Hmmm, failed with a " + e.code + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + pass + def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: if self.authtype == "Basic": From 09b24fbb909dba3091ea202741cf300b74e6824f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 31 May 2009 19:57:56 -0400 Subject: [PATCH 020/687] Support for ending open sessions --- tango.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tango.py b/tango.py index afc6eaf..3b4eb86 100644 --- a/tango.py +++ b/tango.py @@ -174,6 +174,20 @@ class setup: print "destroyStatus() requires you to be authenticated." pass + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except: + if self.debug is True: + print e.headers + print "endSession failed with a " + e.code + " error code." + pass + else: + print "You can't end a session when you're not authenticated to begin with." + pass + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: From 4c4d1bd8762884a2ace3d0018718a4abaeb78e09 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 31 May 2009 23:18:27 -0400 Subject: [PATCH 021/687] A rather large and ugly updateProfile() method, but it works. --- tango.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index 3b4eb86..f147136 100644 --- a/tango.py +++ b/tango.py @@ -8,7 +8,7 @@ Questions, comments? ryan@venodesigns.net """ -import urllib, urllib2 +import httplib, urllib, urllib2, mimetypes from urllib2 import HTTPError @@ -179,7 +179,7 @@ class setup: try: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False - except: + except HTTPError, e: if self.debug is True: print e.headers print "endSession failed with a " + e.code + " error code." @@ -188,6 +188,85 @@ class setup: print "You can't end a session when you're not authenticated to begin with." pass + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateDeliveryDevice() failed with a " + e.code + " error code." + else: + print "updateDeliveryDevice() requires you to be authenticated." + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateProfileColors() failed with a " + e.code + " error code." + else: + print "updateProfileColors() requires you to be authenticated." + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + print "Twitter has a character limit of 20 for all usernames. Try again." + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + print "Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again." + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + print "Twitter has a character limit of 100 for all urls. Try again." + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + print "Twitter has a character limit of 30 for all locations. Try again." + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + print "Twitter has a character limit of 160 for all descriptions. Try again." + + if updateProfileQueryString != "": + try: + self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateProfile() failed with a " + e.code + " error code." + else: + # If they're not authenticated + print "updateProfile() requires you to be authenticated." + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: From 4cf64e3f60d0571bcfada4ebc43b5e1b9daccc87 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 1 Jun 2009 00:02:47 -0400 Subject: [PATCH 022/687] Added in methods for getting, creating, and destroying favorites --- tango.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tango.py b/tango.py index f147136..52e1529 100644 --- a/tango.py +++ b/tango.py @@ -267,6 +267,40 @@ class setup: # If they're not authenticated print "updateProfile() requires you to be authenticated." + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + favoritesTimeline = simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + return self.createGenericTimeline(favoritesTimeline) + except HTTPError, e: + if self.debug: + print e.headers + print "getFavorites() failed with a " + e.code + " error code." + else: + print "getFavorites() requires you to be authenticated." + + def createFavorite(self, id): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "") + except HTTPError, e: + if self.debug: + print e.headers + print "createFavorite() failed with a " + e.code + " error code." + else: + print "createFavorite() requires you to be authenticated." + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "") + except HTTPError, e: + if self.debug: + print e.headers + print "destroyFavorite() failed with a " + e.code + " error code." + else: + print "destroyFavorite() requires you to be authenticated." + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: From 000d9993ee387abe80c9cddf252c89228d11a7ea Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 1 Jun 2009 00:55:46 -0400 Subject: [PATCH 023/687] Methods for the notification API; cleaned up a lot of code, threw in more try/excepts to attempt error catching, moved certain lines behind the debug parameter --- tango.py | 127 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/tango.py b/tango.py index 52e1529..d7cc832 100644 --- a/tango.py +++ b/tango.py @@ -42,10 +42,6 @@ class setup: else: pass - def explainOAuthSupport(self): - print "Sorry, OAuth support is still forthcoming. Default back to Basic Authentication for now, or help out on this front!" - pass - # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): pass @@ -96,19 +92,29 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "It seems that there's something wrong. Twitter gave you a " + e.code + " error code; are you doing something you shouldn't be?" + print "It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?" return {"remaining-hits": rate_limit["remaining-hits"], "hourly-limit": rate_limit["hourly-limit"], "reset-time": rate_limit["reset-time"], "reset-time-in-seconds": rate_limit["reset-time-in-seconds"]} def getPublicTimeline(self): - return self.createGenericTimeline(simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json"))) + try: + return self.createGenericTimeline(simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json"))) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getPublicTimeline() failed with a " + str(e.code) + " error code." def getFriendsTimeline(self, **kwargs): if self.authenticated is True: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return self.createGenericTimeline(simplejson.load(self.opener.open(friendsTimelineURL))) + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return self.createGenericTimeline(simplejson.load(self.opener.open(friendsTimelineURL))) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getFriendsTimeline() failed with a " + str(e.code) + " error code." else: print "getFriendsTimeline() requires you to be authenticated." pass @@ -120,17 +126,19 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "Hmmm, failed with a " + e.code + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." pass def getUserMentions(self, **kwargs): if self.authenticated is True: - if self.authtype == "Basic": + try: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) mentionsFeed = simplejson.load(self.opener.open(mentionsFeedURL)) return self.createGenericTimeline(mentionsFeed) - else: - pass + except HTTPError, e: + if self.debug is True: + print e.headers + print "getUserMentions() failed with a " + str(e.code) + " error code." else: print "getUserMentions() requires you to be authenticated." pass @@ -147,29 +155,29 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "Hmmm, failed with a " + e.code + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." pass def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: - if self.authtype == "Basic": - try: - self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) - except HTTPError, e: - print e.code + try: + self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) + except HTTPError, e: + if self.debug is True: print e.headers - else: - self.explainOAuthSupport() + print "updateStatus() failed with a " + str(e.code) + " error code." else: print "updateStatus() requires you to be authenticated." pass def destroyStatus(self, id): if self.authenticated is True: - if self.authtype == "Basic": + try: self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST") - else: - self.explainOAuthSupport() + except HTTPError, e: + if self.debug is True: + print e.headers + print "destroyStatus() failed with a " + str(e.code) + " error code." else: print "destroyStatus() requires you to be authenticated." pass @@ -182,8 +190,7 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "endSession failed with a " + e.code + " error code." - pass + print "endSession failed with a " + str(e.code) + " error code." else: print "You can't end a session when you're not authenticated to begin with." pass @@ -195,7 +202,7 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "updateDeliveryDevice() failed with a " + e.code + " error code." + print "updateDeliveryDevice() failed with a " + str(e.code) + " error code." else: print "updateDeliveryDevice() requires you to be authenticated." @@ -206,7 +213,7 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "updateProfileColors() failed with a " + e.code + " error code." + print "updateProfileColors() failed with a " + str(e.code) + " error code." else: print "updateProfileColors() requires you to be authenticated." @@ -275,7 +282,7 @@ class setup: except HTTPError, e: if self.debug: print e.headers - print "getFavorites() failed with a " + e.code + " error code." + print "getFavorites() failed with a " + str(e.code) + " error code." else: print "getFavorites() requires you to be authenticated." @@ -286,7 +293,7 @@ class setup: except HTTPError, e: if self.debug: print e.headers - print "createFavorite() failed with a " + e.code + " error code." + print "createFavorite() failed with a " + str(e.code) + " error code." else: print "createFavorite() requires you to be authenticated." @@ -297,10 +304,46 @@ class setup: except HTTPError, e: if self.debug: print e.headers - print "destroyFavorite() failed with a " + e.code + " error code." + print "destroyFavorite() failed with a " + str(e.code) + " error code." else: print "destroyFavorite() requires you to be authenticated." + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name" + screen_name + try: + self.opener.open(apiURL, "") + except HTTPError, e: + if self.debug is True: + print e.headers + print "notificationFollow() failed with a " + str(e.code) + " error code." + else: + print "notificationFollow() requires you to be authenticated." + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name" + screen_name + try: + self.opener.open(apiURL, "") + except HTTPError, e: + if self.debug is True: + print e.headers + print "notificationLeave() failed with a " + str(e.code) + " error code." + else: + print "notificationLeave() requires you to be authenticated." + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: @@ -319,14 +362,20 @@ class setup: }) return genericTimeline except HTTPError, e: - print e.code - print e.headers + if self.debug is True: + print e.headers + print "getSearchTimeline() failed with a " + str(e.code) + " error code." def getCurrentTrends(self): - # Returns an array of dictionary items containing the current trends - trendingTopicsURL = "http://search.twitter.com/trends.json" - trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) - trendingTopicsArray = [] - for topic in trendingTopics['trends']: - trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) - return trendingTopicsArray + try: + # Returns an array of dictionary items containing the current trends + trendingTopicsURL = "http://search.twitter.com/trends.json" + trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) + trendingTopicsArray = [] + for topic in trendingTopics['trends']: + trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) + return trendingTopicsArray + except HTTPError, e: + if self.debug is True: + print e.headers + print "getCurrentTrends() failed with a " + str(e.code) + " error code." From 23d3cca4fcc3de482770d4975f6032bf4b5cd87e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 1 Jun 2009 04:52:07 -0400 Subject: [PATCH 024/687] Finished off the Account methods after a 3 hour session of figuring out multipart data in Python - this seriously needs to be part of the standard language. The only thing I've wished I had out of the box thus far. --- tango.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/tango.py b/tango.py index d7cc832..d50897c 100644 --- a/tango.py +++ b/tango.py @@ -8,18 +8,18 @@ Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes +import httplib, urllib, urllib2, mimetypes, mimetools from urllib2 import HTTPError try: import simplejson -except: +except ImportError: print "Tango requires the simplejson library to work. http://www.undefined.org/python/" try: import oauth -except: +except ImportError: print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" class setup: @@ -70,7 +70,7 @@ class setup: return queryURL def createGenericTimeline(self, existingTimeline): - # Many of Twitter's API functions return the same style of data. This function just wraps it if we need it. + # Many of Twitters API functions return the same style of data. This function just wraps it if we need it. genericTimeline = [] for tweet in existingTimeline: genericTimeline.append({ @@ -379,3 +379,56 @@ class setup: if self.debug is True: print e.headers print "getCurrentTrends() failed with a " + str(e.code) + " error code." + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + #try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + #except: + #print "Oh god, this failed so horribly." + else: + print "You realize you need to be authenticated to change a background image, right?" + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except: + print "Oh god, this failed so horribly." + else: + print "You realize you need to be authenticated to change a profile image, right?" + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From 833ad1ebe8a8db1059214ebb3ab6c70326bfde77 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 3 Jun 2009 03:51:55 -0400 Subject: [PATCH 025/687] Added social graph methods, cleaned up notification methods, tired as all hell. --- tango.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/tango.py b/tango.py index d50897c..007d24d 100644 --- a/tango.py +++ b/tango.py @@ -314,9 +314,9 @@ class setup: if id is not None: apiURL = "http://twitter.com/notifications/follow/" + id + ".json" if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id" + user_id + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name" + screen_name + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name try: self.opener.open(apiURL, "") except HTTPError, e: @@ -332,9 +332,9 @@ class setup: if id is not None: apiURL = "http://twitter.com/notifications/leave/" + id + ".json" if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id" + user_id + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name" + screen_name + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name try: self.opener.open(apiURL, "") except HTTPError, e: @@ -344,6 +344,36 @@ class setup: else: print "notificationLeave() requires you to be authenticated." + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getFriendsIDs() failed with a " + str(e.code) + " error code." + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getFollowersIDs() failed with a " + str(e.code) + " error code." + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: From 3ec4805fd18f68bf198a37fdf4aae9f46158a71a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 4 Jun 2009 02:38:11 -0400 Subject: [PATCH 026/687] Annotating a TODO list --- tango.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tango.py b/tango.py index 007d24d..38e4af0 100644 --- a/tango.py +++ b/tango.py @@ -5,6 +5,8 @@ Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. + TODO: Blocks API Wrapper, Direct Messages API Wrapper, Friendship API Wrapper, OAuth, Streaming API + Questions, comments? ryan@venodesigns.net """ From 2aff3742e0535ec2085dbe5971eccc3c4818ec87 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 6 Jun 2009 19:00:22 -0400 Subject: [PATCH 027/687] friendship() methods are now included. Just a bit more to go... --- tango.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tango.py b/tango.py index 38e4af0..e5f83d7 100644 --- a/tango.py +++ b/tango.py @@ -197,6 +197,55 @@ class setup: print "You can't end a session when you're not authenticated to begin with." pass + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "createFriendship() failed with a " + str(e.code) + " error code. " + if e.code == 403: + print "It seems you've hit the update limit for this method. Try again in 24 hours." + else: + print "createFriendship() requires you to be authenticated." + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "destroyFriendship() failed with a " + str(e.code) + " error code." + else: + print "destroyFriendship() requires you to be authenticated." + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + if self.debug is True: + print e.headers + print "checkIfFriendshipExists() failed with a " + str(e.code) + " error code." + else: + print "checkIfFriendshipExists(), oddly, requires that you be authenticated." + def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: From 4c13afd90a04a328b4266cd2bd213bc10d14d437 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 6 Jun 2009 21:10:04 -0400 Subject: [PATCH 028/687] Added blocking methods, just a wee bit more to go... --- tango.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index e5f83d7..9eb345e 100644 --- a/tango.py +++ b/tango.py @@ -207,7 +207,7 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow try: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: if self.debug is True: print e.headers @@ -227,7 +227,7 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name try: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: if self.debug is True: print e.headers @@ -425,6 +425,65 @@ class setup: print e.headers print "getFollowersIDs() failed with a " + str(e.code) + " error code." + def createBlock(self, id): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "") + except HTTPError, e: + if self.debug is True: + print e.headers + print "createBlock() failed with a " + str(e.code) + " error code." + else: + print "createBlock() requires you to be authenticated." + + def destroyBlock(self, id): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "") + except HTTPError, e: + if self.debug is True: + print e.headers + print "destroyBlock() failed with a " + str(e.code) + " error code." + else: + print "destroyBlock() requires you to be authenticated." + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "checkIfBlockExists() failed with a " + str(e.code) + " error code." + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getBlocking() failed with a " + str(e.code) + " error code." + else: + print "getBlocking() requires you to be authenticated" + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getBlockedIDs() failed with a " + str(e.code) + " error code." + else: + print "getBlockedIDs() requires you to be authenticated." + def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: From 5342fbccc6b1bf4b633207576f225e94d3b9e197 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 6 Jun 2009 22:32:21 -0400 Subject: [PATCH 029/687] The final REST API method, Direct Messages. Remaining targets are OAuth Authentication and the Search/Streaming APIs. --- tango.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tango.py b/tango.py index 9eb345e..5d50291 100644 --- a/tango.py +++ b/tango.py @@ -197,6 +197,69 @@ class setup: print "You can't end a session when you're not authenticated to begin with." pass + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getDirectMessages() failed with a " + str(e.code) + " error code." + else: + print "getDirectMessages() requires you to be authenticated." + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getSentMessages() failed with a " + str(e.code) + " error code." + else: + print "getSentMessages() requires you to be authenticated." + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text" text})) + except HTTPError, e: + if self.debug is True: + print e.headers + print "sendDirectMessage() failed with a " + str(e.code) + " error code." + else: + print "Your message must be longer than 140 characters" + else: + print "You must be authenticated to send a new direct message." + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") + except HTTPError, e: + if self.debug is True: + print e.headers + print "destroyDirectMessage() failed with a " + str(e.code) + " error code." + else: + print "You must be authenticated to destroy a direct message." + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: apiURL = "" From 402a2dddc02c6fd49e0f11b5f818bb7fc42bcb77 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 8 Jun 2009 01:26:32 -0400 Subject: [PATCH 030/687] All methods now return a Python/JSON value depicting the data Twitter sends back --- tango.py | 127 +++++++++++++++++++------------------------------------ 1 file changed, 44 insertions(+), 83 deletions(-) diff --git a/tango.py b/tango.py index 5d50291..99ca13c 100644 --- a/tango.py +++ b/tango.py @@ -10,7 +10,7 @@ Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes, mimetools +import httplib, urllib, urllib2, mimetypes, mimetools, pprint from urllib2 import HTTPError @@ -41,8 +41,6 @@ class setup: self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) self.authenticated = True - else: - pass # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -59,8 +57,12 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - shortURL = urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - return shortURL + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError, e: + if self.debug is True: + print e.headers + print "shortenURL() failed with a " + str(e.code) + " error code." def constructApiURL(self, base_url, params): queryURL = base_url @@ -71,38 +73,20 @@ class setup: questionMarkUsed = True return queryURL - def createGenericTimeline(self, existingTimeline): - # Many of Twitters API functions return the same style of data. This function just wraps it if we need it. - genericTimeline = [] - for tweet in existingTimeline: - genericTimeline.append({ - "created_at": tweet["created_at"], - "in_reply_to_screen_name": tweet["in_reply_to_screen_name"], - "in_reply_to_status_id": tweet["in_reply_to_status_id"], - "in_reply_to_user_id": tweet["in_reply_to_user_id"], - "id": tweet["id"], - "text": tweet["text"] - }) - return genericTimeline - def getRateLimitStatus(self, rate_for = "requestingIP"): try: if rate_for == "requestingIP": - rate_limit = simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) else: - rate_limit = simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) except HTTPError, e: if self.debug is True: print e.headers print "It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?" - return {"remaining-hits": rate_limit["remaining-hits"], - "hourly-limit": rate_limit["hourly-limit"], - "reset-time": rate_limit["reset-time"], - "reset-time-in-seconds": rate_limit["reset-time-in-seconds"]} - + def getPublicTimeline(self): try: - return self.createGenericTimeline(simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json"))) + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError, e: if self.debug is True: print e.headers @@ -112,7 +96,7 @@ class setup: if self.authenticated is True: try: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return self.createGenericTimeline(simplejson.load(self.opener.open(friendsTimelineURL))) + return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: if self.debug is True: print e.headers @@ -124,7 +108,7 @@ class setup: def getUserTimeline(self, **kwargs): userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) try: - return self.createGenericTimeline(simplejson.load(urllib2.urlopen(userTimelineURL))) + return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: if self.debug is True: print e.headers @@ -135,8 +119,7 @@ class setup: if self.authenticated is True: try: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - mentionsFeed = simplejson.load(self.opener.open(mentionsFeedURL)) - return self.createGenericTimeline(mentionsFeed) + return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: if self.debug is True: print e.headers @@ -147,13 +130,7 @@ class setup: def showStatus(self, id): try: - tweet = simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) - return {"created_at": tweet["created_at"], - "id": tweet["id"], - "text": tweet["text"], - "in_reply_to_status_id": tweet["in_reply_to_status_id"], - "in_reply_to_user_id": tweet["in_reply_to_user_id"], - "in_reply_to_screen_name": tweet["in_reply_to_screen_name"]} + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) except HTTPError, e: if self.debug is True: print e.headers @@ -163,7 +140,7 @@ class setup: def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: try: - self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id})) + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: if self.debug is True: print e.headers @@ -175,7 +152,7 @@ class setup: def destroyStatus(self, id): if self.authenticated is True: try: - self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST") + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) except HTTPError, e: if self.debug is True: print e.headers @@ -312,7 +289,7 @@ class setup: def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: - self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) except HTTPError, e: if self.debug is True: print e.headers @@ -323,7 +300,7 @@ class setup: def updateProfileColors(self, **kwargs): if self.authenticated is True: try: - self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError, e: if self.debug is True: print e.headers @@ -379,7 +356,7 @@ class setup: if updateProfileQueryString != "": try: - self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError, e: if self.debug is True: print e.headers @@ -391,8 +368,7 @@ class setup: def getFavorites(self, page = "1"): if self.authenticated is True: try: - favoritesTimeline = simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - return self.createGenericTimeline(favoritesTimeline) + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError, e: if self.debug: print e.headers @@ -403,7 +379,7 @@ class setup: def createFavorite(self, id): if self.authenticated is True: try: - self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "") + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError, e: if self.debug: print e.headers @@ -414,7 +390,7 @@ class setup: def destroyFavorite(self, id): if self.authenticated is True: try: - self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "") + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError, e: if self.debug: print e.headers @@ -432,7 +408,7 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name try: - self.opener.open(apiURL, "") + return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: if self.debug is True: print e.headers @@ -450,7 +426,7 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name try: - self.opener.open(apiURL, "") + return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: if self.debug is True: print e.headers @@ -491,7 +467,7 @@ class setup: def createBlock(self, id): if self.authenticated is True: try: - self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "") + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError, e: if self.debug is True: print e.headers @@ -502,7 +478,7 @@ class setup: def destroyBlock(self, id): if self.authenticated is True: try: - self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "") + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError, e: if self.debug is True: print e.headers @@ -550,20 +526,7 @@ class setup: def getSearchTimeline(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: - searchTimeline = simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) - # This one is custom built because the search feed is a bit different than the other feeds. - genericTimeline = [] - for tweet in searchTimeline["results"]: - genericTimeline.append({ - "created_at": tweet["created_at"], - "from_user": tweet["from_user"], - "from_user_id": tweet["from_user_id"], - "profile_image_url": tweet["profile_image_url"], - "id": tweet["id"], - "text": tweet["text"], - "to_user_id": tweet["to_user_id"] - }) - return genericTimeline + return simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) except HTTPError, e: if self.debug is True: print e.headers @@ -571,13 +534,7 @@ class setup: def getCurrentTrends(self): try: - # Returns an array of dictionary items containing the current trends - trendingTopicsURL = "http://search.twitter.com/trends.json" - trendingTopics = simplejson.load(urllib.urlopen(trendingTopicsURL)) - trendingTopicsArray = [] - for topic in trendingTopics['trends']: - trendingTopicsArray.append({"name" : topic['name'], "url" : topic['url']}) - return trendingTopicsArray + return simplejson.load(urllib.urlopen("http://search.twitter.com/trends.json")) except HTTPError, e: if self.debug is True: print e.headers @@ -586,15 +543,17 @@ class setup: # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): if self.authenticated is True: - #try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - #except: - #print "Oh god, this failed so horribly." + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateProfileBackgroundImage() failed with a " + str(e.code) + " error code." else: print "You realize you need to be authenticated to change a background image, right?" @@ -607,8 +566,10 @@ class setup: headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() - except: - print "Oh god, this failed so horribly." + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateProfileImage() failed with a " + str(e.code) + " error code." else: print "You realize you need to be authenticated to change a profile image, right?" From 9e1159dd6f297b4b0c0e735e8a932b5c7f03fc41 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 8 Jun 2009 02:23:21 -0400 Subject: [PATCH 031/687] Search API Methods, trending and regular search stuff. Cleaned up some more parts of the library. --- tango.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/tango.py b/tango.py index 99ca13c..9f0f760 100644 --- a/tango.py +++ b/tango.py @@ -5,12 +5,12 @@ Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. - TODO: Blocks API Wrapper, Direct Messages API Wrapper, Friendship API Wrapper, OAuth, Streaming API + TODO: Streaming API? Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes, mimetools, pprint +import httplib, urllib, urllib2, mimetypes, mimetools from urllib2 import HTTPError @@ -523,7 +523,7 @@ class setup: else: print "getBlockedIDs() requires you to be authenticated." - def getSearchTimeline(self, search_query, optional_page): + def searchTwitter(self, search_query, optional_page): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: return simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) @@ -532,14 +532,53 @@ class setup: print e.headers print "getSearchTimeline() failed with a " + str(e.code) + " error code." - def getCurrentTrends(self): + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen("http://search.twitter.com/trends.json")) + return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: if self.debug is True: print e.headers print "getCurrentTrends() failed with a " + str(e.code) + " error code." + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getDailyTrends() failed with a " + str(e.code) + " error code." + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getWeeklyTrends() failed with a " + str(e.code) + " error code." + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): if self.authenticated is True: From 90d7e5ffb00b70e8805834161f8ce1344a116b15 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 8 Jun 2009 02:54:53 -0400 Subject: [PATCH 032/687] The final API method, Saved Searches. Fixed an issue with the Direct Message function where urllib.urlencode wasn't formed properly; only thing left for release is OAuth. --- tango.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tango.py b/tango.py index 9f0f760..dc9e674 100644 --- a/tango.py +++ b/tango.py @@ -216,7 +216,7 @@ class setup: if self.authenticated is True: if len(list(text)) < 140: try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text" text})) + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) except HTTPError, e: if self.debug is True: print e.headers @@ -579,6 +579,50 @@ class setup: print e.headers print "getWeeklyTrends() failed with a " + str(e.code) + " error code." + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "getSavedSearches() failed with a " + str(e.code) + " error code." + else: + print "getSavedSearches() requires you to be authenticated." + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "showSavedSearch() failed with a " + str(e.code) + " error code." + else: + print "showSavedSearch() requires you to be authenticated." + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "createSavedSearch() failed with a " + str(e.code) + " error code." + else: + print "createSavedSearch() requires you to be authenticated." + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + if self.debug is True: + print e.headers + print "destroySavedSearch() failed with a " + str(e.code) + " error code." + else: + print "destroySavedSearch() requires you to be authenticated." + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): if self.authenticated is True: From 55b28c9184fbe95bcc5b57791770689a065c13f7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 13 Jun 2009 21:00:58 -0400 Subject: [PATCH 033/687] Examples of various Tango scripts --- tango_examples/search_results.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tango_examples/search_results.py diff --git a/tango_examples/search_results.py b/tango_examples/search_results.py new file mode 100644 index 0000000..97b03a5 --- /dev/null +++ b/tango_examples/search_results.py @@ -0,0 +1,8 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +search_results = twitter.searchTwitter("WebsDotCom", "2") + +for tweet in search_results["results"]: + print tweet["text"] From 2fbb99c38d3cfd8122af7b50b39825c71a52ebc8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 13 Jun 2009 21:34:44 -0400 Subject: [PATCH 034/687] Current Trends example - very basic --- tango_examples/current_trends.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tango_examples/current_trends.py diff --git a/tango_examples/current_trends.py b/tango_examples/current_trends.py new file mode 100644 index 0000000..94f1454 --- /dev/null +++ b/tango_examples/current_trends.py @@ -0,0 +1,8 @@ +import tango +import pprint + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getCurrentTrends()["trends"] + +print trends From b49cc2458daf496e2763e1c8189e05130c0bab37 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 13 Jun 2009 21:42:53 -0400 Subject: [PATCH 035/687] More examples for the Trends/Search API --- tango_examples/current_trends.py | 3 +-- tango_examples/daily_trends.py | 7 +++++++ tango_examples/weekly_trends.py | 7 +++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 tango_examples/daily_trends.py create mode 100644 tango_examples/weekly_trends.py diff --git a/tango_examples/current_trends.py b/tango_examples/current_trends.py index 94f1454..dd2d50d 100644 --- a/tango_examples/current_trends.py +++ b/tango_examples/current_trends.py @@ -1,8 +1,7 @@ import tango -import pprint """ Instantiate Tango with no Authentication """ twitter = tango.setup() -trends = twitter.getCurrentTrends()["trends"] +trends = twitter.getCurrentTrends() print trends diff --git a/tango_examples/daily_trends.py b/tango_examples/daily_trends.py new file mode 100644 index 0000000..28bdde1 --- /dev/null +++ b/tango_examples/daily_trends.py @@ -0,0 +1,7 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getDailyTrends() + +print trends diff --git a/tango_examples/weekly_trends.py b/tango_examples/weekly_trends.py new file mode 100644 index 0000000..fd9b564 --- /dev/null +++ b/tango_examples/weekly_trends.py @@ -0,0 +1,7 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getWeeklyTrends() + +print trends From e697d7206d41a1e1d2c2dcf668ce50ebc6cd9e73 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 04:07:51 -0400 Subject: [PATCH 036/687] An example showing the various ways to instantiate tango. --- tango_examples/tango_setup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tango_examples/tango_setup.py diff --git a/tango_examples/tango_setup.py b/tango_examples/tango_setup.py new file mode 100644 index 0000000..56d2429 --- /dev/null +++ b/tango_examples/tango_setup.py @@ -0,0 +1,11 @@ +import tango + +# Using no authentication and specifying Debug +twitter = tango.setup(debug=True) + +# Using Basic Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") + +# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) +auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} +twitter = tango.setup(username="example", password="example", oauth_keys=auth_keys) From b2ba65cc456fed2d0413b2759d10190e61877a58 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 04:09:27 -0400 Subject: [PATCH 037/687] An example showing how to update a status using basic authentication. --- tango_examples/update_status.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tango_examples/update_status.py diff --git a/tango_examples/update_status.py b/tango_examples/update_status.py new file mode 100644 index 0000000..1466752 --- /dev/null +++ b/tango_examples/update_status.py @@ -0,0 +1,5 @@ +import tango + +# Create a Tango instance using Basic (HTTP) Authentication and update our Status +twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter.updateStatus("See how easy this was?") From 480ba230118dad628298df1d605600f6a018d7d4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 18:42:35 -0400 Subject: [PATCH 038/687] A basic example on using Tango for URL shortening --- tango_examples/shorten_url.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tango_examples/shorten_url.py diff --git a/tango_examples/shorten_url.py b/tango_examples/shorten_url.py new file mode 100644 index 0000000..12b7668 --- /dev/null +++ b/tango_examples/shorten_url.py @@ -0,0 +1,7 @@ +import tango + +# Shortening URLs requires no authentication, huzzah +twitter = tango.setup() +shortURL = twitter.shortenURL("http://www.webs.com/") + +print shortURL From 4af7c8771f81892cc671511c33b8997d1f3caba8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 19:09:04 -0400 Subject: [PATCH 039/687] Examples of getting the public timeline --- tango_examples/public_timeline.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tango_examples/public_timeline.py diff --git a/tango_examples/public_timeline.py b/tango_examples/public_timeline.py new file mode 100644 index 0000000..4a6c161 --- /dev/null +++ b/tango_examples/public_timeline.py @@ -0,0 +1,8 @@ +import tango + +# Getting the public timeline requires no authentication, huzzah +twitter = tango.setup() +public_timeline = twitter.getPublicTimeline() + +for tweet in public_timeline: + print tweet["text"] From b71dd218aaeaf7fdbe09bbcd5fb4cc7924afd43e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 19:31:24 -0400 Subject: [PATCH 040/687] Example of getting the friends timeline --- tango_examples/get_friends_timeline.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tango_examples/get_friends_timeline.py diff --git a/tango_examples/get_friends_timeline.py b/tango_examples/get_friends_timeline.py new file mode 100644 index 0000000..e3c3ddd --- /dev/null +++ b/tango_examples/get_friends_timeline.py @@ -0,0 +1,8 @@ +import tango, pprint + +# Authenticate using Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") +friends_timeline = twitter.getFriendsTimeline(count="150", page="3") + +for tweet in friends_timeline: + print tweet["text"] From 63cf9dba3a4c0b701f0813018ce5b7598de17ca7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 20:14:32 -0400 Subject: [PATCH 041/687] Fixed getUserTimeline(), as it was... way out of whack. Should now properly detect which method of identification is needed for the userTimeline scenario. --- tango.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tango.py b/tango.py index dc9e674..958f877 100644 --- a/tango.py +++ b/tango.py @@ -105,15 +105,23 @@ class setup: print "getFriendsTimeline() requires you to be authenticated." pass - def getUserTimeline(self, **kwargs): - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + def getUserTimeline(self, id = None, **kwargs): + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) try: - return simplejson.load(urllib2.urlopen(userTimelineURL)) + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: if self.debug is True: print e.headers print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - pass def getUserMentions(self, **kwargs): if self.authenticated is True: @@ -523,7 +531,7 @@ class setup: else: print "getBlockedIDs() requires you to be authenticated." - def searchTwitter(self, search_query, optional_page): + def searchTwitter(self, search_query, optional_page = "1"): params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* try: return simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) From 3493047180f276993f91ef1a80b7358f7ec6f2c5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 20:15:54 -0400 Subject: [PATCH 042/687] Examples of user timelines --- tango_examples/get_user_timeline.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tango_examples/get_user_timeline.py diff --git a/tango_examples/get_user_timeline.py b/tango_examples/get_user_timeline.py new file mode 100644 index 0000000..a81c4c1 --- /dev/null +++ b/tango_examples/get_user_timeline.py @@ -0,0 +1,7 @@ +import tango + +# We won't authenticate for this, but sometimes it's necessary +twitter = tango.setup() +user_timeline = twitter.getUserTimeline(id=None, screen_name="ryanmcgrath") + +print user_timeline From f0a27970f15151b233ce1fcea40338846c5298a1 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 20:17:48 -0400 Subject: [PATCH 043/687] Removed needless id parameter --- tango_examples/get_user_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tango_examples/get_user_timeline.py b/tango_examples/get_user_timeline.py index a81c4c1..aa7bf97 100644 --- a/tango_examples/get_user_timeline.py +++ b/tango_examples/get_user_timeline.py @@ -2,6 +2,6 @@ import tango # We won't authenticate for this, but sometimes it's necessary twitter = tango.setup() -user_timeline = twitter.getUserTimeline(id=None, screen_name="ryanmcgrath") +user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") print user_timeline From d85e3dbf2867b5be0b7f4923b942888bd0e1b2b4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 14 Jun 2009 20:23:52 -0400 Subject: [PATCH 044/687] Example of getting user mentions --- tango_examples/get_user_mention.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tango_examples/get_user_mention.py diff --git a/tango_examples/get_user_mention.py b/tango_examples/get_user_mention.py new file mode 100644 index 0000000..6b94371 --- /dev/null +++ b/tango_examples/get_user_mention.py @@ -0,0 +1,6 @@ +import tango + +twitter = tango.setup(authtype="Basic", username="example", password="example") +mentions = twitter.getUserMentions(count="150") + +print mentions From d9743218541463754e15cfb0ff2886e2df2da0a3 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 18 Jun 2009 03:56:41 -0400 Subject: [PATCH 045/687] Finally got around to fixing the searchTwitter() function. It's now able to accept any and all parameters that correspond with Twitter's API; the final thing there should be allowing a user agent to be set, which still needs to be looked into. Shouldn't be too difficult. --- tango.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tango.py b/tango.py index 958f877..bf28d3d 100644 --- a/tango.py +++ b/tango.py @@ -64,9 +64,11 @@ class setup: print e.headers print "shortenURL() failed with a " + str(e.code) + " error code." - def constructApiURL(self, base_url, params): + def constructApiURL(self, base_url, params, **kwargs): queryURL = base_url questionMarkUsed = False + if kwargs.has_key("questionMarkUsed") is True: + questionMarkUsed = True for param in params: if params[param] is not None: queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) @@ -531,10 +533,12 @@ class setup: else: print "getBlockedIDs() requires you to be authenticated." - def searchTwitter(self, search_query, optional_page = "1"): - params = urllib.urlencode({'q': search_query, 'rpp': optional_page}) # Doesn't hurt to do pages this way. *shrug* + def searchTwitter(self, search_query, **kwargs): + baseURL = "http://search.twitter.com/search.json?" + urllib.urlencode({"q": search_query}) + searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) + print searchURL try: - return simplejson.load(urllib2.urlopen("http://search.twitter.com/search.json", params)) + return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: if self.debug is True: print e.headers From 102eac4d6ea370a0ecad26419e60163870bf640d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 20 Jun 2009 01:42:23 -0400 Subject: [PATCH 046/687] Fixed length checking in updateStatus(), removed debug lines from searchTwitter() --- tango.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tango.py b/tango.py index bf28d3d..fcc4eeb 100644 --- a/tango.py +++ b/tango.py @@ -148,6 +148,8 @@ class setup: pass def updateStatus(self, status, in_reply_to_status_id = None): + if len(list(status)) > 140: + print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) @@ -536,7 +538,6 @@ class setup: def searchTwitter(self, search_query, **kwargs): baseURL = "http://search.twitter.com/search.json?" + urllib.urlencode({"q": search_query}) searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) - print searchURL try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: From 5f63187ca2edb5479a007d252870b7dac9220dfb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 01:24:49 -0400 Subject: [PATCH 047/687] Simple example of updating a profile image --- tango_examples/update_profile_image.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tango_examples/update_profile_image.py diff --git a/tango_examples/update_profile_image.py b/tango_examples/update_profile_image.py new file mode 100644 index 0000000..a6f52b2 --- /dev/null +++ b/tango_examples/update_profile_image.py @@ -0,0 +1,5 @@ +import tango + +# Instantiate Tango with Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter.updateProfileImage("myImage.png") From 174e0bc0453b85fa42410cc150b7fcc3bdb488e8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 01:49:00 -0400 Subject: [PATCH 048/687] New README --- README | 48 ++++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/README b/README index 2b8c86d..3624831 100644 --- a/README +++ b/README @@ -1,50 +1,38 @@ -Tango - Easy Twitter utilities for Django-based applications +Tango - Easy Twitter utilities in Python ----------------------------------------------------------------------------------------------------- -As a Django user, I've often wanted a way to easily implement Twitter scraping calls in my applications. -I could write them from scratch using the already existing (and quite excellent) library called -"Python-Twitter" (http://code.google.com/p/python-twitter/)... +I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain +things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at +a library that offers more coverage. -However, Python-Twitter doesn't (currently) handle some things well, such as using Twitter's -Search API. I've found myself wanting a largely drop-in solution for this (and other) -problems... +This is my first library I've ever written in Python, so there could be some stuff in here that'll +make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, +and I'm open to anything that'll improve the library as a whole. -Thus, we now have Tango. +OAuth support is in the works, but every other part of the Twitter API should be covered. Tango +handles both Baisc (HTTP) Authentication and OAuth, and OAuth is the default method for +Authentication. To override this, specify 'authtype="Basic"' in your tango.setup() call. +Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All +parameters for API calls should translate over as function arguments. -Installation +Requirements ----------------------------------------------------------------------------------------------------- -You can install this like any other Django-app; just throw it in as a new app, register it in your -settings as an "Installed App", and sync the models. - Tango requires (much like Python-Twitter, because they had the right idea :D) a library called -"simplejson" - Django should include this by default, but if you need the library for any reason, -you can grab it at the following link: +"simplejson". You can grab it at the following link: http://pypi.python.org/pypi/simplejson -Tango also works well as a standalone Python library, so feel free to use it however you like. - Example Use ----------------------------------------------------------------------------------------------------- -An extremely generic, and somewhat useless, demo is below. Instantiate your class by passing in a -Twitter username, then all functions will come off of that. Results are returned as a list. +import tango -testList = tango.setup(username="ryanmcgrath") -newTestList = testList.getSearchTimeline("b", "20") -for testTweet in newTestList: - print testTweet +twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter.updateStatus("See how easy this was?") Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- -I want to note that Tango is *not* like other Twitter libraries - we don't handle authentication or -anything of the sort (yet); there are already battle-tested solutions out there for both Basic Auth and -OAuth. This isn't production ready quite yet. - -Tango will (hopefully) be compatible with Python 3; as it stands, I think it might be now, I've just -not had the time to check over it. - -My hope is that Tango is so plug-and-play that you'd never *have* to ask any questions, but if +My hope is that Tango is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. From ea7b2e5cdc8972f986a6ba802b1a3daf06c8d6bb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 01:51:17 -0400 Subject: [PATCH 049/687] Fixing spacing --- README | 1 + 1 file changed, 1 insertion(+) diff --git a/README b/README index 3624831..6469bfc 100644 --- a/README +++ b/README @@ -15,6 +15,7 @@ Authentication. To override this, specify 'authtype="Basic"' in your tango.setup Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All parameters for API calls should translate over as function arguments. + Requirements ----------------------------------------------------------------------------------------------------- Tango requires (much like Python-Twitter, because they had the right idea :D) a library called From be6cc881f0e8f2b25ab6b74c5e4150013ba1e1b5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:11:37 -0400 Subject: [PATCH 050/687] Added a small note... --- tango.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tango.py b/tango.py index fcc4eeb..63600c3 100644 --- a/tango.py +++ b/tango.py @@ -5,7 +5,7 @@ Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. - TODO: Streaming API? + TODO: OAuth, Streaming API? Questions, comments? ryan@venodesigns.net """ From e8660df23c5f9488bae8b571ac15844a4866d4ec Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:12:00 -0400 Subject: [PATCH 051/687] An experimental version of Tango for Python 3k --- tango_3k.py | 694 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 694 insertions(+) create mode 100644 tango_3k.py diff --git a/tango_3k.py b/tango_3k.py new file mode 100644 index 0000000..32a1a6d --- /dev/null +++ b/tango_3k.py @@ -0,0 +1,694 @@ +#!/usr/bin/python + +""" + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools + +from urllib.error import HTTPError + +try: + import simplejson +except ImportError: + print("Tango requires the simplejson library to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + print("Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py") + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + self.debug = debug + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + pass + elif self.authtype == "Basic": + self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib.request.build_opener(self.handler) + self.authenticated = True + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("shortenURL() failed with a " + str(e.code) + " error code.") + + def constructApiURL(self, base_url, params, **kwargs): + queryURL = base_url + questionMarkUsed = False + if ("questionMarkUsed" in kwargs) is True: + questionMarkUsed = True + for param in params: + if params[param] is not None: + queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) + questionMarkUsed = True + return queryURL + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?") + + def getPublicTimeline(self): + try: + return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getPublicTimeline() failed with a " + str(e.code) + " error code.") + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFriendsTimeline() failed with a " + str(e.code) + " error code.") + else: + print("getFriendsTimeline() requires you to be authenticated.") + pass + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib.request.urlopen(userTimelineURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getUserMentions() failed with a " + str(e.code) + " error code.") + else: + print("getUserMentions() requires you to be authenticated.") + pass + + def showStatus(self, id): + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") + pass + + def updateStatus(self, status, in_reply_to_status_id = None): + if len(list(status)) > 140: + print("This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!") + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateStatus() failed with a " + str(e.code) + " error code.") + else: + print("updateStatus() requires you to be authenticated.") + pass + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyStatus() failed with a " + str(e.code) + " error code.") + else: + print("destroyStatus() requires you to be authenticated.") + pass + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("endSession failed with a " + str(e.code) + " error code.") + else: + print("You can't end a session when you're not authenticated to begin with.") + pass + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getDirectMessages() failed with a " + str(e.code) + " error code.") + else: + print("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSentMessages() failed with a " + str(e.code) + " error code.") + else: + print("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("sendDirectMessage() failed with a " + str(e.code) + " error code.") + else: + print("Your message must be longer than 140 characters") + else: + print("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyDirectMessage() failed with a " + str(e.code) + " error code.") + else: + print("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createFriendship() failed with a " + str(e.code) + " error code. ") + if e.code == 403: + print("It seems you've hit the update limit for this method. Try again in 24 hours.") + else: + print("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyFriendship() failed with a " + str(e.code) + " error code.") + else: + print("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("checkIfFriendshipExists() failed with a " + str(e.code) + " error code.") + else: + print("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateDeliveryDevice() failed with a " + str(e.code) + " error code.") + else: + print("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileColors() failed with a " + str(e.code) + " error code.") + else: + print("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + print("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + print("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.parse.urlencode({"url": url}) + useAmpersands = True + else: + print("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.parse.urlencode({"location": location}) + useAmpersands = True + else: + print("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.parse.urlencode({"description": description}) + else: + print("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfile() failed with a " + e.code + " error code.") + else: + # If they're not authenticated + print("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError as e: + if self.debug: + print(e.headers) + print("getFavorites() failed with a " + str(e.code) + " error code.") + else: + print("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError as e: + if self.debug: + print(e.headers) + print("createFavorite() failed with a " + str(e.code) + " error code.") + else: + print("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug: + print(e.headers) + print("destroyFavorite() failed with a " + str(e.code) + " error code.") + else: + print("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("notificationFollow() failed with a " + str(e.code) + " error code.") + else: + print("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("notificationLeave() failed with a " + str(e.code) + " error code.") + else: + print("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFriendsIDs() failed with a " + str(e.code) + " error code.") + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFollowersIDs() failed with a " + str(e.code) + " error code.") + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createBlock() failed with a " + str(e.code) + " error code.") + else: + print("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyBlock() failed with a " + str(e.code) + " error code.") + else: + print("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("checkIfBlockExists() failed with a " + str(e.code) + " error code.") + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getBlocking() failed with a " + str(e.code) + " error code.") + else: + print("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getBlockedIDs() failed with a " + str(e.code) + " error code.") + else: + print("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + baseURL = "http://search.twitter.com/search.json?" + urllib.parse.urlencode({"q": search_query}) + searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) + try: + return simplejson.load(urllib.request.urlopen(searchURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSearchTimeline() failed with a " + str(e.code) + " error code.") + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getCurrentTrends() failed with a " + str(e.code) + " error code.") + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getDailyTrends() failed with a " + str(e.code) + " error code.") + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getWeeklyTrends() failed with a " + str(e.code) + " error code.") + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSavedSearches() failed with a " + str(e.code) + " error code.") + else: + print("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("showSavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createSavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroySavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileBackgroundImage() failed with a " + str(e.code) + " error code.") + else: + print("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileImage() failed with a " + str(e.code) + " error code.") + else: + print("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From f915891b1b94713de84dee5ce722039c63c442a8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:14:33 -0400 Subject: [PATCH 052/687] A note about Tango3k --- README | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README b/README index 6469bfc..472884b 100644 --- a/README +++ b/README @@ -32,6 +32,13 @@ twitter = tango.setup(authtype="Basic", username="example", password="example") twitter.updateStatus("See how easy this was?") +Tango 3k +----------------------------------------------------------------------------------------------------- +There's an experimental version of Tango that's made for Python 3k. This is currently not guaranteed +to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, +be aware of this. + + Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- My hope is that Tango is so simple that you'd never *have* to ask any questions, but if From d8563c0a6df2b381d8433b97dd9115a37f97115e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:16:10 -0400 Subject: [PATCH 053/687] Experimental version of Tango for Python 3k --- tango3k.py | 694 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 694 insertions(+) create mode 100644 tango3k.py diff --git a/tango3k.py b/tango3k.py new file mode 100644 index 0000000..32a1a6d --- /dev/null +++ b/tango3k.py @@ -0,0 +1,694 @@ +#!/usr/bin/python + +""" + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools + +from urllib.error import HTTPError + +try: + import simplejson +except ImportError: + print("Tango requires the simplejson library to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + print("Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py") + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + self.debug = debug + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + pass + elif self.authtype == "Basic": + self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib.request.build_opener(self.handler) + self.authenticated = True + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("shortenURL() failed with a " + str(e.code) + " error code.") + + def constructApiURL(self, base_url, params, **kwargs): + queryURL = base_url + questionMarkUsed = False + if ("questionMarkUsed" in kwargs) is True: + questionMarkUsed = True + for param in params: + if params[param] is not None: + queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) + questionMarkUsed = True + return queryURL + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?") + + def getPublicTimeline(self): + try: + return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getPublicTimeline() failed with a " + str(e.code) + " error code.") + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFriendsTimeline() failed with a " + str(e.code) + " error code.") + else: + print("getFriendsTimeline() requires you to be authenticated.") + pass + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib.request.urlopen(userTimelineURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getUserMentions() failed with a " + str(e.code) + " error code.") + else: + print("getUserMentions() requires you to be authenticated.") + pass + + def showStatus(self, id): + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") + pass + + def updateStatus(self, status, in_reply_to_status_id = None): + if len(list(status)) > 140: + print("This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!") + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateStatus() failed with a " + str(e.code) + " error code.") + else: + print("updateStatus() requires you to be authenticated.") + pass + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyStatus() failed with a " + str(e.code) + " error code.") + else: + print("destroyStatus() requires you to be authenticated.") + pass + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("endSession failed with a " + str(e.code) + " error code.") + else: + print("You can't end a session when you're not authenticated to begin with.") + pass + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getDirectMessages() failed with a " + str(e.code) + " error code.") + else: + print("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSentMessages() failed with a " + str(e.code) + " error code.") + else: + print("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("sendDirectMessage() failed with a " + str(e.code) + " error code.") + else: + print("Your message must be longer than 140 characters") + else: + print("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyDirectMessage() failed with a " + str(e.code) + " error code.") + else: + print("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createFriendship() failed with a " + str(e.code) + " error code. ") + if e.code == 403: + print("It seems you've hit the update limit for this method. Try again in 24 hours.") + else: + print("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyFriendship() failed with a " + str(e.code) + " error code.") + else: + print("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("checkIfFriendshipExists() failed with a " + str(e.code) + " error code.") + else: + print("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateDeliveryDevice() failed with a " + str(e.code) + " error code.") + else: + print("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileColors() failed with a " + str(e.code) + " error code.") + else: + print("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + print("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + print("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.parse.urlencode({"url": url}) + useAmpersands = True + else: + print("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.parse.urlencode({"location": location}) + useAmpersands = True + else: + print("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.parse.urlencode({"description": description}) + else: + print("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfile() failed with a " + e.code + " error code.") + else: + # If they're not authenticated + print("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError as e: + if self.debug: + print(e.headers) + print("getFavorites() failed with a " + str(e.code) + " error code.") + else: + print("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError as e: + if self.debug: + print(e.headers) + print("createFavorite() failed with a " + str(e.code) + " error code.") + else: + print("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug: + print(e.headers) + print("destroyFavorite() failed with a " + str(e.code) + " error code.") + else: + print("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("notificationFollow() failed with a " + str(e.code) + " error code.") + else: + print("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("notificationLeave() failed with a " + str(e.code) + " error code.") + else: + print("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFriendsIDs() failed with a " + str(e.code) + " error code.") + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getFollowersIDs() failed with a " + str(e.code) + " error code.") + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createBlock() failed with a " + str(e.code) + " error code.") + else: + print("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroyBlock() failed with a " + str(e.code) + " error code.") + else: + print("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("checkIfBlockExists() failed with a " + str(e.code) + " error code.") + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getBlocking() failed with a " + str(e.code) + " error code.") + else: + print("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getBlockedIDs() failed with a " + str(e.code) + " error code.") + else: + print("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + baseURL = "http://search.twitter.com/search.json?" + urllib.parse.urlencode({"q": search_query}) + searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) + try: + return simplejson.load(urllib.request.urlopen(searchURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSearchTimeline() failed with a " + str(e.code) + " error code.") + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getCurrentTrends() failed with a " + str(e.code) + " error code.") + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getDailyTrends() failed with a " + str(e.code) + " error code.") + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getWeeklyTrends() failed with a " + str(e.code) + " error code.") + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("getSavedSearches() failed with a " + str(e.code) + " error code.") + else: + print("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("showSavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("createSavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("destroySavedSearch() failed with a " + str(e.code) + " error code.") + else: + print("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileBackgroundImage() failed with a " + str(e.code) + " error code.") + else: + print("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("updateProfileImage() failed with a " + str(e.code) + " error code.") + else: + print("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From 0de4dc738374ba501e45e819f5f5a2ac9cf1d409 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:16:45 -0400 Subject: [PATCH 054/687] Removing bad filename --- tango_3k.py | 694 ---------------------------------------------------- 1 file changed, 694 deletions(-) delete mode 100644 tango_3k.py diff --git a/tango_3k.py b/tango_3k.py deleted file mode 100644 index 32a1a6d..0000000 --- a/tango_3k.py +++ /dev/null @@ -1,694 +0,0 @@ -#!/usr/bin/python - -""" - Tango is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools - -from urllib.error import HTTPError - -try: - import simplejson -except ImportError: - print("Tango requires the simplejson library to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - print("Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py") - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - self.debug = debug - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - pass - elif self.authtype == "Basic": - self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib.request.build_opener(self.handler) - self.authenticated = True - - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass - - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("shortenURL() failed with a " + str(e.code) + " error code.") - - def constructApiURL(self, base_url, params, **kwargs): - queryURL = base_url - questionMarkUsed = False - if ("questionMarkUsed" in kwargs) is True: - questionMarkUsed = True - for param in params: - if params[param] is not None: - queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) - questionMarkUsed = True - return queryURL - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?") - - def getPublicTimeline(self): - try: - return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getPublicTimeline() failed with a " + str(e.code) + " error code.") - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFriendsTimeline() failed with a " + str(e.code) + " error code.") - else: - print("getFriendsTimeline() requires you to be authenticated.") - pass - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib.request.urlopen(userTimelineURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getUserMentions() failed with a " + str(e.code) + " error code.") - else: - print("getUserMentions() requires you to be authenticated.") - pass - - def showStatus(self, id): - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") - pass - - def updateStatus(self, status, in_reply_to_status_id = None): - if len(list(status)) > 140: - print("This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!") - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateStatus() failed with a " + str(e.code) + " error code.") - else: - print("updateStatus() requires you to be authenticated.") - pass - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyStatus() failed with a " + str(e.code) + " error code.") - else: - print("destroyStatus() requires you to be authenticated.") - pass - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("endSession failed with a " + str(e.code) + " error code.") - else: - print("You can't end a session when you're not authenticated to begin with.") - pass - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getDirectMessages() failed with a " + str(e.code) + " error code.") - else: - print("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSentMessages() failed with a " + str(e.code) + " error code.") - else: - print("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("sendDirectMessage() failed with a " + str(e.code) + " error code.") - else: - print("Your message must be longer than 140 characters") - else: - print("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyDirectMessage() failed with a " + str(e.code) + " error code.") - else: - print("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createFriendship() failed with a " + str(e.code) + " error code. ") - if e.code == 403: - print("It seems you've hit the update limit for this method. Try again in 24 hours.") - else: - print("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyFriendship() failed with a " + str(e.code) + " error code.") - else: - print("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("checkIfFriendshipExists() failed with a " + str(e.code) + " error code.") - else: - print("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateDeliveryDevice() failed with a " + str(e.code) + " error code.") - else: - print("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileColors() failed with a " + str(e.code) + " error code.") - else: - print("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - print("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - print("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.parse.urlencode({"url": url}) - useAmpersands = True - else: - print("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.parse.urlencode({"location": location}) - useAmpersands = True - else: - print("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.parse.urlencode({"description": description}) - else: - print("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfile() failed with a " + e.code + " error code.") - else: - # If they're not authenticated - print("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError as e: - if self.debug: - print(e.headers) - print("getFavorites() failed with a " + str(e.code) + " error code.") - else: - print("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError as e: - if self.debug: - print(e.headers) - print("createFavorite() failed with a " + str(e.code) + " error code.") - else: - print("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError as e: - if self.debug: - print(e.headers) - print("destroyFavorite() failed with a " + str(e.code) + " error code.") - else: - print("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("notificationFollow() failed with a " + str(e.code) + " error code.") - else: - print("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("notificationLeave() failed with a " + str(e.code) + " error code.") - else: - print("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFriendsIDs() failed with a " + str(e.code) + " error code.") - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFollowersIDs() failed with a " + str(e.code) + " error code.") - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createBlock() failed with a " + str(e.code) + " error code.") - else: - print("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyBlock() failed with a " + str(e.code) + " error code.") - else: - print("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("checkIfBlockExists() failed with a " + str(e.code) + " error code.") - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getBlocking() failed with a " + str(e.code) + " error code.") - else: - print("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getBlockedIDs() failed with a " + str(e.code) + " error code.") - else: - print("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - baseURL = "http://search.twitter.com/search.json?" + urllib.parse.urlencode({"q": search_query}) - searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) - try: - return simplejson.load(urllib.request.urlopen(searchURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSearchTimeline() failed with a " + str(e.code) + " error code.") - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getCurrentTrends() failed with a " + str(e.code) + " error code.") - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getDailyTrends() failed with a " + str(e.code) + " error code.") - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getWeeklyTrends() failed with a " + str(e.code) + " error code.") - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSavedSearches() failed with a " + str(e.code) + " error code.") - else: - print("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("showSavedSearch() failed with a " + str(e.code) + " error code.") - else: - print("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createSavedSearch() failed with a " + str(e.code) + " error code.") - else: - print("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroySavedSearch() failed with a " + str(e.code) + " error code.") - else: - print("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileBackgroundImage() failed with a " + str(e.code) + " error code.") - else: - print("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileImage() failed with a " + str(e.code) + " error code.") - else: - print("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From 3f049a79bbda57a015d81533fedbc2f0ebaedb92 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Jun 2009 02:17:06 -0400 Subject: [PATCH 055/687] Experimental version of Tango for Python 3k --- tango3k.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tango3k.py b/tango3k.py index 32a1a6d..cee3d6d 100644 --- a/tango3k.py +++ b/tango3k.py @@ -7,6 +7,8 @@ TODO: OAuth, Streaming API? + NOTE: THIS IS EXPERIMENTAL. + Questions, comments? ryan@venodesigns.net """ From f52e1415b81044d9041a25928dc86b910155ff49 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 27 Jun 2009 01:52:18 -0400 Subject: [PATCH 056/687] Removing useless pass things --- tango.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tango.py b/tango.py index 63600c3..5316e00 100644 --- a/tango.py +++ b/tango.py @@ -105,7 +105,6 @@ class setup: print "getFriendsTimeline() failed with a " + str(e.code) + " error code." else: print "getFriendsTimeline() requires you to be authenticated." - pass def getUserTimeline(self, id = None, **kwargs): if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: @@ -136,7 +135,6 @@ class setup: print "getUserMentions() failed with a " + str(e.code) + " error code." else: print "getUserMentions() requires you to be authenticated." - pass def showStatus(self, id): try: @@ -145,7 +143,6 @@ class setup: if self.debug is True: print e.headers print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - pass def updateStatus(self, status, in_reply_to_status_id = None): if len(list(status)) > 140: @@ -159,7 +156,6 @@ class setup: print "updateStatus() failed with a " + str(e.code) + " error code." else: print "updateStatus() requires you to be authenticated." - pass def destroyStatus(self, id): if self.authenticated is True: @@ -171,7 +167,6 @@ class setup: print "destroyStatus() failed with a " + str(e.code) + " error code." else: print "destroyStatus() requires you to be authenticated." - pass def endSession(self): if self.authenticated is True: @@ -184,7 +179,6 @@ class setup: print "endSession failed with a " + str(e.code) + " error code." else: print "You can't end a session when you're not authenticated to begin with." - pass def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: From 860563b9f944947a7c72f5db5b3cd9b62045ccec Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 27 Jun 2009 01:53:04 -0400 Subject: [PATCH 057/687] Removing useless pass methods --- tango3k.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tango3k.py b/tango3k.py index cee3d6d..addb42e 100644 --- a/tango3k.py +++ b/tango3k.py @@ -107,7 +107,6 @@ class setup: print("getFriendsTimeline() failed with a " + str(e.code) + " error code.") else: print("getFriendsTimeline() requires you to be authenticated.") - pass def getUserTimeline(self, id = None, **kwargs): if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: @@ -138,7 +137,6 @@ class setup: print("getUserMentions() failed with a " + str(e.code) + " error code.") else: print("getUserMentions() requires you to be authenticated.") - pass def showStatus(self, id): try: @@ -147,7 +145,6 @@ class setup: if self.debug is True: print(e.headers) print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") - pass def updateStatus(self, status, in_reply_to_status_id = None): if len(list(status)) > 140: @@ -161,7 +158,6 @@ class setup: print("updateStatus() failed with a " + str(e.code) + " error code.") else: print("updateStatus() requires you to be authenticated.") - pass def destroyStatus(self, id): if self.authenticated is True: @@ -173,7 +169,6 @@ class setup: print("destroyStatus() failed with a " + str(e.code) + " error code.") else: print("destroyStatus() requires you to be authenticated.") - pass def endSession(self): if self.authenticated is True: @@ -186,7 +181,6 @@ class setup: print("endSession failed with a " + str(e.code) + " error code.") else: print("You can't end a session when you're not authenticated to begin with.") - pass def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: From ecee9a438a20c89ca446e0670566cb7ec0be6553 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 1 Jul 2009 05:16:22 -0400 Subject: [PATCH 058/687] Example for checking rate limits --- tango_examples/rate_limit.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tango_examples/rate_limit.py diff --git a/tango_examples/rate_limit.py b/tango_examples/rate_limit.py new file mode 100644 index 0000000..ae85973 --- /dev/null +++ b/tango_examples/rate_limit.py @@ -0,0 +1,10 @@ +import tango + +# Instantiate with Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") + +# This returns the rate limit for the requesting IP +rateLimit = twitter.getRateLimitStatus() + +# This returns the rate limit for the requesting authenticated user +rateLimit = twitter.getRateLimitStatus(rate_for="user") From c69834935b44111c419d183164ff3cc44779222a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 1 Jul 2009 05:18:48 -0400 Subject: [PATCH 059/687] Added check for authentication if user is getting rate limit status for an authenticated user --- tango.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tango.py b/tango.py index 5316e00..5313cfa 100644 --- a/tango.py +++ b/tango.py @@ -80,7 +80,10 @@ class setup: if rate_for == "requestingIP": return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) else: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + print "You need to be authenticated to check for this." except HTTPError, e: if self.debug is True: print e.headers @@ -143,19 +146,16 @@ class setup: if self.debug is True: print e.headers print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - + def updateStatus(self, status, in_reply_to_status_id = None): if len(list(status)) > 140: print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - if self.debug is True: - print e.headers - print "updateStatus() failed with a " + str(e.code) + " error code." - else: - print "updateStatus() requires you to be authenticated." + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + if self.debug is True: + print e.headers + print "updateStatus() failed with a " + str(e.code) + " error code." def destroyStatus(self, id): if self.authenticated is True: From b82f88c1010b6a0a06f26c60f5f7440d82215880 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 1 Jul 2009 05:18:52 -0400 Subject: [PATCH 060/687] Added check for authentication if user is getting rate limit status for an authenticated user --- tango3k.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tango3k.py b/tango3k.py index addb42e..c707aae 100644 --- a/tango3k.py +++ b/tango3k.py @@ -82,7 +82,10 @@ class setup: if rate_for == "requestingIP": return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) else: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + print("You need to be authenticated to check for this.") except HTTPError as e: if self.debug is True: print(e.headers) From 76ca83cb87cdfda2e08a62e4092cc9f255c5283e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 2 Jul 2009 03:56:24 -0400 Subject: [PATCH 061/687] Fixed credential verification with Basic (HTTP) Auth; previously, it assumed that passing in credentials meant you knew what you were doing. I take full blame for that act of idiocracy, but it's fixed now. --- tango.py | 8 +++++++- tango3k.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index 5313cfa..0994257 100644 --- a/tango.py +++ b/tango.py @@ -40,7 +40,13 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) - self.authenticated = True + try: + test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + if self.debug is True: + print e.headers + print "Huh, authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)" # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): diff --git a/tango3k.py b/tango3k.py index c707aae..ba3d771 100644 --- a/tango3k.py +++ b/tango3k.py @@ -42,7 +42,13 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) - self.authenticated = True + try: + test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError as e: + if self.debug is True: + print(e.headers) + print("Huh, authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): From 69f747a65881200afbdac4c6b6253dac66b347e4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 3 Jul 2009 03:04:47 -0400 Subject: [PATCH 062/687] Updated search example; major thanks to Sai for pointing out that this was broken. --- tango_examples/search_results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tango_examples/search_results.py b/tango_examples/search_results.py index 97b03a5..6cec48f 100644 --- a/tango_examples/search_results.py +++ b/tango_examples/search_results.py @@ -2,7 +2,7 @@ import tango """ Instantiate Tango with no Authentication """ twitter = tango.setup() -search_results = twitter.searchTwitter("WebsDotCom", "2") +search_results = twitter.searchTwitter("WebsDotCom", rpp="50") for tweet in search_results["results"]: print tweet["text"] From 716fe69e608afffbbf68634059fdcee352de0af6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 5 Jul 2009 03:53:28 -0400 Subject: [PATCH 063/687] Fixing issue pointed out by sk89q, wherein updateStatus() doesn't handle the in_reply_to_status_id parameter correctly. --- tango.py | 2 +- tango3k.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tango.py b/tango.py index 0994257..d19193c 100644 --- a/tango.py +++ b/tango.py @@ -157,7 +157,7 @@ class setup: if len(list(status)) > 140: print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: if self.debug is True: print e.headers diff --git a/tango3k.py b/tango3k.py index ba3d771..696706d 100644 --- a/tango3k.py +++ b/tango3k.py @@ -160,7 +160,7 @@ class setup: print("This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!") if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status}, {"in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: if self.debug is True: print(e.headers) From 4a910f3b803757e86d98cde16ca79437db011f53 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 6 Jul 2009 03:02:01 -0400 Subject: [PATCH 064/687] Added check for authentication in updateStatus(). Not sure why this was never there; ideally, I should replace all these is_autheticated calls at some point with a decorator. This commit also has a new TangoError Exception class, which updateStatus() will now raise. This should be spread throughout the library in the coming week, when I have a chance to hit these two issues. (Note: The Python 3 version of Tango will most likely incur a lag of a day or so this week, in terms of receiving updates. This will hopefully be made easier in the future... --- tango.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tango.py b/tango.py index d19193c..4d3546a 100644 --- a/tango.py +++ b/tango.py @@ -24,6 +24,12 @@ try: except ImportError: print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" +class TangoError(Exception): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + class setup: def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): self.authtype = authtype @@ -154,14 +160,18 @@ class setup: print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." def updateStatus(self, status, in_reply_to_status_id = None): - if len(list(status)) > 140: - print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - if self.debug is True: - print e.headers - print "updateStatus() failed with a " + str(e.code) + " error code." + if self.authenticated is True: + if len(list(status)) > 140: + print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + if self.debug is True: + print e.headers + raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") + #print "updateStatus() failed with a " + str(e.code) + " error code." + else: + raise TangoError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: From b4f62e942059c8467df8fa04088181a66b23df04 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 7 Jul 2009 08:27:33 -0400 Subject: [PATCH 065/687] Shortened constructApiURL() down to one line, and it's hopefully more efficient. searchTwitter() and some others now raise TangoError Exceptions when they hit a snag; entire library can hopefully be converted by the end of the week. Need to swap out string concatenation methods, as the method used in here is proven to be slower... could be a mess in larger applications. --- tango.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/tango.py b/tango.py index 4d3546a..722eda8 100644 --- a/tango.py +++ b/tango.py @@ -52,7 +52,7 @@ class setup: except HTTPError, e: if self.debug is True: print e.headers - print "Huh, authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)" + raise TangoError("Authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -76,16 +76,8 @@ class setup: print e.headers print "shortenURL() failed with a " + str(e.code) + " error code." - def constructApiURL(self, base_url, params, **kwargs): - queryURL = base_url - questionMarkUsed = False - if kwargs.has_key("questionMarkUsed") is True: - questionMarkUsed = True - for param in params: - if params[param] is not None: - queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) - questionMarkUsed = True - return queryURL + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) def getRateLimitStatus(self, rate_for = "requestingIP"): try: @@ -169,7 +161,6 @@ class setup: if self.debug is True: print e.headers raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") - #print "updateStatus() failed with a " + str(e.code) + " error code." else: raise TangoError("updateStatus() requires you to be authenticated.") @@ -384,7 +375,6 @@ class setup: print e.headers print "updateProfile() failed with a " + e.code + " error code." else: - # If they're not authenticated print "updateProfile() requires you to be authenticated." def getFavorites(self, page = "1"): @@ -543,17 +533,16 @@ class setup: print e.headers print "getBlockedIDs() failed with a " + str(e.code) + " error code." else: - print "getBlockedIDs() requires you to be authenticated." + raise TangoError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): - baseURL = "http://search.twitter.com/search.json?" + urllib.urlencode({"q": search_query}) - searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: if self.debug is True: print e.headers - print "getSearchTimeline() failed with a " + str(e.code) + " error code." + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`) def getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" From e642c117a458a2378eb8e7abf301015cf5c40ff9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 8 Jul 2009 01:27:57 -0400 Subject: [PATCH 066/687] All methods now raise tango exceptions; debug parameter is deprecated, header information is only reported back with HTTP code now --- tango.py | 248 +++++++++++++++++++------------------------------------ 1 file changed, 83 insertions(+), 165 deletions(-) diff --git a/tango.py b/tango.py index 722eda8..ca04bf5 100644 --- a/tango.py +++ b/tango.py @@ -17,11 +17,15 @@ from urllib2 import HTTPError try: import simplejson except ImportError: - print "Tango requires the simplejson library to work. http://www.undefined.org/python/" + try: + import json as simplejson + except: + raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: import oauth except ImportError: + # Need to figure out a better way to signal that this is an optional thing print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" class TangoError(Exception): @@ -31,13 +35,12 @@ class TangoError(Exception): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password self.oauth_keys = oauth_keys - self.debug = debug if self.username is not None and self.password is not None: if self.authtype == "OAuth": pass @@ -50,8 +53,6 @@ class setup: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: - if self.debug is True: - print e.headers raise TangoError("Authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") # OAuth functions; shortcuts for verifying the credentials. @@ -72,9 +73,7 @@ class setup: try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError, e: - if self.debug is True: - print e.headers - print "shortenURL() failed with a " + str(e.code) + " error code." + raise TangoError("shortenURL() failed with a %s error code." % `e.code`) def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) @@ -87,19 +86,15 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) else: - print "You need to be authenticated to check for this." + raise TangoError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: - if self.debug is True: - print e.headers - print "It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?" + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`) def getPublicTimeline(self): try: return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError, e: - if self.debug is True: - print e.headers - print "getPublicTimeline() failed with a " + str(e.code) + " error code." + raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -107,11 +102,9 @@ class setup: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getFriendsTimeline() failed with a " + str(e.code) + " error code." + raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: - print "getFriendsTimeline() requires you to be authenticated." + raise TangoError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: @@ -127,9 +120,8 @@ class setup: else: return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`) def getUserMentions(self, **kwargs): if self.authenticated is True: @@ -137,29 +129,23 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getUserMentions() failed with a " + str(e.code) + " error code." + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`) else: - print "getUserMentions() requires you to be authenticated." + raise TangoError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) except HTTPError, e: - if self.debug is True: - print e.headers - print "Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`) def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: if len(list(status)) > 140: - print "This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!" + raise TangoError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: - if self.debug is True: - print e.headers raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") else: raise TangoError("updateStatus() requires you to be authenticated.") @@ -169,11 +155,9 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) except HTTPError, e: - if self.debug is True: - print e.headers - print "destroyStatus() failed with a " + str(e.code) + " error code." + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`) else: - print "destroyStatus() requires you to be authenticated." + raise TangoError("destroyStatus() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -181,9 +165,7 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError, e: - if self.debug is True: - print e.headers - print "endSession failed with a " + str(e.code) + " error code." + raise TangoError("endSession failed with a %s error code." % `e.code`) else: print "You can't end a session when you're not authenticated to begin with." @@ -200,11 +182,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getDirectMessages() failed with a " + str(e.code) + " error code." + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`) else: - print "getDirectMessages() requires you to be authenticated." + raise TangoError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -219,11 +199,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getSentMessages() failed with a " + str(e.code) + " error code." + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`) else: - print "getSentMessages() requires you to be authenticated." + raise TangoError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -231,24 +209,20 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) except HTTPError, e: - if self.debug is True: - print e.headers - print "sendDirectMessage() failed with a " + str(e.code) + " error code." + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`) else: - print "Your message must be longer than 140 characters" + raise TangoError("Your message must not be longer than 140 characters") else: - print "You must be authenticated to send a new direct message." + raise TangoError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") except HTTPError, e: - if self.debug is True: - print e.headers - print "destroyDirectMessage() failed with a " + str(e.code) + " error code." + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`) else: - print "You must be authenticated to destroy a direct message." + raise TangoError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: @@ -262,13 +236,11 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "createFriendship() failed with a " + str(e.code) + " error code. " if e.code == 403: - print "It seems you've hit the update limit for this method. Try again in 24 hours." + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`) else: - print "createFriendship() requires you to be authenticated." + raise TangoError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -282,44 +254,36 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "destroyFriendship() failed with a " + str(e.code) + " error code." + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`) else: - print "destroyFriendship() requires you to be authenticated." + raise TangoError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError, e: - if self.debug is True: - print e.headers - print "checkIfFriendshipExists() failed with a " + str(e.code) + " error code." + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`) else: - print "checkIfFriendshipExists(), oddly, requires that you be authenticated." + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) except HTTPError, e: - if self.debug is True: - print e.headers - print "updateDeliveryDevice() failed with a " + str(e.code) + " error code." + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`) else: - print "updateDeliveryDevice() requires you to be authenticated." + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError, e: - if self.debug is True: - print e.headers - print "updateProfileColors() failed with a " + str(e.code) + " error code." + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`) else: - print "updateProfileColors() requires you to be authenticated." + raise TangoError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -330,7 +294,7 @@ class setup: updateProfileQueryString += "name=" + name useAmpersands = True else: - print "Twitter has a character limit of 20 for all usernames. Try again." + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") if email is not None and "@" in email: if len(list(email)) < 40: if useAmpersands is True: @@ -339,7 +303,7 @@ class setup: updateProfileQueryString += "email=" + email useAmpersands = True else: - print "Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again." + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") if url is not None: if len(list(url)) < 100: if useAmpersands is True: @@ -348,7 +312,7 @@ class setup: updateProfileQueryString += urllib.urlencode({"url": url}) useAmpersands = True else: - print "Twitter has a character limit of 100 for all urls. Try again." + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: @@ -357,7 +321,7 @@ class setup: updateProfileQueryString += urllib.urlencode({"location": location}) useAmpersands = True else: - print "Twitter has a character limit of 30 for all locations. Try again." + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: @@ -365,50 +329,42 @@ class setup: else: updateProfileQueryString += urllib.urlencode({"description": description}) else: - print "Twitter has a character limit of 160 for all descriptions. Try again." + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError, e: - if self.debug is True: - print e.headers - print "updateProfile() failed with a " + e.code + " error code." + raise TangoError("updateProfile() failed with a %s error code." % `e.code`) else: - print "updateProfile() requires you to be authenticated." + raise TangoError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError, e: - if self.debug: - print e.headers - print "getFavorites() failed with a " + str(e.code) + " error code." + raise TangoError("getFavorites() failed with a %s error code." % `e.code`) else: - print "getFavorites() requires you to be authenticated." + raise TangoError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError, e: - if self.debug: - print e.headers - print "createFavorite() failed with a " + str(e.code) + " error code." + raise TangoError("createFavorite() failed with a %s error code." % `e.code`) else: - print "createFavorite() requires you to be authenticated." + raise TangoError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError, e: - if self.debug: - print e.headers - print "destroyFavorite() failed with a " + str(e.code) + " error code." + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`) else: - print "destroyFavorite() requires you to be authenticated." + raise TangoError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -422,11 +378,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "notificationFollow() failed with a " + str(e.code) + " error code." + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`) else: - print "notificationFollow() requires you to be authenticated." + raise TangoError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -440,11 +394,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "notificationLeave() failed with a " + str(e.code) + " error code." + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`) else: - print "notificationLeave() requires you to be authenticated." + raise TangoError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -457,9 +409,7 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getFriendsIDs() failed with a " + str(e.code) + " error code." + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -472,31 +422,25 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getFollowersIDs() failed with a " + str(e.code) + " error code." + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "createBlock() failed with a " + str(e.code) + " error code." + raise TangoError("createBlock() failed with a %s error code." % `e.code`) else: - print "createBlock() requires you to be authenticated." + raise TangoError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "destroyBlock() failed with a " + str(e.code) + " error code." + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`) else: - print "destroyBlock() requires you to be authenticated." + raise TangoError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" @@ -509,29 +453,23 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "checkIfBlockExists() failed with a " + str(e.code) + " error code." + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getBlocking() failed with a " + str(e.code) + " error code." + raise TangoError("getBlocking() failed with a %s error code." % `e.code`) else: - print "getBlocking() requires you to be authenticated" + raise TangoError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError, e: - if self.debug is True: - print e.headers - print "getBlockedIDs() failed with a " + str(e.code) + " error code." + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`) else: raise TangoError("getBlockedIDs() requires you to be authenticated.") @@ -540,8 +478,6 @@ class setup: try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: - if self.debug is True: - print e.headers raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`) def getCurrentTrends(self, excludeHashTags = False): @@ -551,9 +487,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getCurrentTrends() failed with a " + str(e.code) + " error code." + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -569,9 +503,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getDailyTrends() failed with a " + str(e.code) + " error code." + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -587,53 +519,43 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - if self.debug is True: - print e.headers - print "getWeeklyTrends() failed with a " + str(e.code) + " error code." + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError, e: - if self.debug is True: - print e.headers - print "getSavedSearches() failed with a " + str(e.code) + " error code." + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`) else: - print "getSavedSearches() requires you to be authenticated." + raise TangoError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError, e: - if self.debug is True: - print e.headers - print "showSavedSearch() failed with a " + str(e.code) + " error code." + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`) else: - print "showSavedSearch() requires you to be authenticated." + raise TangoError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "createSavedSearch() failed with a " + str(e.code) + " error code." + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`) else: - print "createSavedSearch() requires you to be authenticated." + raise TangoError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError, e: - if self.debug is True: - print e.headers - print "destroySavedSearch() failed with a " + str(e.code) + " error code." + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`) else: - print "destroySavedSearch() requires you to be authenticated." + raise TangoError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -646,11 +568,9 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError, e: - if self.debug is True: - print e.headers - print "updateProfileBackgroundImage() failed with a " + str(e.code) + " error code." + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`) else: - print "You realize you need to be authenticated to change a background image, right?" + raise TangoError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -662,11 +582,9 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError, e: - if self.debug is True: - print e.headers - print "updateProfileImage() failed with a " + str(e.code) + " error code." + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`) else: - print "You realize you need to be authenticated to change a profile image, right?" + raise TangoError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() From 5236a85cdaba4ff20aee4f54abe43d28ed97834b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 8 Jul 2009 01:34:05 -0400 Subject: [PATCH 067/687] Update to tango3k that mirrors the recent changes in Tango master; all methods now raise TangoError() Exceptions when they hit snags, and there are various other changes, such as string concating that should hopefully be a bit faster. --- tango3k.py | 288 +++++++++++++++++++---------------------------------- 1 file changed, 100 insertions(+), 188 deletions(-) diff --git a/tango3k.py b/tango3k.py index 696706d..c9ea6e9 100644 --- a/tango3k.py +++ b/tango3k.py @@ -7,8 +7,6 @@ TODO: OAuth, Streaming API? - NOTE: THIS IS EXPERIMENTAL. - Questions, comments? ryan@venodesigns.net """ @@ -19,21 +17,30 @@ from urllib.error import HTTPError try: import simplejson except ImportError: - print("Tango requires the simplejson library to work. http://www.undefined.org/python/") + try: + import json as simplejson + except: + raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: import oauth except ImportError: + # Need to figure out a better way to signal that this is an optional thing print("Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py") +class TangoError(Exception): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, debug = False): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password self.oauth_keys = oauth_keys - self.debug = debug if self.username is not None and self.password is not None: if self.authtype == "OAuth": pass @@ -46,9 +53,7 @@ class setup: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: - if self.debug is True: - print(e.headers) - print("Huh, authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") + raise TangoError("Authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -68,20 +73,10 @@ class setup: try: return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError as e: - if self.debug is True: - print(e.headers) - print("shortenURL() failed with a " + str(e.code) + " error code.") + raise TangoError("shortenURL() failed with a %s error code." % repr(e.code)) - def constructApiURL(self, base_url, params, **kwargs): - queryURL = base_url - questionMarkUsed = False - if ("questionMarkUsed" in kwargs) is True: - questionMarkUsed = True - for param in params: - if params[param] is not None: - queryURL += (("&" if questionMarkUsed is True else "?") + param + "=" + params[param]) - questionMarkUsed = True - return queryURL + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) def getRateLimitStatus(self, rate_for = "requestingIP"): try: @@ -91,19 +86,15 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) else: - print("You need to be authenticated to check for this.") + raise TangoError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: - if self.debug is True: - print(e.headers) - print("It seems that there's something wrong. Twitter gave you a " + str(e.code) + " error code; are you doing something you shouldn't be?") + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code)) def getPublicTimeline(self): try: return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getPublicTimeline() failed with a " + str(e.code) + " error code.") + raise TangoError("getPublicTimeline() failed with a %s error code." % repr(e.code)) def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -111,11 +102,9 @@ class setup: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFriendsTimeline() failed with a " + str(e.code) + " error code.") + raise TangoError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) else: - print("getFriendsTimeline() requires you to be authenticated.") + raise TangoError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: @@ -131,9 +120,8 @@ class setup: else: return simplejson.load(urllib.request.urlopen(userTimelineURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % repr(e.code)) def getUserMentions(self, **kwargs): if self.authenticated is True: @@ -141,43 +129,35 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getUserMentions() failed with a " + str(e.code) + " error code.") + raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code)) else: - print("getUserMentions() requires you to be authenticated.") + raise TangoError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("Failed with a " + str(e.code) + " error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline.") - + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code)) + def updateStatus(self, status, in_reply_to_status_id = None): - if len(list(status)) > 140: - print("This status message is over 140 characters, but we're gonna try it anyway. Might wanna watch this!") if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateStatus() failed with a " + str(e.code) + " error code.") + raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") else: - print("updateStatus() requires you to be authenticated.") + raise TangoError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyStatus() failed with a " + str(e.code) + " error code.") + raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code)) else: - print("destroyStatus() requires you to be authenticated.") + raise TangoError("destroyStatus() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -185,9 +165,7 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError as e: - if self.debug is True: - print(e.headers) - print("endSession failed with a " + str(e.code) + " error code.") + raise TangoError("endSession failed with a %s error code." % repr(e.code)) else: print("You can't end a session when you're not authenticated to begin with.") @@ -204,11 +182,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getDirectMessages() failed with a " + str(e.code) + " error code.") + raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code)) else: - print("getDirectMessages() requires you to be authenticated.") + raise TangoError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -223,11 +199,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSentMessages() failed with a " + str(e.code) + " error code.") + raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code)) else: - print("getSentMessages() requires you to be authenticated.") + raise TangoError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -235,24 +209,20 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("sendDirectMessage() failed with a " + str(e.code) + " error code.") + raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code)) else: - print("Your message must be longer than 140 characters") + raise TangoError("Your message must not be longer than 140 characters") else: - print("You must be authenticated to send a new direct message.") + raise TangoError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyDirectMessage() failed with a " + str(e.code) + " error code.") + raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code)) else: - print("You must be authenticated to destroy a direct message.") + raise TangoError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: @@ -266,13 +236,11 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createFriendship() failed with a " + str(e.code) + " error code. ") if e.code == 403: - print("It seems you've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % repr(e.code)) else: - print("createFriendship() requires you to be authenticated.") + raise TangoError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -286,44 +254,36 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyFriendship() failed with a " + str(e.code) + " error code.") + raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code)) else: - print("destroyFriendship() requires you to be authenticated.") + raise TangoError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("checkIfFriendshipExists() failed with a " + str(e.code) + " error code.") + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code)) else: - print("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateDeliveryDevice() failed with a " + str(e.code) + " error code.") + raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code)) else: - print("updateDeliveryDevice() requires you to be authenticated.") + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileColors() failed with a " + str(e.code) + " error code.") + raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code)) else: - print("updateProfileColors() requires you to be authenticated.") + raise TangoError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -334,7 +294,7 @@ class setup: updateProfileQueryString += "name=" + name useAmpersands = True else: - print("Twitter has a character limit of 20 for all usernames. Try again.") + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") if email is not None and "@" in email: if len(list(email)) < 40: if useAmpersands is True: @@ -343,7 +303,7 @@ class setup: updateProfileQueryString += "email=" + email useAmpersands = True else: - print("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") if url is not None: if len(list(url)) < 100: if useAmpersands is True: @@ -352,7 +312,7 @@ class setup: updateProfileQueryString += urllib.parse.urlencode({"url": url}) useAmpersands = True else: - print("Twitter has a character limit of 100 for all urls. Try again.") + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: @@ -361,7 +321,7 @@ class setup: updateProfileQueryString += urllib.parse.urlencode({"location": location}) useAmpersands = True else: - print("Twitter has a character limit of 30 for all locations. Try again.") + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: @@ -369,51 +329,42 @@ class setup: else: updateProfileQueryString += urllib.parse.urlencode({"description": description}) else: - print("Twitter has a character limit of 160 for all descriptions. Try again.") + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfile() failed with a " + e.code + " error code.") + raise TangoError("updateProfile() failed with a %s error code." % repr(e.code)) else: - # If they're not authenticated - print("updateProfile() requires you to be authenticated.") + raise TangoError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError as e: - if self.debug: - print(e.headers) - print("getFavorites() failed with a " + str(e.code) + " error code.") + raise TangoError("getFavorites() failed with a %s error code." % repr(e.code)) else: - print("getFavorites() requires you to be authenticated.") + raise TangoError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError as e: - if self.debug: - print(e.headers) - print("createFavorite() failed with a " + str(e.code) + " error code.") + raise TangoError("createFavorite() failed with a %s error code." % repr(e.code)) else: - print("createFavorite() requires you to be authenticated.") + raise TangoError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError as e: - if self.debug: - print(e.headers) - print("destroyFavorite() failed with a " + str(e.code) + " error code.") + raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code)) else: - print("destroyFavorite() requires you to be authenticated.") + raise TangoError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -427,11 +378,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("notificationFollow() failed with a " + str(e.code) + " error code.") + raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code)) else: - print("notificationFollow() requires you to be authenticated.") + raise TangoError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -445,11 +394,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("notificationLeave() failed with a " + str(e.code) + " error code.") + raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code)) else: - print("notificationLeave() requires you to be authenticated.") + raise TangoError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -462,9 +409,7 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFriendsIDs() failed with a " + str(e.code) + " error code.") + raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code)) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -477,31 +422,25 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getFollowersIDs() failed with a " + str(e.code) + " error code.") + raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code)) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createBlock() failed with a " + str(e.code) + " error code.") + raise TangoError("createBlock() failed with a %s error code." % repr(e.code)) else: - print("createBlock() requires you to be authenticated.") + raise TangoError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroyBlock() failed with a " + str(e.code) + " error code.") + raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code)) else: - print("destroyBlock() requires you to be authenticated.") + raise TangoError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" @@ -514,41 +453,32 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("checkIfBlockExists() failed with a " + str(e.code) + " error code.") + raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code)) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getBlocking() failed with a " + str(e.code) + " error code.") + raise TangoError("getBlocking() failed with a %s error code." % repr(e.code)) else: - print("getBlocking() requires you to be authenticated") + raise TangoError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getBlockedIDs() failed with a " + str(e.code) + " error code.") + raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code)) else: - print("getBlockedIDs() requires you to be authenticated.") + raise TangoError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): - baseURL = "http://search.twitter.com/search.json?" + urllib.parse.urlencode({"q": search_query}) - searchURL = self.constructApiURL(baseURL, kwargs, questionMarkUsed=True) + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": search_query}) try: return simplejson.load(urllib.request.urlopen(searchURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSearchTimeline() failed with a " + str(e.code) + " error code.") + raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code)) def getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" @@ -557,9 +487,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getCurrentTrends() failed with a " + str(e.code) + " error code.") + raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code)) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -575,9 +503,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getDailyTrends() failed with a " + str(e.code) + " error code.") + raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code)) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -593,53 +519,43 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getWeeklyTrends() failed with a " + str(e.code) + " error code.") + raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code)) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("getSavedSearches() failed with a " + str(e.code) + " error code.") + raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code)) else: - print("getSavedSearches() requires you to be authenticated.") + raise TangoError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("showSavedSearch() failed with a " + str(e.code) + " error code.") + raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code)) else: - print("showSavedSearch() requires you to be authenticated.") + raise TangoError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("createSavedSearch() failed with a " + str(e.code) + " error code.") + raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code)) else: - print("createSavedSearch() requires you to be authenticated.") + raise TangoError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError as e: - if self.debug is True: - print(e.headers) - print("destroySavedSearch() failed with a " + str(e.code) + " error code.") + raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code)) else: - print("destroySavedSearch() requires you to be authenticated.") + raise TangoError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -652,11 +568,9 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileBackgroundImage() failed with a " + str(e.code) + " error code.") + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code)) else: - print("You realize you need to be authenticated to change a background image, right?") + raise TangoError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -668,11 +582,9 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError as e: - if self.debug is True: - print(e.headers) - print("updateProfileImage() failed with a " + str(e.code) + " error code.") + raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code)) else: - print("You realize you need to be authenticated to change a profile image, right?") + raise TangoError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() From 9bd05aaab6ca985e132c01ff9d3fa138be6958a6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 20 Jul 2009 00:24:23 -0400 Subject: [PATCH 068/687] Tango now raises an APILimit error that should make catching API rate limit issues easier. More work was also done to fix the bad way that strings are concatenated, though this is still ongoing - this updates applies to both tango and tango3k. --- tango.py | 111 +++++++++++++++++++++++++++++++---------------------- tango3k.py | 111 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 130 insertions(+), 92 deletions(-) diff --git a/tango.py b/tango.py index ca04bf5..3e30015 100644 --- a/tango.py +++ b/tango.py @@ -14,6 +14,16 @@ import httplib, urllib, urllib2, mimetypes, mimetools from urllib2 import HTTPError +__author__ = "Ryan McGrath " +__version__ = "0.5" + +""" +REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'http://twitter.com/oauth/authorize' +SIGNIN_URL = 'http://twitter.com/oauth/authenticate' +""" + try: import simplejson except ImportError: @@ -25,10 +35,17 @@ except ImportError: try: import oauth except ImportError: - # Need to figure out a better way to signal that this is an optional thing - print "Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py" + pass class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): def __init__(self, msg): self.msg = msg def __str__(self): @@ -53,7 +70,7 @@ class setup: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -88,7 +105,7 @@ class setup: else: raise TangoError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`) + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) def getPublicTimeline(self): try: @@ -121,7 +138,7 @@ class setup: return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`) + % `e.code`, e.code) def getUserMentions(self, **kwargs): if self.authenticated is True: @@ -129,15 +146,16 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`) + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`) + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: @@ -146,16 +164,16 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: - raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`) + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("destroyStatus() requires you to be authenticated.") @@ -165,9 +183,9 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`) + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) else: - print "You can't end a session when you're not authenticated to begin with." + raise TangoError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -182,7 +200,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`) + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getDirectMessages() requires you to be authenticated.") @@ -199,7 +217,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`) + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getSentMessages() requires you to be authenticated.") @@ -209,7 +227,7 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`) + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("Your message must not be longer than 140 characters") else: @@ -218,9 +236,9 @@ class setup: def destroyDirectMessage(self, id): if self.authenticated is True: try: - return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`) + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("You must be authenticated to destroy a direct message.") @@ -236,9 +254,10 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: + # Rate limiting is done differently here for API reasons... if e.code == 403: raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`) + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("createFriendship() requires you to be authenticated.") @@ -254,7 +273,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`) + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("destroyFriendship() requires you to be authenticated.") @@ -263,7 +282,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`) + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") @@ -272,7 +291,7 @@ class setup: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`) + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("updateDeliveryDevice() requires you to be authenticated.") @@ -281,7 +300,7 @@ class setup: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`) + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("updateProfileColors() requires you to be authenticated.") @@ -335,7 +354,7 @@ class setup: try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`) + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("updateProfile() requires you to be authenticated.") @@ -344,7 +363,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`) + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getFavorites() requires you to be authenticated.") @@ -353,7 +372,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`) + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("createFavorite() requires you to be authenticated.") @@ -362,7 +381,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`) + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("destroyFavorite() requires you to be authenticated.") @@ -378,7 +397,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`) + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("notificationFollow() requires you to be authenticated.") @@ -394,7 +413,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`) + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("notificationLeave() requires you to be authenticated.") @@ -409,7 +428,7 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`) + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -422,14 +441,14 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`) + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`) + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("createBlock() requires you to be authenticated.") @@ -438,7 +457,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`) + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("destroyBlock() requires you to be authenticated.") @@ -453,14 +472,14 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`) + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`) + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getBlocking() requires you to be authenticated") @@ -469,7 +488,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`) + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getBlockedIDs() requires you to be authenticated.") @@ -478,7 +497,7 @@ class setup: try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: - raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`) + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) def getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" @@ -487,7 +506,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`) + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -503,7 +522,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`) + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -519,14 +538,14 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`) + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`) + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("getSavedSearches() requires you to be authenticated.") @@ -535,7 +554,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`) + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("showSavedSearch() requires you to be authenticated.") @@ -544,7 +563,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`) + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("createSavedSearch() requires you to be authenticated.") @@ -553,7 +572,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`) + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("destroySavedSearch() requires you to be authenticated.") @@ -568,7 +587,7 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`) + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("You realize you need to be authenticated to change a background image, right?") @@ -582,7 +601,7 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`) + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) else: raise TangoError("You realize you need to be authenticated to change a profile image, right?") diff --git a/tango3k.py b/tango3k.py index c9ea6e9..8d12d84 100644 --- a/tango3k.py +++ b/tango3k.py @@ -14,6 +14,16 @@ import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetype from urllib.error import HTTPError +__author__ = "Ryan McGrath " +__version__ = "0.5" + +""" +REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'http://twitter.com/oauth/authorize' +SIGNIN_URL = 'http://twitter.com/oauth/authenticate' +""" + try: import simplejson except ImportError: @@ -25,10 +35,17 @@ except ImportError: try: import oauth except ImportError: - # Need to figure out a better way to signal that this is an optional thing - print("Tango requires oauth for authentication purposes. http://oauth.googlecode.com/svn/code/python/oauth/oauth.py") + pass class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): def __init__(self, msg): self.msg = msg def __str__(self): @@ -53,7 +70,7 @@ class setup: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: - raise TangoError("Authentication failed with your provided credentials. Try again? (" + str(e.code) + " failure)") + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -88,7 +105,7 @@ class setup: else: raise TangoError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code)) + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) def getPublicTimeline(self): try: @@ -121,7 +138,7 @@ class setup: return simplejson.load(urllib.request.urlopen(userTimelineURL)) except HTTPError as e: raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % repr(e.code)) + % repr(e.code), e.code) def getUserMentions(self, **kwargs): if self.authenticated is True: @@ -129,15 +146,16 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError as e: - raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code)) + raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/" + id + ".json")) + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError as e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code)) + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % repr(e.code), e.code) def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: @@ -146,16 +164,16 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: - raise TangoError("updateStatus() failed with a " + str(e.code) + "error code.") + raise TangoError("updateStatus() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/" + id + ".json", "POST")) + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) except HTTPError as e: - raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code)) + raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("destroyStatus() requires you to be authenticated.") @@ -165,9 +183,9 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError as e: - raise TangoError("endSession failed with a %s error code." % repr(e.code)) + raise TangoError("endSession failed with a %s error code." % repr(e.code), e.code) else: - print("You can't end a session when you're not authenticated to begin with.") + raise TangoError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -182,7 +200,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code)) + raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getDirectMessages() requires you to be authenticated.") @@ -199,7 +217,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code)) + raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getSentMessages() requires you to be authenticated.") @@ -209,7 +227,7 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) except HTTPError as e: - raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code)) + raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("Your message must not be longer than 140 characters") else: @@ -218,9 +236,9 @@ class setup: def destroyDirectMessage(self, id): if self.authenticated is True: try: - return self.opener.open("http://twitter.com/direct_messages/destroy/" + id + ".json", "") + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") except HTTPError as e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code)) + raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("You must be authenticated to destroy a direct message.") @@ -236,9 +254,10 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: + # Rate limiting is done differently here for API reasons... if e.code == 403: raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % repr(e.code)) + raise TangoError("createFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("createFriendship() requires you to be authenticated.") @@ -254,7 +273,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code)) + raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("destroyFriendship() requires you to be authenticated.") @@ -263,7 +282,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError as e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code)) + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") @@ -272,7 +291,7 @@ class setup: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) except HTTPError as e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code)) + raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("updateDeliveryDevice() requires you to be authenticated.") @@ -281,7 +300,7 @@ class setup: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError as e: - raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code)) + raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("updateProfileColors() requires you to be authenticated.") @@ -335,7 +354,7 @@ class setup: try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError as e: - raise TangoError("updateProfile() failed with a %s error code." % repr(e.code)) + raise TangoError("updateProfile() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("updateProfile() requires you to be authenticated.") @@ -344,7 +363,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError as e: - raise TangoError("getFavorites() failed with a %s error code." % repr(e.code)) + raise TangoError("getFavorites() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getFavorites() requires you to be authenticated.") @@ -353,7 +372,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError as e: - raise TangoError("createFavorite() failed with a %s error code." % repr(e.code)) + raise TangoError("createFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("createFavorite() requires you to be authenticated.") @@ -362,7 +381,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code)) + raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("destroyFavorite() requires you to be authenticated.") @@ -378,7 +397,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code)) + raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("notificationFollow() requires you to be authenticated.") @@ -394,7 +413,7 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code)) + raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("notificationLeave() requires you to be authenticated.") @@ -409,7 +428,7 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code)) + raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -422,14 +441,14 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code)) + raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError as e: - raise TangoError("createBlock() failed with a %s error code." % repr(e.code)) + raise TangoError("createBlock() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("createBlock() requires you to be authenticated.") @@ -438,7 +457,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code)) + raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("destroyBlock() requires you to be authenticated.") @@ -453,14 +472,14 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code)) + raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError as e: - raise TangoError("getBlocking() failed with a %s error code." % repr(e.code)) + raise TangoError("getBlocking() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getBlocking() requires you to be authenticated") @@ -469,7 +488,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError as e: - raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code)) + raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getBlockedIDs() requires you to be authenticated.") @@ -478,7 +497,7 @@ class setup: try: return simplejson.load(urllib.request.urlopen(searchURL)) except HTTPError as e: - raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code)) + raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) def getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" @@ -487,7 +506,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code)) + raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -503,7 +522,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code)) + raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -519,14 +538,14 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code)) + raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError as e: - raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code)) + raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("getSavedSearches() requires you to be authenticated.") @@ -535,7 +554,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError as e: - raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code)) + raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("showSavedSearch() requires you to be authenticated.") @@ -544,7 +563,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError as e: - raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code)) + raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("createSavedSearch() requires you to be authenticated.") @@ -553,7 +572,7 @@ class setup: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code)) + raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("destroySavedSearch() requires you to be authenticated.") @@ -568,7 +587,7 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code)) + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("You realize you need to be authenticated to change a background image, right?") @@ -582,7 +601,7 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code)) + raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) else: raise TangoError("You realize you need to be authenticated to change a profile image, right?") From ccc9ded28b1e1d4da01fe4bd29d9d8a3631f8ad8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 23 Jul 2009 23:59:13 -0400 Subject: [PATCH 069/687] Fixing a README typo --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index 472884b..6b14013 100644 --- a/README +++ b/README @@ -9,7 +9,7 @@ make a seasoned Python vet scratch his head, or possibly call me insane. It's op and I'm open to anything that'll improve the library as a whole. OAuth support is in the works, but every other part of the Twitter API should be covered. Tango -handles both Baisc (HTTP) Authentication and OAuth, and OAuth is the default method for +handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for Authentication. To override this, specify 'authtype="Basic"' in your tango.setup() call. Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All From 8bf063fdf9626c31427ecb54fb3a8720a7666558 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 27 Jul 2009 03:13:15 -0400 Subject: [PATCH 070/687] Specifying OAuth urls when user specifies OAuth style login --- tango.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tango.py b/tango.py index 3e30015..f2ccb48 100644 --- a/tango.py +++ b/tango.py @@ -17,13 +17,6 @@ from urllib2 import HTTPError __author__ = "Ryan McGrath " __version__ = "0.5" -""" -REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token' -ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token' -AUTHORIZATION_URL = 'http://twitter.com/oauth/authorize' -SIGNIN_URL = 'http://twitter.com/oauth/authenticate' -""" - try: import simplejson except ImportError: @@ -60,7 +53,11 @@ class setup: self.oauth_keys = oauth_keys if self.username is not None and self.password is not None: if self.authtype == "OAuth": - pass + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... elif self.authtype == "Basic": self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) From 4fed1e241f5e2a9ff5bf0198bae6784e60330a99 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 02:52:35 -0400 Subject: [PATCH 071/687] Tango now supports adding a unique User Agent, in keeping with Twitter's Search API requirements. See the Wiki for information on usage. --- tango.py | 6 ++++-- tango3k.py | 19 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tango.py b/tango.py index f2ccb48..0b7d734 100644 --- a/tango.py +++ b/tango.py @@ -15,7 +15,7 @@ import httplib, urllib, urllib2, mimetypes, mimetools from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.5" +__version__ = "0.6" try: import simplejson @@ -45,7 +45,7 @@ class APILimit(TangoError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username @@ -63,6 +63,8 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] try: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True diff --git a/tango3k.py b/tango3k.py index 8d12d84..e3149be 100644 --- a/tango3k.py +++ b/tango3k.py @@ -15,14 +15,7 @@ import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetype from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.5" - -""" -REQUEST_TOKEN_URL = 'https://twitter.com/oauth/request_token' -ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token' -AUTHORIZATION_URL = 'http://twitter.com/oauth/authorize' -SIGNIN_URL = 'http://twitter.com/oauth/authenticate' -""" +__version__ = "0.6" try: import simplejson @@ -52,7 +45,7 @@ class APILimit(TangoError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None): + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username @@ -60,12 +53,18 @@ class setup: self.oauth_keys = oauth_keys if self.username is not None and self.password is not None: if self.authtype == "OAuth": - pass + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... elif self.authtype == "Basic": self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] try: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True From d7d170cc3b7f937579c022e1c6247b24379f4d47 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 03:13:48 -0400 Subject: [PATCH 072/687] Reorganizing into a more package based structure, setting an initial (not working) setup.py file - this will be expanded soon --- examples/current_trends.py | 7 + examples/daily_trends.py | 7 + examples/get_friends_timeline.py | 8 + examples/get_user_mention.py | 6 + examples/get_user_timeline.py | 7 + examples/public_timeline.py | 8 + examples/rate_limit.py | 10 + examples/search_results.py | 8 + examples/shorten_url.py | 7 + examples/tango_setup.py | 11 + examples/update_profile_image.py | 5 + examples/update_status.py | 5 + examples/weekly_trends.py | 7 + setup.py | 3 + tango/tango.py | 629 +++++++++++++++++++++++++++++++ tango/tango3k.py | 629 +++++++++++++++++++++++++++++++ 16 files changed, 1357 insertions(+) create mode 100644 examples/current_trends.py create mode 100644 examples/daily_trends.py create mode 100644 examples/get_friends_timeline.py create mode 100644 examples/get_user_mention.py create mode 100644 examples/get_user_timeline.py create mode 100644 examples/public_timeline.py create mode 100644 examples/rate_limit.py create mode 100644 examples/search_results.py create mode 100644 examples/shorten_url.py create mode 100644 examples/tango_setup.py create mode 100644 examples/update_profile_image.py create mode 100644 examples/update_status.py create mode 100644 examples/weekly_trends.py create mode 100644 setup.py create mode 100644 tango/tango.py create mode 100644 tango/tango3k.py diff --git a/examples/current_trends.py b/examples/current_trends.py new file mode 100644 index 0000000..dd2d50d --- /dev/null +++ b/examples/current_trends.py @@ -0,0 +1,7 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getCurrentTrends() + +print trends diff --git a/examples/daily_trends.py b/examples/daily_trends.py new file mode 100644 index 0000000..28bdde1 --- /dev/null +++ b/examples/daily_trends.py @@ -0,0 +1,7 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getDailyTrends() + +print trends diff --git a/examples/get_friends_timeline.py b/examples/get_friends_timeline.py new file mode 100644 index 0000000..e3c3ddd --- /dev/null +++ b/examples/get_friends_timeline.py @@ -0,0 +1,8 @@ +import tango, pprint + +# Authenticate using Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") +friends_timeline = twitter.getFriendsTimeline(count="150", page="3") + +for tweet in friends_timeline: + print tweet["text"] diff --git a/examples/get_user_mention.py b/examples/get_user_mention.py new file mode 100644 index 0000000..6b94371 --- /dev/null +++ b/examples/get_user_mention.py @@ -0,0 +1,6 @@ +import tango + +twitter = tango.setup(authtype="Basic", username="example", password="example") +mentions = twitter.getUserMentions(count="150") + +print mentions diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py new file mode 100644 index 0000000..aa7bf97 --- /dev/null +++ b/examples/get_user_timeline.py @@ -0,0 +1,7 @@ +import tango + +# We won't authenticate for this, but sometimes it's necessary +twitter = tango.setup() +user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") + +print user_timeline diff --git a/examples/public_timeline.py b/examples/public_timeline.py new file mode 100644 index 0000000..4a6c161 --- /dev/null +++ b/examples/public_timeline.py @@ -0,0 +1,8 @@ +import tango + +# Getting the public timeline requires no authentication, huzzah +twitter = tango.setup() +public_timeline = twitter.getPublicTimeline() + +for tweet in public_timeline: + print tweet["text"] diff --git a/examples/rate_limit.py b/examples/rate_limit.py new file mode 100644 index 0000000..ae85973 --- /dev/null +++ b/examples/rate_limit.py @@ -0,0 +1,10 @@ +import tango + +# Instantiate with Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") + +# This returns the rate limit for the requesting IP +rateLimit = twitter.getRateLimitStatus() + +# This returns the rate limit for the requesting authenticated user +rateLimit = twitter.getRateLimitStatus(rate_for="user") diff --git a/examples/search_results.py b/examples/search_results.py new file mode 100644 index 0000000..6cec48f --- /dev/null +++ b/examples/search_results.py @@ -0,0 +1,8 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +search_results = twitter.searchTwitter("WebsDotCom", rpp="50") + +for tweet in search_results["results"]: + print tweet["text"] diff --git a/examples/shorten_url.py b/examples/shorten_url.py new file mode 100644 index 0000000..12b7668 --- /dev/null +++ b/examples/shorten_url.py @@ -0,0 +1,7 @@ +import tango + +# Shortening URLs requires no authentication, huzzah +twitter = tango.setup() +shortURL = twitter.shortenURL("http://www.webs.com/") + +print shortURL diff --git a/examples/tango_setup.py b/examples/tango_setup.py new file mode 100644 index 0000000..56d2429 --- /dev/null +++ b/examples/tango_setup.py @@ -0,0 +1,11 @@ +import tango + +# Using no authentication and specifying Debug +twitter = tango.setup(debug=True) + +# Using Basic Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") + +# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) +auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} +twitter = tango.setup(username="example", password="example", oauth_keys=auth_keys) diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py new file mode 100644 index 0000000..a6f52b2 --- /dev/null +++ b/examples/update_profile_image.py @@ -0,0 +1,5 @@ +import tango + +# Instantiate Tango with Basic (HTTP) Authentication +twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter.updateProfileImage("myImage.png") diff --git a/examples/update_status.py b/examples/update_status.py new file mode 100644 index 0000000..1466752 --- /dev/null +++ b/examples/update_status.py @@ -0,0 +1,5 @@ +import tango + +# Create a Tango instance using Basic (HTTP) Authentication and update our Status +twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter.updateStatus("See how easy this was?") diff --git a/examples/weekly_trends.py b/examples/weekly_trends.py new file mode 100644 index 0000000..fd9b564 --- /dev/null +++ b/examples/weekly_trends.py @@ -0,0 +1,7 @@ +import tango + +""" Instantiate Tango with no Authentication """ +twitter = tango.setup() +trends = twitter.getWeeklyTrends() + +print trends diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4a45b80 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +from distutils.core import setup diff --git a/tango/tango.py b/tango/tango.py new file mode 100644 index 0000000..0b7d734 --- /dev/null +++ b/tango/tango.py @@ -0,0 +1,629 @@ +#!/usr/bin/python + +""" + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools + +from urllib2 import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.6" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except: + raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... + elif self.authtype == "Basic": + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] + try: + test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError, e: + raise TangoError("shortenURL() failed with a %s error code." % `e.code`) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TangoError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError, e: + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self): + try: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError, e: + raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + else: + raise TangoError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`, e.code) + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError, e: + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getUserMentions() requires you to be authenticated.") + + def showStatus(self, id): + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError, e: + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError, e: + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/tango/tango3k.py b/tango/tango3k.py new file mode 100644 index 0000000..e3149be --- /dev/null +++ b/tango/tango3k.py @@ -0,0 +1,629 @@ +#!/usr/bin/python + +""" + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools + +from urllib.error import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.6" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except: + raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... + elif self.authtype == "Basic": + self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib.request.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] + try: + test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError as e: + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError as e: + raise TangoError("shortenURL() failed with a %s error code." % repr(e.code)) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TangoError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError as e: + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) + + def getPublicTimeline(self): + try: + return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError as e: + raise TangoError("getPublicTimeline() failed with a %s error code." % repr(e.code)) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError as e: + raise TangoError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) + else: + raise TangoError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib.request.urlopen(userTimelineURL)) + except HTTPError as e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % repr(e.code), e.code) + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError as e: + raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getUserMentions() requires you to be authenticated.") + + def showStatus(self, id): + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError as e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % repr(e.code), e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError as e: + raise TangoError("updateStatus() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError as e: + raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError as e: + raise TangoError("endSession failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) + except HTTPError as e: + raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError as e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError as e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) + except HTTPError as e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError as e: + raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.parse.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.parse.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.parse.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError as e: + raise TangoError("updateProfile() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError as e: + raise TangoError("getFavorites() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError as e: + raise TangoError("createFavorite() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError as e: + raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError as e: + raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError as e: + raise TangoError("createBlock() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError as e: + raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError as e: + raise TangoError("getBlocking() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError as e: + raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": search_query}) + try: + return simplejson.load(urllib.request.urlopen(searchURL)) + except HTTPError as e: + raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError as e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError as e: + raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError as e: + raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError as e: + raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError as e: + raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError as e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError as e: + raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From f374ac3e61365b27bb2f5ca456726929baaeee25 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 03:14:08 -0400 Subject: [PATCH 073/687] Getting rid of cruft --- tango.py | 629 ------------------------- tango3k.py | 629 ------------------------- tango_examples/current_trends.py | 7 - tango_examples/daily_trends.py | 7 - tango_examples/get_friends_timeline.py | 8 - tango_examples/get_user_mention.py | 6 - tango_examples/get_user_timeline.py | 7 - tango_examples/public_timeline.py | 8 - tango_examples/rate_limit.py | 10 - tango_examples/search_results.py | 8 - tango_examples/shorten_url.py | 7 - tango_examples/tango_setup.py | 11 - tango_examples/update_profile_image.py | 5 - tango_examples/update_status.py | 5 - tango_examples/weekly_trends.py | 7 - 15 files changed, 1354 deletions(-) delete mode 100644 tango.py delete mode 100644 tango3k.py delete mode 100644 tango_examples/current_trends.py delete mode 100644 tango_examples/daily_trends.py delete mode 100644 tango_examples/get_friends_timeline.py delete mode 100644 tango_examples/get_user_mention.py delete mode 100644 tango_examples/get_user_timeline.py delete mode 100644 tango_examples/public_timeline.py delete mode 100644 tango_examples/rate_limit.py delete mode 100644 tango_examples/search_results.py delete mode 100644 tango_examples/shorten_url.py delete mode 100644 tango_examples/tango_setup.py delete mode 100644 tango_examples/update_profile_image.py delete mode 100644 tango_examples/update_status.py delete mode 100644 tango_examples/weekly_trends.py diff --git a/tango.py b/tango.py deleted file mode 100644 index 0b7d734..0000000 --- a/tango.py +++ /dev/null @@ -1,629 +0,0 @@ -#!/usr/bin/python - -""" - Tango is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.6" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except: - raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TangoError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - try: - test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass - - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): - try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getUserMentions() requires you to be authenticated.") - - def showStatus(self, id): - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("Your message must not be longer than 140 characters") - else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) - except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.urlencode({"url": url}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.urlencode({"location": location}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.urlencode({"description": description}) - else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/tango3k.py b/tango3k.py deleted file mode 100644 index e3149be..0000000 --- a/tango3k.py +++ /dev/null @@ -1,629 +0,0 @@ -#!/usr/bin/python - -""" - Tango is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools - -from urllib.error import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.6" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except: - raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TangoError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": - self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib.request.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - try: - test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError as e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) - - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass - - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError as e: - raise TangoError("shortenURL() failed with a %s error code." % repr(e.code)) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError as e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) - - def getPublicTimeline(self): - try: - return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError as e: - raise TangoError("getPublicTimeline() failed with a %s error code." % repr(e.code)) - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError as e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) - else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib.request.urlopen(userTimelineURL)) - except HTTPError as e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % repr(e.code), e.code) - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError as e: - raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getUserMentions() requires you to be authenticated.") - - def showStatus(self, id): - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError as e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % repr(e.code), e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError as e: - raise TangoError("updateStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError as e: - raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError as e: - raise TangoError("endSession failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) - except HTTPError as e: - raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("Your message must not be longer than 140 characters") - else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError as e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError as e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) - except HTTPError as e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError as e: - raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.parse.urlencode({"url": url}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.parse.urlencode({"location": location}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.parse.urlencode({"description": description}) - else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError as e: - raise TangoError("updateProfile() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError as e: - raise TangoError("getFavorites() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError as e: - raise TangoError("createFavorite() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError as e: - raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError as e: - raise TangoError("createBlock() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError as e: - raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib.request.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError as e: - raise TangoError("getBlocking() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError as e: - raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": search_query}) - try: - return simplejson.load(urllib.request.urlopen(searchURL)) - except HTTPError as e: - raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError as e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError as e: - raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError as e: - raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError as e: - raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError as e: - raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError as e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError as e: - raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) - else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/tango_examples/current_trends.py b/tango_examples/current_trends.py deleted file mode 100644 index dd2d50d..0000000 --- a/tango_examples/current_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -import tango - -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() -trends = twitter.getCurrentTrends() - -print trends diff --git a/tango_examples/daily_trends.py b/tango_examples/daily_trends.py deleted file mode 100644 index 28bdde1..0000000 --- a/tango_examples/daily_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -import tango - -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() -trends = twitter.getDailyTrends() - -print trends diff --git a/tango_examples/get_friends_timeline.py b/tango_examples/get_friends_timeline.py deleted file mode 100644 index e3c3ddd..0000000 --- a/tango_examples/get_friends_timeline.py +++ /dev/null @@ -1,8 +0,0 @@ -import tango, pprint - -# Authenticate using Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") -friends_timeline = twitter.getFriendsTimeline(count="150", page="3") - -for tweet in friends_timeline: - print tweet["text"] diff --git a/tango_examples/get_user_mention.py b/tango_examples/get_user_mention.py deleted file mode 100644 index 6b94371..0000000 --- a/tango_examples/get_user_mention.py +++ /dev/null @@ -1,6 +0,0 @@ -import tango - -twitter = tango.setup(authtype="Basic", username="example", password="example") -mentions = twitter.getUserMentions(count="150") - -print mentions diff --git a/tango_examples/get_user_timeline.py b/tango_examples/get_user_timeline.py deleted file mode 100644 index aa7bf97..0000000 --- a/tango_examples/get_user_timeline.py +++ /dev/null @@ -1,7 +0,0 @@ -import tango - -# We won't authenticate for this, but sometimes it's necessary -twitter = tango.setup() -user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") - -print user_timeline diff --git a/tango_examples/public_timeline.py b/tango_examples/public_timeline.py deleted file mode 100644 index 4a6c161..0000000 --- a/tango_examples/public_timeline.py +++ /dev/null @@ -1,8 +0,0 @@ -import tango - -# Getting the public timeline requires no authentication, huzzah -twitter = tango.setup() -public_timeline = twitter.getPublicTimeline() - -for tweet in public_timeline: - print tweet["text"] diff --git a/tango_examples/rate_limit.py b/tango_examples/rate_limit.py deleted file mode 100644 index ae85973..0000000 --- a/tango_examples/rate_limit.py +++ /dev/null @@ -1,10 +0,0 @@ -import tango - -# Instantiate with Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") - -# This returns the rate limit for the requesting IP -rateLimit = twitter.getRateLimitStatus() - -# This returns the rate limit for the requesting authenticated user -rateLimit = twitter.getRateLimitStatus(rate_for="user") diff --git a/tango_examples/search_results.py b/tango_examples/search_results.py deleted file mode 100644 index 6cec48f..0000000 --- a/tango_examples/search_results.py +++ /dev/null @@ -1,8 +0,0 @@ -import tango - -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() -search_results = twitter.searchTwitter("WebsDotCom", rpp="50") - -for tweet in search_results["results"]: - print tweet["text"] diff --git a/tango_examples/shorten_url.py b/tango_examples/shorten_url.py deleted file mode 100644 index 12b7668..0000000 --- a/tango_examples/shorten_url.py +++ /dev/null @@ -1,7 +0,0 @@ -import tango - -# Shortening URLs requires no authentication, huzzah -twitter = tango.setup() -shortURL = twitter.shortenURL("http://www.webs.com/") - -print shortURL diff --git a/tango_examples/tango_setup.py b/tango_examples/tango_setup.py deleted file mode 100644 index 56d2429..0000000 --- a/tango_examples/tango_setup.py +++ /dev/null @@ -1,11 +0,0 @@ -import tango - -# Using no authentication and specifying Debug -twitter = tango.setup(debug=True) - -# Using Basic Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") - -# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) -auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} -twitter = tango.setup(username="example", password="example", oauth_keys=auth_keys) diff --git a/tango_examples/update_profile_image.py b/tango_examples/update_profile_image.py deleted file mode 100644 index a6f52b2..0000000 --- a/tango_examples/update_profile_image.py +++ /dev/null @@ -1,5 +0,0 @@ -import tango - -# Instantiate Tango with Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") -twitter.updateProfileImage("myImage.png") diff --git a/tango_examples/update_status.py b/tango_examples/update_status.py deleted file mode 100644 index 1466752..0000000 --- a/tango_examples/update_status.py +++ /dev/null @@ -1,5 +0,0 @@ -import tango - -# Create a Tango instance using Basic (HTTP) Authentication and update our Status -twitter = tango.setup(authtype="Basic", username="example", password="example") -twitter.updateStatus("See how easy this was?") diff --git a/tango_examples/weekly_trends.py b/tango_examples/weekly_trends.py deleted file mode 100644 index fd9b564..0000000 --- a/tango_examples/weekly_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -import tango - -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() -trends = twitter.getWeeklyTrends() - -print trends From 95f3ff17f0d105cf2f66200f905db3ed373f8f04 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 03:24:12 -0400 Subject: [PATCH 074/687] Licensing stuff --- LICENSE | 12 ++++++++++++ README | 2 ++ setup.py | 3 +++ 3 files changed, 17 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..03c5461 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Tango is released under the "Tango License". + +What's the "Tango License"? It basically means you can do +whatever you want with this; hack on it, redistribute it, contribute changes, whatever. + +All I ask is that, if you do make changes to Tango, decide to fork it, etc., please provide some +note about the origin of the work. (i.e, a link back to Tango, etc) + +Tango is provided "as-is"; that is, I don't guarantee that it will 100% work in your +environment without some effort. Tango is a base to build off of. + +~ Ryan McGrath, 07/27/2009 diff --git a/README b/README index 6b14013..bf8aad1 100644 --- a/README +++ b/README @@ -44,3 +44,5 @@ Questions, Comments, etc? My hope is that Tango is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. + +Tango is released under the "Tango License" - see the LICENSE file for more information. diff --git a/setup.py b/setup.py index 4a45b80..987ded4 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,6 @@ #!/usr/bin/env python +__author__ = 'Ryan McGrath ' +__version__ = '0.6' + from distutils.core import setup From 100768892569143aeb8f2e06970247eb1eda1f26 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 23:00:43 -0400 Subject: [PATCH 075/687] A somewhat working setup.py - still a work in progress --- setup.py | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 987ded4..449e678 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,45 @@ -#!/usr/bin/env python +#!/usr/bin/python __author__ = 'Ryan McGrath ' __version__ = '0.6' -from distutils.core import setup +# For the love of god, use Pip to install this. + +# Distutils version +METADATA = dict( + name = "tango", + version = __version__, + py_modules = ['tango/tango'], + author='Ryan McGrath', + author_email='ryan@venodesigns.net', + description='A new and easy way to access Twitter data with Python.', + license='Tango License', + url='http://github.com/ryanmcgrath/tango/tree/master', + keywords='twitter search api tweet tango', +) + +# Setuptools version +SETUPTOOLS_METADATA = dict( + install_requires = ['setuptools', 'simplejson'], + include_package_data = True, + classifiers = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: Tango License', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Communications :: Chat :: Twitter', + 'Topic :: Internet :: Search', + ] +) + +def Main(): + try: + import setuptools + METADATA.update(SETUPTOOLS_METADATA) + setuptools.setup(**METADATA) + except ImportError: + import distutils.core + distutils.core.setup(**METADATA) + +if __name__ == '__main__': + Main() From 932d29a1ab98a9bda00ae804a06f8a8dc1d40fdc Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 23:22:52 -0400 Subject: [PATCH 076/687] Changed licensing, modified contents of setup.py to conform to what Pypi wants/needs --- LICENSE | 25 +++++++++++++++++-------- README | 2 +- setup.py | 6 +++--- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/LICENSE b/LICENSE index 03c5461..6fdab59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,21 @@ -Tango is released under the "Tango License". +The MIT License -What's the "Tango License"? It basically means you can do -whatever you want with this; hack on it, redistribute it, contribute changes, whatever. +Copyright (c) 2009 Ryan McGrath -All I ask is that, if you do make changes to Tango, decide to fork it, etc., please provide some -note about the origin of the work. (i.e, a link back to Tango, etc) +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Tango is provided "as-is"; that is, I don't guarantee that it will 100% work in your -environment without some effort. Tango is a base to build off of. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -~ Ryan McGrath, 07/27/2009 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README b/README index bf8aad1..7eb7671 100644 --- a/README +++ b/README @@ -45,4 +45,4 @@ My hope is that Tango is so simple that you'd never *have* to ask any questions, you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. -Tango is released under the "Tango License" - see the LICENSE file for more information. +Tango is released under an MIT License - see the LICENSE file for more information. diff --git a/setup.py b/setup.py index 449e678..f5b217b 100644 --- a/setup.py +++ b/setup.py @@ -25,10 +25,10 @@ SETUPTOOLS_METADATA = dict( classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', - 'License :: Tango License', + 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Communications :: Chat :: Twitter', - 'Topic :: Internet :: Search', + 'Topic :: Communications :: Chat', + 'Topic :: Internet', ] ) From 6fe1a95a2ef1361bf93ae0177c50b582afa8109f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 28 Jul 2009 23:31:15 -0400 Subject: [PATCH 077/687] Packaged versions galore... Need to figure out the best way to handle Tango3k soon - perhaps in a new branch? Is anyone even using it at the moment? --- build/lib/tango/tango.py | 629 ++++++++++++++++++++++++++++ dist/tango-0.6.tar.gz | Bin 0 -> 6707 bytes dist/tango-0.6.win32.exe | Bin 0 -> 67962 bytes dist/tango-0.7.tar.gz | Bin 0 -> 6709 bytes dist/tango-0.7.win32.exe | Bin 0 -> 67963 bytes setup.py | 4 +- tango.egg-info/PKG-INFO | 17 + tango.egg-info/SOURCES.txt | 8 + tango.egg-info/dependency_links.txt | 1 + tango.egg-info/requires.txt | 2 + tango.egg-info/top_level.txt | 1 + 11 files changed, 660 insertions(+), 2 deletions(-) create mode 100644 build/lib/tango/tango.py create mode 100644 dist/tango-0.6.tar.gz create mode 100644 dist/tango-0.6.win32.exe create mode 100644 dist/tango-0.7.tar.gz create mode 100644 dist/tango-0.7.win32.exe create mode 100644 tango.egg-info/PKG-INFO create mode 100644 tango.egg-info/SOURCES.txt create mode 100644 tango.egg-info/dependency_links.txt create mode 100644 tango.egg-info/requires.txt create mode 100644 tango.egg-info/top_level.txt diff --git a/build/lib/tango/tango.py b/build/lib/tango/tango.py new file mode 100644 index 0000000..0b7d734 --- /dev/null +++ b/build/lib/tango/tango.py @@ -0,0 +1,629 @@ +#!/usr/bin/python + +""" + Tango is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools + +from urllib2 import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.6" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except: + raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... + elif self.authtype == "Basic": + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] + try: + test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError, e: + raise TangoError("shortenURL() failed with a %s error code." % `e.code`) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TangoError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError, e: + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self): + try: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError, e: + raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + else: + raise TangoError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`, e.code) + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError, e: + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getUserMentions() requires you to be authenticated.") + + def showStatus(self, id): + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError, e: + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError, e: + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/dist/tango-0.6.tar.gz b/dist/tango-0.6.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..8a005c42fdc4417fd37f8799e1dc1d5ee31f13aa GIT binary patch literal 6707 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 JG|d3W0004)?VA7q literal 0 HcmV?d00001 diff --git a/dist/tango-0.6.win32.exe b/dist/tango-0.6.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..2b48facf30d3e0d4c5bfa871ba80bc2bbb3eb0d8 GIT binary patch literal 67962 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 a|4q%KB*V#0NEnqDhK~%`LTRLK*#7{$*S`?} literal 0 HcmV?d00001 diff --git a/dist/tango-0.7.tar.gz b/dist/tango-0.7.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b5a0defb84e6befee4915d8316b41d619b4fe1e6 GIT binary patch literal 6709 zcmV-58p`D#iwFpg!EZ_e19V|-XKyVqE;lZ8VR8WMJ#BN_Hq!ZO{R)(sR8mfs?AS?W z-1GU|IJtTnC%M>pI-OjH2a%A(GexQdX-B=ue}8rt00~l*BFUDM-dHn^l}9 zl=5g8Z|`)U_a1EQvjd;!PoJv4&(z zxSX^$XZCfUIO+h^PCM__iT>1mXT z$QM34&ip_`p1_v6Kd`;jR+Ek`-kzVb34fdWl}#`0XqW!XQPr5JcK7G`8>J(>P_< zNt_C%$m$D^XW;Tq*&xm!)TH2RSe%Z4MD45$#bNHqu0Xegn_rGQ3)ohu)I{O+{F-eNrIFp+v4=En#|3Nc0)eZdA<7*3f7PB#&LmtBkk z;E!bEDdRxHc#^{32G^uaHL-&M_;EIl!S_;62z}i~!|FQ-5}D?mPMcSPfsamr4>+=@ zkU)pY7zrjwS?D8462d8yX*L*OJ9>s_n_<`(a~L1T2qxnQ!h~kn6$CK|eLRIBBM~FP z{SJ#0Hi^N5_i=zc3>kn8BFQ7*+iBXA48lUCjHwp-geS;SYGNc0 za$|C27*a@w0`&+;gjmM<(4YOtB?P9{PZt;O+6)mx5Y&;35*aQ0{1WLwOuiWKEKGqX zFmNAKQRN~8V#MqPNtKfJ5f1qoDAuEuK)-Pd-;juH}(Niw7cOK zU`L};8EeC2w1*|Nt14%Q0pF4cROHYRiek7of#ePoVvLPyX_XcuQ`rvW~VQ|3-?FCgRlXC@@&-yO=w(FQ4I9o-Rc&%mm{=PUon zjYooSFleRp>OPFB>BBq`J;~z{zY>orO2|u$6zun66vVzk1Gz8|A|z9bfWg?@i$QK4 zdS4O`DOC$Dh_P+(2N>S~##CGED4@0w`5Qn&-Hfnnlo22$A_M?{3DOJncac4pg0u+2 zKT-4g^u^0J=Pw~zhl&o0=s|?DKotWTfA;*JJVMtS4DUMshnstq`p@3()1CYL|1Lfs z#c=4N9{<)z`OxiW0b~J(jJ}{}hwB~sBCjGh5my0uPrP|Qt^ePe|10~SOz(XD|77>+ zu4VsE(f)V#?&tq^@_G1OFOx~HA4ENszcw1K%c-jBx?tx`yWGnDS{G0M)`TuPuTp(L zotlltL-tC~N!WUtWB8ck%Y^>vMOO`C4LbJ`F?8${a|u!08E10XcH>s3YAk>%0D~(KT9B==IqVUClo$(-Q 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 LD{Jm+0LTCUj^!F2 literal 0 HcmV?d00001 diff --git a/dist/tango-0.7.win32.exe b/dist/tango-0.7.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..3b371e028d1f92a31f6182123218b03035ac89e9 GIT binary patch literal 67963 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 bvVT+a2+44;5fMe@Md2d@#!v>S8}>f{jw``H literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index f5b217b..b9c3470 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/python __author__ = 'Ryan McGrath ' -__version__ = '0.6' +__version__ = '0.7' # For the love of god, use Pip to install this. @@ -13,7 +13,7 @@ METADATA = dict( author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', - license='Tango License', + license='MIT License', url='http://github.com/ryanmcgrath/tango/tree/master', keywords='twitter search api tweet tango', ) diff --git a/tango.egg-info/PKG-INFO b/tango.egg-info/PKG-INFO new file mode 100644 index 0000000..88d5a4e --- /dev/null +++ b/tango.egg-info/PKG-INFO @@ -0,0 +1,17 @@ +Metadata-Version: 1.0 +Name: tango +Version: 0.7 +Summary: A new and easy way to access Twitter data with Python. +Home-page: http://github.com/ryanmcgrath/tango/tree/master +Author: Ryan McGrath +Author-email: ryan@venodesigns.net +License: MIT License +Description: UNKNOWN +Keywords: twitter search api tweet tango +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Communications :: Chat +Classifier: Topic :: Internet diff --git a/tango.egg-info/SOURCES.txt b/tango.egg-info/SOURCES.txt new file mode 100644 index 0000000..ff5194e --- /dev/null +++ b/tango.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README +setup.py +tango/tango.py +tango.egg-info/PKG-INFO +tango.egg-info/SOURCES.txt +tango.egg-info/dependency_links.txt +tango.egg-info/requires.txt +tango.egg-info/top_level.txt \ No newline at end of file diff --git a/tango.egg-info/dependency_links.txt b/tango.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tango.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/tango.egg-info/requires.txt b/tango.egg-info/requires.txt new file mode 100644 index 0000000..58cf212 --- /dev/null +++ b/tango.egg-info/requires.txt @@ -0,0 +1,2 @@ +setuptools +simplejson \ No newline at end of file diff --git a/tango.egg-info/top_level.txt b/tango.egg-info/top_level.txt new file mode 100644 index 0000000..67deecb --- /dev/null +++ b/tango.egg-info/top_level.txt @@ -0,0 +1 @@ +tango/tango From c83a033765e53ebf3a93b8c8d395831304cd545e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 29 Jul 2009 00:08:26 -0400 Subject: [PATCH 078/687] Fixing a bug with authentication in showStatus() - previously it assumed that you were *always* authenticated --- tango/tango.py | 5 ++++- tango/tango3k.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tango/tango.py b/tango/tango.py index 0b7d734..87d1098 100644 --- a/tango/tango.py +++ b/tango/tango.py @@ -151,7 +151,10 @@ class setup: def showStatus(self, id): try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError, e: raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) diff --git a/tango/tango3k.py b/tango/tango3k.py index e3149be..201b3e6 100644 --- a/tango/tango3k.py +++ b/tango/tango3k.py @@ -151,7 +151,10 @@ class setup: def showStatus(self, id): try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError as e: raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) From 91e03fe7efebaf2b19738250a1c0e0792da8a248 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 29 Jul 2009 00:10:34 -0400 Subject: [PATCH 079/687] Updating version to 0.8.0.1, as showStatus() had a bug with authentication and needed to be pushed out --- build/lib/tango/tango.py | 5 +++- dist/tango-0.7.tar.gz | Bin 6709 -> 7900 bytes dist/tango-0.7.win32.exe | Bin 67963 -> 71385 bytes dist/tango-0.8.0.1.tar.gz | Bin 0 -> 7915 bytes dist/tango-0.8.0.1.win32.exe | Bin 0 -> 71447 bytes dist/tango-0.8.tar.gz | Bin 0 -> 7895 bytes dist/tango-0.8.win32.exe | Bin 0 -> 71385 bytes setup.py | 5 +++- tango.egg-info/PKG-INFO | 52 +++++++++++++++++++++++++++++++++-- 9 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 dist/tango-0.8.0.1.tar.gz create mode 100644 dist/tango-0.8.0.1.win32.exe create mode 100644 dist/tango-0.8.tar.gz create mode 100644 dist/tango-0.8.win32.exe diff --git a/build/lib/tango/tango.py b/build/lib/tango/tango.py index 0b7d734..87d1098 100644 --- a/build/lib/tango/tango.py +++ b/build/lib/tango/tango.py @@ -151,7 +151,10 @@ class setup: def showStatus(self, id): try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError, e: raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) diff --git a/dist/tango-0.7.tar.gz b/dist/tango-0.7.tar.gz index b5a0defb84e6befee4915d8316b41d619b4fe1e6..93ef50e3d79bc1d5e0e17966271c0e9b46469022 100644 GIT binary patch literal 7900 zcmV<29wXr&iwFqu#BWLh19V|-XKyVqE;lZ8VR8WMJ#BN_Hq!ZO{R)(sR8mit{MKZ~ zJ)h5wldGq3l8c?E)9H116bVT@Q>03ecGR2v_uE|nBuG(;C`)qMCz?r2k-+X^->|y? zp~MM?(e7UNS?|%tK6~(a_U*Uw?^F4A@8C%Om7hoZ`v-@I&yEfbpYA`}+uuJtc>0Ka z`)CWFR3uK!*rRyrgp1liv%%oepYD@n@_+l|KXxx(zkIV9`9D26nkRpF^t2@ZgQLT5 zAF;hJ^56L9k|&PmB+l;t@mTm#c+B>@dyUu5m>)CJjYjqWl%#Q$j>k?sJ!WSt%$Zs(9 z+1$;wa&Y%szLWRB*UEh*DsGGIA0YB_kkNQkjh)@cXe7;|T6}%)@3P+oltg z6o`g**>fHzj-L-K8TsK*K+hWv*PLB(Cw52d?CnK|y+K}g7*E^|^CAQ%I3FXdK4*h8 z2&T-9ZUK|mW!EDg_$JtR${e6&JW1ehhQ(2&O%?Hj0jL-oM=_TWKs~xyJw8LF`Z<9A?uj)+3; zX~y1i7_$p{2P{EiM%?i_EQ;AA62k8z1DqgW0NxLU69RuPlBQtM1V|E58oRs$sG@W@ z>g2ot5neRMvJqNPI3*!ynwUSH#K061haRybfV#U;6jbqvDb2)*Q3TXPYYxbpiIHYlE>-de8qWww4?wy40Ed0=2;XI`AFi+8wiyz@ z!^C$Tv@aT>Ql9J*=|YSiA2?}{0MCFi9;kGj!7oAq>=8pkV&6k%_%IYP;jTZJvZez~ zlj($?SVV`_#4%5SJV--VvM?SJ zg`$G`?BX5&E%jqg?Hoedl?!_fK>$iKVY3!m;D^2&>&j9^!egJDKmdN8A|CXLl0x#uF!MMbC(~8)!Qo9l)46F3b8G^0Ko})#n1Zc` z2{&{WdCx%h~0w>K5}osc|a~A1OZ_;Hv+l4I17<@NqJjv zK>}%mKf^cxU`>^$8Tr&Uo#fMaQW<=9=jl8khR58NoRdA8v83eZ^atXAr~F?CI8>X zXX)H`JJY}xa^215xJ%`>jVLRRU^@8*tSEf?6wVd-!zxs*!R%87I*-}856NxfLw=&*H7;LVpPipwp8;6Ucav6Q z2cuQ^W|1aaoyLw1@lKdQ2Hc-a?Xj#3!lPfBYKH;--~1IG%R!zL2H4E*+qA{tq**m! zY~|$}3Qn4KZ~uy-@c4jTI)2z{A2)U&b%6hOv8ar8q1cIS- zvYSM^lnv^lsZlATQqNuNOhmZ?-EHi=S-e=IS68q�FT1#hP{BPgtLi{LrInJP;#0 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%0D~(KT9B==IqVUClo$(-Q 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 LD{Jm+0LTCUj^!F2 diff --git a/dist/tango-0.7.win32.exe b/dist/tango-0.7.win32.exe index 3b371e028d1f92a31f6182123218b03035ac89e9..2e8219fa4e6b614823c71b67e35ace74400fba7e 100644 GIT binary patch delta 4045 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 F003<$=m`J- diff --git a/dist/tango-0.8.0.1.tar.gz b/dist/tango-0.8.0.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a58d12e40c339cf16707c94ab5388e7bfbe6ac64 GIT binary patch literal 7915 zcmV6IPC82 z`|cgU#0mS+_D=i%+B@yt&Xcu#cHr~s#S8iOrTn|IcOd`D&y(HVz5V@H2QOai?>^bt z-F>oOlX&cev&O-&Uhl~t?voVcfA!;cUmhGxlmE+u z7kef7@4edJd%||U%75*j3!XThlQ`S|;IZ(d@R04ccWUpPAwOiK8@23$Qj*$rIvhIj z_>i5jkl!;W^cZ)x%ZQV#|(ipI3BR8aWaU)cI}5~$hSvMA21A( zWOUf+^x<;aZM)I1gJKxE{n$wc9m3y9V$M55M*y(eNeUq1L-rmm3gxj|;yaigw78+FRe{p#X#z=Qm7!shx7+#{MXw$=x6>w$09PCYts%@dH2c zxnO>%$geT=+1|t_!*Z%r1AGRB$-ki&9{JGH?Y#8LFWJ z^LJo=j)+3;X~y1hn6nFd2P{Ei2Hf#lEQ;AE62k8y1DqgW0NxLU69RwFle%Eh2uKo9 z8oRs&sG_t#Xyv>B5uVqFau8ZjI3^)znwUQv#lRF2hn}$`fV%rZ6jbqvXQl9J(=|YSi?>T9Z0MCFi9;kGj!7oAq>=8pkV&6k%_%IbQ z;;!Etv$_LAlktcjSwx3b$2pIIZ5z-e%;3Foedl?!_fK>$i-(a0qoBbT6Fd$?QIJ(S7PeZ_ZQ4mD;Xi9+} z-X2c!O9i0Oc;vT7GCoCdpT2i;1Tz?Z_R+yn>;nXp$Kf|niAJ6j3^VuUN->WLW>7Yk z76UoNA*kk&#pR6P+{h6EJcgH_YnI`*(-B6IYp|PCG^}gR*&w=?`2ZRySb#V-ZmV$V zq%QlnkEN~X{J}wk?(mNz9{c130`T*c^`KXj6p}B7$m4jNOjpH&{o6d!xoHeBHh~xr zMhWYuVC!MR4V?v_fgV`;6Cc5$Uk6?HWQ@{LToiQ~9Ol4rZy7qK1OiGA{Tg!zQG~_= zH-dG*xF}8Xm;yfAxpyG0kz4m?I*q@lT%d~qcltUSlJB4xfBZ-?xd3kfu~5bbM|)oQ z227GhDbE8J>K^2v=qaBMoIC!UqNljU2+DrTXGQGpW$=-EJrfWFOxB z`0nzjceU36SiIl!Ii%R9z}6rdp*ApS-t6#@9RN|^L&;DWdJbA1q7t8^p3g&<<4AQs zD05+~o*Rs~ygp|qBg$+%{BGvv<}EkT2=WRTa2@p$(rJ^nLxic5T==5w@*?t5$V{pM zp_Zqi$|~rt*Pu_;V{vjZ)(}trke_1x|NiXc^x|wC{ht~2|G|shtp4AD+8^uxgI6!U z*8iLM%w6|xCIoDt&RuVPJ6By>+dv(=f%^5qb!%$T1B zc3t(^YU?sqn_7_7)=_}{5%PcX3T?O7-(>y|C-+P1fBOdqufFF0oA~^~`+Xbr_}5zE z^zClyLl$tv=nHywINqTz;x4pfe&>_-#FKx}g82X0r^^0EBLctA*x2ARo;TDx=$$OM~ z)oZnMxj6yQ zp6@1&+7@Q4@XaD^wpz6YwULAk$N02W&eQp!Oj@V6{a=7Yd4O;?L+uEwnZd>Cdx9XqiSIa^3HLB&G!JWAP(Hx_TiT2>_QE+W5>>V}Dx}qfVfj6(PJI5{ zxXeGge3Yb_g?LP-T(P$@|PW=)n$RR^S_q|ySuwu{s*sK!uj2UgRlEPA1(j0EUUJa zbx0r)N=MsCv`y8ZZkigMGHUhQ$IeKUJJ9Xg*5&NQ8oj!L%^}vqMlANMyMDsDeBg&3 zb>o2;d1GBr!+PKd#_ys4qcII`vma!$ZvX+3mLF(gBN$-du``3{1IM_=^ zNXKy1=%u-;mv&sehZNbOb7Rb{U>1M`kYh(xs6z_8wO$+z^%%Wj2iGAe zdX_S5YmN-tDrMN#PWvTLCnLWO{CWGiGGZ6g1RRI$e&a3`=y6c&_>c;n(RaYvuu9E% z6nuVXrX`JBOZM6?n|l9CTW`C%zrSx|4~Q zNV?F-h{co$^CR2#LqD-s_%(_f8h%p~odL+HpH6cniHqZ;G^`ql zUK2pIBC9c01>hX=DPI!EPLP0XC?G_33YQjTW?tm2fj}Rcm(;o%N7!$iKx7vUBJ`oY zWRRl|^`!z~`4=hIB3$qq*x*NMsc-4L1zMub5^&$!uXGB`3Fub0Fk#*y|P|VRL5OOm9?^IiuP^y8b(cFN59>|~XRxL((HK>Bfb8JuDi?C#$xXnN=!afi}eoc;o>EFkZkdw-EJd%9#l$0E@~YX zIRMo~2&E)0g(;8RMpt~LHdXM`@UJBoy0?xra&y zE}~7Vw#}wNaM_b!$AB)Y7?MR_2m*>k>CTN1VwE-t^c){_Qub?T1wUKuGid=-W9Ias zZarp=XM*lZW8SpbGxjgu#vt-9KuQNdaSd!k8irnJ7vG^Z#EF|Ga+O|ASY|f7fJ-{? z9#7nX4RLq`H*=e8P&sqy@RU~}7fj)DOWxS$iJgX-p?h}0{p6vfe9wd5@R)u!v=q!t=8Z5DLUVW3`=nlQxLHZ`oNx$lBbBys8{CnG;+R@VVV0qR=v zi3DM04EkC&J?mSm{qM*R8^2i3#L+VWcp4BxTkOsWQr;vTEu8Q}1-9*j@xs5MXf%Iq z8Y=W2%#xBGwR1q}1e~=I8c0Ox2gv{Yswft4S^2>7NNLMzCS^BMmgFoiLns0y5fUq; z(R4B#(vri1|1A0&@S^B&v}38JW+A6HlN^Jd3jM*oEP6E^w-Xd7#TE7ecW*(NVp-q; z3MNr7+8D(Y32kdJwLO=c=hPm7MD54G9cyxv*nJE(*UpN_y&^R9(MR&;vWuC?AO)txp%zm_)fO?B zNs^R#PK)uCye_k8A!g^MHz(|@W=8d`ut+kqOoW(VmE`B=MOQ9LCS~b+6Ge<10ZE_0 zx~E}b7Ha#R)x?ohUsVnhkY+%sf%&R2lqQ-QG%c$IM1@v?4h9(@9U{%7Hc)CW=e3?W z<)z+qBUsOri`D0p*{_S~len~{KV?60aJCFHTsIX-BzO3Nj|NHH^Lkv;*cSGUAZSp> zjX8G_NXvyBLa=)(@MvS4gUNEjUF0m%QJXJz&g%(E>?ud-IX!z7l=DL8$j96Q5?b8=*LVh$%$j!4=5tq(1v@cb>udgL@L|m>PG1WaZI0UV40LHn*Z#$ zh)(7LVMkeZzHE>fDll(=>Sfhb!+1z!DABP@SSWC1{+M7e_%pGggkXWd09ns{A#wJ- za)!Jr!E`-ykHYq(E0JlNy0Ck>= z&l6zZ!yjTC@S^+h8!8rZzpxT@lguGKIJR09RktOFT8A0atV*uIfXeewpfBnu3IbcX zmncDBz(sga5y#QE%1bQZ&L1wejv^#8V35hx<@HU$UCht!;ix(vwk`U}6a~ z(WvP1z8?}9#UkkxMC2wH(miEv(+^#48^zMl`DlJ6P8#c3eOmC^wdNF@Ly3&hS~TV4 z5kaNshVGV!>R!o$nYYpq zx|5FZ`ce);!dxZ|D+zZUiP!{+#cK3xIRE7;Uq=9puNwFO4J^oa9r{;7ut*^Q9<-pn zdM!lYbC3!Dm@JVQ(rr!l=R7aWm+>^W=ZRKjMGX19j#d?^8nUzdc9E-E0c$kWWw782~+e2JmP6kd`N|LQPLL)({>eCAuE^%AAWUWR% znf6ub#PZqSGQuvWeU$EHY0Bam-zucJEaMdkfM+V--I&8@)$JYlqsd;a>QQFjjb>za zPCj0Ye!Yho+v4HCQnoz9A6EyBUPig-$hk~3WobE3Qb_Jt9x8--b`QlW1C@eCXf$M8 zhMb1ongOUUPvsn4n(=vR-#s;x1! zeXX5uUE80vPKa-xzz1k?pMk7B)q-I5=&gJSg0aZ~7kl>(*Z`cNXsaS*6840BtlWYt zFw7N5i;nYQ7W_E&s$Ff_U{_|E3c*y{T&Wcnkj*f|O(ubp%CWA_!{Yj@;t#IjpZD_H z&ptvbCyaE^cGTbQCYwN}WI2)anH11=R1%2v$}?Skrn^LcZW?1&?|-HtYO%=kf^o~- zEiGS;FiW7)`=0W^$M@fy@;l$viJV7YwBNBpurX_$sh#dRNs??&(PT?HX0>DG2oRq^ z@~0(RtR+^gsnt4xWdR$M5U%0~UlQ>;3ZnR-t7+4uH20cXNpvGgo9z;DndB64%gLoU z-!+M)jAU7^%vCvK)B}1b zbHwMl^*#r4gRIIIF+(LGl=rS!f3+~Zly7#Qm^GcIY3n|PN_UKz6gT3{X3Mwo@or=c z9m#tP2F$*_^6qsp_8@1hWS!%AU=ISHJ90w##;d}Gth&8(0^h5Y0jmB?HG=`NFSN57 zephduwQwIUZJqtpTucP>z*?6;fG4@C$3eB0XAWg1oAoH-_5NB}tx*Aw^x+DaJ_G8y z*uS51U!$xRf$73$O2HT?mHy>gC1IeWd@1|30(pE6_5(HlWzKs8b_M&jpP5GeHyHFD z(s8a%ck&E%3gX54%f+N(y-Cl)>rro+Eyk<)zhBewvDkwWp1MN9#}bSQ zeUY+THl5VXkFw1oEj8U2E}M%JTUWO(hrjsNmM?_s)4Fdh1$u`qc zjE7+9_te91Z>a12Wj9GpdPPU}=~vq&t;8Vztgl=8S)}Xfnl7c#6)LlGUs8!^$o` z9UuNxE-ROJCe6^@c@fQCNv=RROM4eX`nkp9GV%qVKQtDx7J8=8(2C`BHrFrmvQYkh zIU1%Z+JKIks5Yo&lW4zGJu|gqDNQqrtM7xzy?vA%zl)gHQC7(*t*NHcz!xv59-Y(c zD5ciK>k{E?sIrsBI?n6FAAm-Ueaz_ZfEhj_#aB(oF6P58eG@jk!> zf;S~de%OE1W}aeoSJASfmR>fsmA5&Hz&~f5)&#$~r!$*J41t)-bO=dq54X#xB~ zJJ#`q(rQsbKP^G$YsG?B=vy!H+aKql z_5!2jt0~HL)544|Pi*CUpG2_m8uujQDMflj-kXo>5G~~0R1&##Z)UA>s6w&CI~~3( z8(2rzX01}F#4)EBp0b<5&w$Fup#;67(4n8FrB}V<_DlJbwI^~*k{($4anhIvV|vS? z{6$*$xHnTjFP)N;CrwRW0< zM#8?o{P6DdeeEtf76O@G}}=vv^T!(Wx&WlK2=w4somQkfO_XOt#6N39AmA25kRr@7j9X zwxRI5eg%;iCxz|YoCMGi6j|bJMjEGxm+fH~3e}<=A+lviR1^REo$rvMF0tFDZ5*H@ zUu?@1d3bm(@`gH#GW+EA=Y;(-;lRxXz{h4p*y0KoG2u?N%62h|H-TLD5&4u=}ZbswQi% z4RnP;HH}!{n(j1b@Q4~;{@MFi(6qN`OtE=1Wj7fssX7@W>a-j}+fRf4)s@p}2av$K zKM?=xdH8R=-r=6Ze|z=$4<#%ZIl$$IIGW#QUG=D50{?q&zdrv5d%crmhyS*T;_SAw z|7-RC{pIhS!SJ{1m&N}+?j0R8>_2@t{O^sFE2#o3Gdq82I6+_RclW#@t@(V7_3YU{ zjF5Pvd^QV!Zu4yr*WMq=Om^_hh%cUrQSF^E@Q^R65Q*kuN53V$8oSY;HP@hN7So1AX4u$h@;zZnXA{NtHUM0TGMil+LmbdPDd)Gn17+fkYoRtD zkSnUCblsex);XK9IG3pkqH8D7bu!ijoU~<)lwG06S=SRI9}3b zK zvQj^!82P-Mv9`z(4L^enk};?028fKoprd;HAINCMvF&OWFyvgu$+ToxF!qyFc&aQ= z#1aiN8iZ|ZjcD(hMIXTcya4D39_pmW8{S{k9@z;!Zd z;>_HR@o*L{b(q;jP{(=9pluAZN!3hL<2evLo~u;zk)9G7;30J6C%gf-?m?G zg?djo^>`YElGBmV+%Du8m~yQG`0v!9{MrqSdO#_eb@bs+U*hMnV0UBW6&nGg} z9a$ur2hA|8dPbBv!}~1aU{#ZA5#srAkWM8`Vpp71q>5p&BvL`zbZsCgv-pQ;FwbI~ z<#UR4nq(XMrv{nAYz6i9%Z)z9;CioQsv6L++q0Zhg{_HJp^o8I<0#}V&{-=u{3d9E z1L0rnRO|%!BtzTHR(W={)fFbEv>?av5jp5J;#B3J2KyKPDZh*ZS}Xwxr~Q+LpG4DUY%s3mU6p}Y>i4MMJ}DQ0QSjbNVE)S#&>CBp0JmeeEKDnVY$Ydg($yR{7U zTO%nM&6C#yoGIJaw=>=O9Z zJRv(pQq8tzJIruPUfkDRL0aUAd%)F%>BH&K&90vt*)5JcZ2i( zaMb6(EtAI!CG64CnRbn3Czo;wrOkY+`}(L}1dE6+Nmkrzq(Nx)7QQcfMAWaeWvEphAixYEKFh;M&c_lp&HW_IwYtUFsK)hZ zAkG%NvI+6Kx;C%g@@KN3bp=%zB@@LmU8T)Oo5doRei*&HO2V90CaW8Ql;`u}eN(e<9=Tq|2sHp?Eg=2$?g9)P|u7ypC)kFIZT z&ikY9RR1sP|H095L;sHt_fMSuZ=&4v&n~X|9-9H$0eL3o#&S^nd>Rn;)(eXF6=z7^ zXZ%d_Kb0&##PIjuD}}B+TmOF$@4x8(_j^tMzklTX|K{dj&$~Mcbj1}{Tyez}S6p$$ V6<1tw#g(75{09{Baufi_008P~r^EmN literal 0 HcmV?d00001 diff --git a/dist/tango-0.8.0.1.win32.exe b/dist/tango-0.8.0.1.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..4005412632b6428c0ca5fe9f26109578524fa0ac GIT binary patch literal 71447 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 ~>-P z-Hn~i`N}m+$B;e!re>Imudy`_VlxuFv3Phd6<@r#Ibo(_8;!7NR`43W|QgBKNp&r8gqBV3M=&{dFOnw3j)4F!G^FtQ00@Wt!E^O#!jyFao$EgZ@af_xj=_O zr4jv(s;VltJeZOSUsMwY0J~2fc!se!joqYb!WW76XPwzM2w*gF@y2 zHVrTmytm>ZBL31l4tNd3tHHJowGFrfG@$z&@EUxagn<}9GoW7vw=p=p@gUgApEo6O zdA@8I9%Dn&G&FlS8^#e}wu~m5e6VY4dJ=} z!U^Qphsm87X5qfE-&YW5`umlBy~Sl5JXoOy(N0iQq#%SJMaG9ux#Bz-j)yOSe^4+! z@5n@@7zTSnP~dF_axoZ8SmM3`G!#8@@W%qMHwKYr^Zj{$n(tWAeh}bDKq_=L{+tTG zP=>+79Su+lGotu-76wAN`y8UrL@!O_z_1J7x`K+oX2Z)M=g?n{cq-$+g999NG{EBW zkl#=s`LRKzobi1;fI+@607GEJD}w@K)GGKf`6!ZNzc_aqv!8aortb;n> z<9t8`@mCD!G4g-KCk(XpH?IJ+3^f&8kO(%+6$5k-#&?K77r^l0`miDS5P1JE9<>4T z4juC2^$l^am@$Wj4`#Tb{ENX+4SH!RF?6I!qI1|m!-wyS5e~i`!{Zg50WTJY_vdhb zkiWPsLU)A33H?0W)72z_HJ|}58g6UA=4hY`@axWD!w3%Ehxvdu!{7iip`ikLjsv|9 zCdM(~sz9Y+CmbYDN)SBO;&Q{EKL)a(n&AxxlNkw)i|8wSBKZcw#fZLhsIKBPQs5;P zBMfL9)NOvBh2BK4(W`e{C$VVXQ1D9x6;!$(GJV+h#*abmr(FeJOp$!hRz2kY{x~=i zjhe85c^EwL=jRUx{&3(A2mWy24+s8m;137>aNrLI{&3(A2mX)YfFS<9nuKBNMnIck zhvjv{n=XK{008ePeuE2RXwOpsu0!v*Qs4}apx9|Rqx{$KTW~RO2;f(b2h#l)0+_fk z>@VM+95@s3zdo!HaA~5tf!ye+4%I^fxb_p+`xQ-`@DKer@E(3h{(Em>PkZ!|o_{=k z2Yc-gNgI8H_xwZht3W=~CUy81zyJvx>Eifl*biEs@Z!Y_thKcjYier3%FD~KjT<*& zCMG5r%&%fPIy%^_S+lT-6DMNQ($cVPwHPck?!%7av;Pni@jpBMPbv4luMaT@PH-{g z7bS2c)GtF!T|SiM5xfv<99f!r@vg^xk^;7?_5?^ zWgjFUNn8(yXQA6e%l&X(hEVi48=H8+uctHmH8xp>;J@Cz$FC6tK*P#)lax`pqtX~A zd+MmPj5PX%k4jtt_^)?3Tp9nxPz0-U(z{8rSx2R10UaQemZ62lW=W5S`npN@HGX&euyQ>{W1wZI_8y}pMi%h51R%yH z9ylH7E^+-G(nIO&hQ?sBsCK~2|3hX6*QbE7=q_B3hbI0NpRo?A9pSTd_8{;{16Hpd zNC7^$w(fEIOZ#MKzS2t2O4PpbSy_1deBl?9O#(Q&3%HY{vA@HI^N^3#66Y;o(!n`Q z@Yz`BcYGw&09KzCJz<>cyL^NObF|EXeVEG!^#mB*w+Kb?nTH6w>o46xQ#4v8qm#Q

J|9Ynr` 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 G!Tv8>sM9|H literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index b9c3470..527d347 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,9 @@ #!/usr/bin/python +import sys, os + __author__ = 'Ryan McGrath ' -__version__ = '0.7' +__version__ = '0.8.0.1' # For the love of god, use Pip to install this. @@ -13,6 +15,7 @@ METADATA = dict( author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', + long_description= open("README").read(), license='MIT License', url='http://github.com/ryanmcgrath/tango/tree/master', keywords='twitter search api tweet tango', diff --git a/tango.egg-info/PKG-INFO b/tango.egg-info/PKG-INFO index 88d5a4e..1763131 100644 --- a/tango.egg-info/PKG-INFO +++ b/tango.egg-info/PKG-INFO @@ -1,12 +1,60 @@ Metadata-Version: 1.0 Name: tango -Version: 0.7 +Version: 0.8.0.1 Summary: A new and easy way to access Twitter data with Python. Home-page: http://github.com/ryanmcgrath/tango/tree/master Author: Ryan McGrath Author-email: ryan@venodesigns.net License: MIT License -Description: UNKNOWN +Description: Tango - Easy Twitter utilities in Python + ----------------------------------------------------------------------------------------------------- + I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain + things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at + a library that offers more coverage. + + This is my first library I've ever written in Python, so there could be some stuff in here that'll + make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, + and I'm open to anything that'll improve the library as a whole. + + OAuth support is in the works, but every other part of the Twitter API should be covered. Tango + handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for + Authentication. To override this, specify 'authtype="Basic"' in your tango.setup() call. + + Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All + parameters for API calls should translate over as function arguments. + + + Requirements + ----------------------------------------------------------------------------------------------------- + Tango requires (much like Python-Twitter, because they had the right idea :D) a library called + "simplejson". You can grab it at the following link: + + http://pypi.python.org/pypi/simplejson + + + Example Use + ----------------------------------------------------------------------------------------------------- + import tango + + twitter = tango.setup(authtype="Basic", username="example", password="example") + twitter.updateStatus("See how easy this was?") + + + Tango 3k + ----------------------------------------------------------------------------------------------------- + There's an experimental version of Tango that's made for Python 3k. This is currently not guaranteed + to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, + be aware of this. + + + Questions, Comments, etc? + ----------------------------------------------------------------------------------------------------- + My hope is that Tango is so simple that you'd never *have* to ask any questions, but if + you feel the need to contact me for this (or other) reasons, you can hit me up + at ryan@venodesigns.net. + + Tango is released under an MIT License - see the LICENSE file for more information. + Keywords: twitter search api tweet tango Platform: UNKNOWN Classifier: Development Status :: 4 - Beta From 0101d3b3e4b01ce2309aacdaca6c9469aea41902 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 29 Jul 2009 12:54:33 -0400 Subject: [PATCH 080/687] Fixing a bug wherein self.headers doesn't exist - reference headers arg instead --- build/lib/tango/tango.py | 4 ++-- tango/tango.py | 6 +++--- tango/tango3k.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/lib/tango/tango.py b/build/lib/tango/tango.py index 87d1098..340ba59 100644 --- a/build/lib/tango/tango.py +++ b/build/lib/tango/tango.py @@ -63,8 +63,8 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] try: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True diff --git a/tango/tango.py b/tango/tango.py index 87d1098..e16bbe9 100644 --- a/tango/tango.py +++ b/tango/tango.py @@ -63,10 +63,10 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] try: - test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) diff --git a/tango/tango3k.py b/tango/tango3k.py index 201b3e6..77f930e 100644 --- a/tango/tango3k.py +++ b/tango/tango3k.py @@ -23,7 +23,7 @@ except ImportError: try: import json as simplejson except: - raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + raise Exception("Tango requires a json library to work. http://www.undefined.org/python/") try: import oauth @@ -63,8 +63,8 @@ class setup: self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] try: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True From 0d086c53952ed115b70f30c3584b5403401e2f3f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 29 Jul 2009 13:47:25 -0400 Subject: [PATCH 081/687] Name change, inserting a basic docstring to get the ball rolling --- tango/tango.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tango/tango.py b/tango/tango.py index e16bbe9..e728bf1 100644 --- a/tango/tango.py +++ b/tango/tango.py @@ -1,7 +1,9 @@ #!/usr/bin/python """ - Tango is an up-to-date library for Python that wraps the Twitter API. + NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. + + Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -15,7 +17,9 @@ import httplib, urllib, urllib2, mimetypes, mimetools from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.6" +__version__ = "0.8.0.1" + +"""Twython - Easy Twitter utilities in Python""" try: import simplejson @@ -23,7 +27,7 @@ except ImportError: try: import json as simplejson except: - raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: import oauth @@ -86,13 +90,27 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: + """ + function:: shortenURL(url_to_shorten, shortener, query) + + Returns a shortened URL. + + :param url_to_shorten: URL to shorten, as a String + :param shortener: URL to an API call for a different shortening service, allows overriding. + :param query: Name of the param that you pass the long URL as. + + Example: + >>> twitter = tango.setup() + >>> twitter.shortenURL("http://webs.com/") + + """ + try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError, e: raise TangoError("shortenURL() failed with a %s error code." % `e.code`) def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) def getRateLimitStatus(self, rate_for = "requestingIP"): try: From 4a86adb9779ae41879d4fece11aeda72a2a28275 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 31 Jul 2009 03:31:08 -0400 Subject: [PATCH 082/687] NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". In renaming the GitHub repo, most watchers/followers will be lost, so please take note of this date if you wish to continue following development! There should (hopefully) be no further disruptions after that. Eventually, I'll get around to creating a setup.py file that works correctly. ;) --- README | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README b/README index 7eb7671..e150c66 100644 --- a/README +++ b/README @@ -1,3 +1,9 @@ +NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". In renaming the GitHub repo, most watchers/followers +will be lost, so please take note of this date if you wish to continue following development! + +There should (hopefully) be no further disruptions after that. Eventually, I'll get around to creating a setup.py file +that works correctly. ;) + Tango - Easy Twitter utilities in Python ----------------------------------------------------------------------------------------------------- I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain From 983f8e3065a36d3c7b5d7f86d56d4672a6a85aed Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 1 Aug 2009 15:34:39 -0400 Subject: [PATCH 083/687] Huh, that indentation was off... backing out this comment for now --- tango/tango.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tango/tango.py b/tango/tango.py index e728bf1..aa731ae 100644 --- a/tango/tango.py +++ b/tango/tango.py @@ -90,20 +90,6 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - """ - function:: shortenURL(url_to_shorten, shortener, query) - - Returns a shortened URL. - - :param url_to_shorten: URL to shorten, as a String - :param shortener: URL to an API call for a different shortening service, allows overriding. - :param query: Name of the param that you pass the long URL as. - - Example: - >>> twitter = tango.setup() - >>> twitter.shortenURL("http://webs.com/") - - """ try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError, e: From cbfa71c286537ce8de2e6155ec60e282f9476be9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:36:08 -0400 Subject: [PATCH 084/687] Changing over to Twython, rolled back version number to aim for a more consistent release schedule - nowhere near a 1.0 release yet... --- build/lib/tango/tango.py | 632 ------------------ dist/tango-0.6.tar.gz | Bin 6707 -> 0 bytes dist/tango-0.7.tar.gz | Bin 7900 -> 0 bytes dist/tango-0.7.win32.exe | Bin 71385 -> 0 bytes dist/tango-0.8.0.1.tar.gz | Bin 7915 -> 0 bytes dist/tango-0.8.0.1.win32.exe | Bin 71447 -> 0 bytes dist/tango-0.8.tar.gz | Bin 7895 -> 0 bytes dist/tango-0.8.win32.exe | Bin 71385 -> 0 bytes dist/twython-0.5.tar.gz | Bin 0 -> 2713 bytes ...go-0.6.win32.exe => twython-0.5.win32.exe} | Bin 67962 -> 67389 bytes setup.py | 10 +- tango.egg-info/SOURCES.txt | 8 - tango.egg-info/top_level.txt | 1 - {tango.egg-info => twython.egg-info}/PKG-INFO | 16 +- twython.egg-info/SOURCES.txt | 7 + .../dependency_links.txt | 0 .../requires.txt | 0 twython.egg-info/top_level.txt | 1 + {tango => twython}/tango.py | 0 {tango => twython}/tango3k.py | 0 20 files changed, 24 insertions(+), 651 deletions(-) delete mode 100644 build/lib/tango/tango.py delete mode 100644 dist/tango-0.6.tar.gz delete mode 100644 dist/tango-0.7.tar.gz delete mode 100644 dist/tango-0.7.win32.exe delete mode 100644 dist/tango-0.8.0.1.tar.gz delete mode 100644 dist/tango-0.8.0.1.win32.exe delete mode 100644 dist/tango-0.8.tar.gz delete mode 100644 dist/tango-0.8.win32.exe create mode 100644 dist/twython-0.5.tar.gz rename dist/{tango-0.6.win32.exe => twython-0.5.win32.exe} (90%) delete mode 100644 tango.egg-info/SOURCES.txt delete mode 100644 tango.egg-info/top_level.txt rename {tango.egg-info => twython.egg-info}/PKG-INFO (84%) create mode 100644 twython.egg-info/SOURCES.txt rename {tango.egg-info => twython.egg-info}/dependency_links.txt (100%) rename {tango.egg-info => twython.egg-info}/requires.txt (100%) create mode 100644 twython.egg-info/top_level.txt rename {tango => twython}/tango.py (100%) rename {tango => twython}/tango3k.py (100%) diff --git a/build/lib/tango/tango.py b/build/lib/tango/tango.py deleted file mode 100644 index 340ba59..0000000 --- a/build/lib/tango/tango.py +++ /dev/null @@ -1,632 +0,0 @@ -#!/usr/bin/python - -""" - Tango is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.6" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except: - raise Exception("Tango requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TangoError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass - - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): - try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getUserMentions() requires you to be authenticated.") - - def showStatus(self, id): - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("Your message must not be longer than 140 characters") - else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) - except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.urlencode({"url": url}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.urlencode({"location": location}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.urlencode({"description": description}) - else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/dist/tango-0.6.tar.gz b/dist/tango-0.6.tar.gz deleted file mode 100644 index 8a005c42fdc4417fd37f8799e1dc1d5ee31f13aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6707 zcmV-38qDP%iwFpN!EZ_e19V|-XKyVqE;cT7VR8WMJ#BN_Hq!ZO{R)(sR8mfs?AS?W z-1GU|IJtTnC%M>pI-OjH2a%A(GexQdX-B=ue}8rt00~l*BFUDM-q^l}9 zl=5g8Z|`)U^&V{OZwLOKKYgk`pQ+ECC;RGC{XN*-eX_Ure1GTZ^SuWxWNFaM4Ior#qDJmuT}ClVRN(E;1-?lj);u{dC)8;$${C`scy8;^N1Jz&Qy64#7J zJ`-F{*)^ZCG-llMgp};!I!IHIFa*rtdc@vM(@`9C8$ZQku|45KfIdpo$w99-gvVLG z>&4?9ieT&w6P}KGgu9m}LiEO50N9e0>ZP?3#Pv`PK5N5V3N|nKD_z)&D&qzG=3J->p1Zxj9d>u3Z8f) z#wP*X5+cx+;}84Lme1fGa15yq3Th*EZ*@-&m|;DGJ3ZT2HDyVUeF zN=4)gpB-m@ARh)s8)B3hJxCABe;QsGV;Q!yhJU%&l zxvBWS!2j<*-F=e#fAIb2|9781yZ8Th@ppmp2iNoxy}0q)SsH{vif$o@v|DIw-^E|! zlwBurDwraxFFc-s%R6O*ID=4=g0Eq5Isy{4voaKixg)z4iHMr1YMV_kDnS?$UG_pG zDG#DXItrqpgdUdy8VPo;eEjjd(++!!@ub5<>UEeOBN!>f7-94U8)RWPWga-)MEG5H zF$#b`l8vW~0}bOz3ZD(GNttS52LtfqY#f8{rJfM_x{Zd_cMv2p%{!eouLJ`hod6$j zWKki34wEqwOpvnBN0KCjQzp}FFu->74AC~jurcN^K8_Jg#u0=G&9Ey7Vi5Xx3PVOB zMuPht7AI^Hg9q>90C^ZP02@S-N5Hq!v?&>cqX@8Ni6=UMBF=`RP6MO=X>+U|V)T!u z#9TuI3&s?qh|Ok%Tmn_sqc}u95WktsCX+Zxks&Y&Yz$%eQg&EBONkj%E%XUbkfqeb zNFLE`;kisOs$_TF5a~nB8DKSBN-(!TKM@T(u0_MG2mI4 z0#9JzKB%I~MT*{lEoO)~349!E0Hn)_@PfgVH90g*rxS7L5PqkLW1IpfRW8w$BF!eP zHnANzauRzPMrTD9Pyp!C5oAZeSH%!z8d~Kf@_ig62}rQY6~F`9$djSK^sLK{K`VfR zklLlFlK_~SA!2D*m?k`uklhFx4+?9LMV?|&G9=0*P}z7de$RqL(DyrX4QkwpLM&Np zoPi3H6)V-=)?@GFZUY!HWh3qrXOdtz0u}QGX9p*3R@f()V5g5E%CI6UT4t1Egqo z!!N*&Mx`>=hRJ9TOKMkD&JF{W_Bo!nkP#`n)mNXoxEl#ingQp`HKBi^2YRe{e} z{*fDx1m9rLO6k>o7**4Uc_Mm}$0L3v9#fQ%ml!G7@5LyHeS-#aVIV|ErWOH%vAGw6 z+&uKYBpy<#7F-Zx+u#o{z5$G>w%AcXZ6WeEfP}gkVb>@lKuSai000xD7wGRIdoBfO z5rluD=Jn}|mv7ErLbMJQ9Td@n2xoyR1~mTc`9FDtt~VIob^Z@G_bT`;P=KZw(e{24)?0+)7^ZEZ1 zG}nWx}J6OQUb(g2(J zeV0nght0YHV=KQXARji@D~2POP-Pm2O)Krd`&k0GSFDg@ze`=hK5Q;7VME(CGW;Bk zK8<6Gg9!F0@c{sYA!CH;~=-1U#Od_M#~k(OG=H#v*9GGNBWxZtm>$)Aw(zaTf8|fUjrIR0`}_Cn|6fJ^ zsycMLS>WRQ@7dn-T>g8{p6_7(x4*l$f1m$-rTouvoW@pOA%Wy5n{21?Hq8c2(bTGx zQK{!T;S*V{KzAEkZx=7tnAH_54zU3iVzFl34^q|_qagCB8V|&%j!j7g>k*esT*V>A za%$XVKdEBh2*N8ZKhVNPIKiZH>*DRn+XGYFKZksYV=Wya9l=wpmgebN+EM*J0%VIR zs$ymaqkv>K39Zj39X1Ze0xPOgUsBj^43c%UXUrsWx9hUp8UOG*{MhOofk$Oqr0cXpp6ys6wMbBJIT7{N8={{>ST`%pL zy&u=t*Ig{U4T4Dcx^RxIdss0p4JJkg)sF+K(~SltlByC(4_Y~~m{4JM<+?!>q^{eN zA{>Bp|jI96;Pzt!8k};(7J6< z$u!ZJ2xbB~QxXBcCP~Y{ZyTa>00s5)X|5%4b)1yM)gv)$0;pDGHP)&C+(SMUO9J@~ z5|9hi51HS>rA3w3mw9U;Fqifty{;w@_A4IB{GmmJxipU~a?GW9R3fbSqU2hJ3tj^Y z{75ZL=E|V0rzCyTy%IeFzFTrXMJTEEnQb50Rr9KbYhxf19 zIkG@RIO=>0!YKRW4?a>*rFsrcH9#N(fVuj9$21^gFzOV@?$GaXT&y2Wz%GG=1ohDg z<7fx~93sSMt&X6^lHikcvjF@T<{UZjziNREM1*2({g(hARA~C#ZbLi(bw@>=r-!pz zXpOX8(m~EZy(l$dh_!2LSle*l1D!~d%uA0aLBXs(2SowuFp`5)m3GEpp5?=HzIVF+ ziG!&1(Rm~fA4$N|f*9IiS3Jx_n{>2vBZxHEZUDv$|HiV_{;h4P(0edTN_zCp0j(2o z*F|U`5v?B}|I>G6v4G1e8dh5>Th=3~x|ymZ7kL>%5h96@SRswJlM#@X9F+WL+24Q{ zMTcV?OFcC!IlYDCSoGBB5AJ0#tLeC%phPLIu#bdy3CfhH2_B$e3I(H$QB0H2byic` zQ?+?c?-5A#ehl2PrZ$OPCt!2kd{Q{XDIhN5SG4*EhSgYn;K$gl0D$*Y+JGDN`r>G= zht*2=F6)PZcLCF~5GEXjd#l*{I$EO`CS-3yINVetv81#-=VnP=!|nmdE0|+PzAO#6 z!yZ4ryynSJnf#a4HxJ>1GBotjhpKbc#Vlly22^l;G?t5<%X)$ed#X`-NzYyfWqG&#YJt~)=`WSZU~q_pjbPf)(;UIr z7$i(B=*c784=7{W(1v@cP2{$jL@L`Cx})-f*k&jUuw2Rxjel`mL??5Gu;V;CUpGjM zHJDdG^}1?mVB91!ROnbIER?vid`z$y{Ds(1L9jw#fUM`blsE_eGKtJm31=<_MACbt zr2qp29AC#Ce*Ll`=tN+i4X`g6L1`DWVyWT-uokzyO~NV7ws9IeKA^ zx1tD&Iiv?iPN!zF>!_yALC!R%mTShf#9N4KA&5G(<{xv0hcQkvYe?- zr8Zo*da(+pi}Bnxll3btFjdBE0pAQ$Xdp?W7m_fq!8t z?kRVhLF5V7nxl;QC;KaL(pp9u(mKtsI_;cg=T znL)AI)c73kf4j=plK|sh18<;#CHZbb|0)Pp$-lvambBL_K}dZ4FcqKDH8Ml$)@FY# z^1@>E%y4^|XjN6jNL-s}Rg=0Q$&i;R${nNuzTwAbQ(9hOy=;~*>&eZYx!1+9Bu|RR znM_HG`?$+I!e!lb#G^`*>?{!)2~u5oT&i%1JG>QZ4f5HvuTCe{&;HgCb~WvzbT3a+ zR?qluLz?R{UXuWL=JMT31hm#g)=@B-Es^TdVE)~z0p|DATfOMl2bi&~9u6FB%M1K* zeb5->l*^tP%S-dbX=sGTK*rU-dDv}U-VOy8L>2Qa409>j zOiX&n-skIKxY`5*^s)Zz2Y94|dL5X`o8~=w{U)m3kwUx1y2XyMy+zA&c<2MZw~EXA zYVN7m;qo_L)!75q-U3`>eX`F+;0$F~PeNv4&)CPUTW}4Ar2=W$ao)^=zmB~cS6emM zTQg0KV6JW6suh-yEil8oOaf<>V^fuf)%91$A6z;=9TeSPeu7j^TIrz6^=JPsi$Jzy z1(EcH6wu{52}F9;oxZu#U!y)Zk1@CJf94_Tu-Ny*X~*8Atj;7@Bv9pjPed5tu`?&) zD)3Aq=hGMCcbo`p%-LjWXR1z$B-ht8xr&ZOP90Q6Aih_6vL~cdU_=_rEP>%lGO{Yh(;vsXi717I&}hy(z}-7mQV` zgV=qB3K62 zrUU{!DO5cP>$N<0C=1zaS`n}JHp*&)3V38LZ-MCxpl*u&djJ`6=aj(0{K385|x}BMyp`vE(X|2t%`KUIr1Y<*AW!kNZPP+3~*=CXRd0yR? zitZ~vzxvjeZ-ndfs&8wty*jin@_lQ~GSga&hhP~D^gC#;sOtS~7fEe; zWlz=VZ?{TXi$Q&Cu3GvJSOuq*5yc^#5I{ z^bE>xRH2o!Y*e2OiWM_JRfIl0k?XBPS2D)3($?$jGV43N0RGi3D_3_WEzsR%5iMRx zu0gm+dsjpHrN!ea@)ckEvlg*ddX>-8iuH81&@b|`F#Y{@G|W(R2Rde>xhAA|3 zmSymblhQIjZpPaa=sPWOR{ubS-cX_jxh{Ksgq}0koG*9AlgkgX^fn)^W+L7>L6aDc z4ai611wO2Sr8A*Tl0_HyBsF?%fzSS;)PFqqK^4Z;p)(VY18U23Lp@5#;y|}Y{!PH&c4%M z&5@$|#N9z4LfYZ?QGLgsPXt^po{0=xi2bRV(y95En~xpO(;VaYTMCB4sLx$0PW}CF z^MwMreE}ew2ds=CfBITP!*tZdGhvbzZtp|f?e=G-fz|=V@73|0t8Aq2=ZtrWJIWg= zc7rA0g&J#q2zh(q#{R@Qmv!x}_*HG#$jh=oW*GdT&cj@MrYD}bi$9N$Q^kl?9rZ)F zM{{=S*D3P7?%t;E77FX!y^elMnyDEwPc7$**l4FMXe8{%w;$e|9KZh+e-**}^hQ_x zs;=8d3*gC=*!SZ7>sNSw{lAib(ee1Njmp&>WUTPez+ZRy1VrJtn%mn=@G}NsyZq2- z(Mcd7k^~q*4sfgPn4-%MOqIzWNv8>%25gn?&GL6eNr||^nrt$G4I82Iha6u{beW9y z9LQ*62cTTogVY(14!13?H4nl(7R(zOT;iQ_6Z)#121;BVVf`BK+C-otsOgD$dECbx-yg>7{|zI8+TJ|Gk**Fr(jWDDmav&TkCF-h?rQ|KHx(^d<;{as2*# z3T`_UX*W3ZW|Y0eh(_afG-M-QG@95Y^V_c#6iZOqM2yM*=gk%qY@er3dC4z(G^Mrt zREc8it;ptqYW6?5$!x}q4z_hu|JM_L6!3rD!2gwx|5swdNN3^A)VS`~uKH2e@;`gr z9)F=f9{gWT052o_kEQWH?7gtJ=y`B`|1;;h&CdV65b!@YLO)Tlsdw0Ws*{rrcRh-9 z@gY-FFzp-kB%$&5Vi6m&$^Vqc^txOm%sjXtoj=-*slAoXNas+Gmwn07SCz}XeVOOi zlKxURe`8KMlc*OBJ|gO6@??GHC*{dBoX5&kius|Py~kKOrv=7V*E1ett8X=(v31BL zR^##?Dwi9V_uV;;vBcbC{@-1hy`b)^T>lBbkpBZ8`M((s|9sI^np6 z@IM>>!Vik^?+K3hZ-PYj?e6mo5>j-pWttqytpw5<|AN5B`rq96r(=&g9RL6T000000000000000000000002)h+n!G JG|d3W0004)?VA7q diff --git a/dist/tango-0.7.tar.gz b/dist/tango-0.7.tar.gz deleted file mode 100644 index 93ef50e3d79bc1d5e0e17966271c0e9b46469022..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7900 zcmV<29wXr&iwFqu#BWLh19V|-XKyVqE;lZ8VR8WMJ#BN_Hq!ZO{R)(sR8mit{MKZ~ zJ)h5wldGq3l8c?E)9H116bVT@Q>03ecGR2v_uE|nBuG(;C`)qMCz?r2k-+X^->|y? zp~MM?(e7UNS?|%tK6~(a_U*Uw?^F4A@8C%Om7hoZ`v-@I&yEfbpYA`}+uuJtc>0Ka z`)CWFR3uK!*rRyrgp1liv%%oepYD@n@_+l|KXxx(zkIV9`9D26nkRpF^t2@ZgQLT5 zAF;hJ^56L9k|&PmB+l;t@mTm#c+B>@dyUu5m>)CJjYjqWl%#Q$j>k?sJ!WSt%$Zs(9 z+1$;wa&Y%szLWRB*UEh*DsGGIA0YB_kkNQkjh)@cXe7;|T6}%)@3P+oltg z6o`g**>fHzj-L-K8TsK*K+hWv*PLB(Cw52d?CnK|y+K}g7*E^|^CAQ%I3FXdK4*h8 z2&T-9ZUK|mW!EDg_$JtR${e6&JW1ehhQ(2&O%?Hj0jL-oM=_TWKs~xyJw8LF`Z<9A?uj)+3; zX~y1i7_$p{2P{EiM%?i_EQ;AA62k8z1DqgW0NxLU69RuPlBQtM1V|E58oRs$sG@W@ z>g2ot5neRMvJqNPI3*!ynwUSH#K061haRybfV#U;6jbqvDb2)*Q3TXPYYxbpiIHYlE>-de8qWww4?wy40Ed0=2;XI`AFi+8wiyz@ z!^C$Tv@aT>Ql9J*=|YSiA2?}{0MCFi9;kGj!7oAq>=8pkV&6k%_%IYP;jTZJvZez~ zlj($?SVV`_#4%5SJV--VvM?SJ zg`$G`?BX5&E%jqg?Hoedl?!_fK>$iKVY3!m;D^2&>&j9^!egJDKmdN8A|CXLl0x#uF!MMbC(~8)!Qo9l)46F3b8G^0Ko})#n1Zc` z2{&{WdCx%h~0w>K5}osc|a~A1OZ_;Hv+l4I17<@NqJjv zK>}%mKf^cxU`>^$8Tr&Uo#fMaQW<=9=jl8khR58NoRdA8v83eZ^atXAr~F?CI8>X zXX)H`JJY}xa^215xJ%`>jVLRRU^@8*tSEf?6wVd-!zxs*!R%87I*-}856NxfLw=&*H7;LVpPipwp8;6Ucav6Q z2cuQ^W|1aaoyLw1@lKdQ2Hc-a?Xj#3!lPfBYKH;--~1IG%R!zL2H4E*+qA{tq**m! zY~|$}3Qn4KZ~uy-@c4jTI)2z{A2)U&b%6hOv8ar8q1cIS- zvYSM^lnv^lsZlATQqNuNOhmZ?-EHi=S-e=IS68q�FT1#hP{BPgtLi{LrInJP;#0 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 AYXATM diff --git a/dist/tango-0.8.0.1.tar.gz b/dist/tango-0.8.0.1.tar.gz deleted file mode 100644 index a58d12e40c339cf16707c94ab5388e7bfbe6ac64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7915 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 V6<1tw#g(75{09{Baufi_008P~r^EmN diff --git a/dist/tango-0.8.0.1.win32.exe b/dist/tango-0.8.0.1.win32.exe deleted file mode 100644 index 4005412632b6428c0ca5fe9f26109578524fa0ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71447 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 ~>-P z-Hn~i`N}m+$B;e!re>Imudy`_VlxuFv3Phd6<@r#Ibo(_8;!7NR`43W|QgBKNp&r8gqBV3M=&{dFOnw3j)4F!G^FtQ00@Wt!E^O#!jyFao$EgZ@af_xj=_O zr4jv(s;VltJeZOSUsMwY0J~2fc!se!joqYb!WW76XPwzM2w*gF@y2 zHVrTmytm>ZBL31l4tNd3tHHJowGFrfG@$z&@EUxagn<}9GoW7vw=p=p@gUgApEo6O zdA@8I9%Dn&G&FlS8^#e}wu~m5e6VY4dJ=} z!U^Qphsm87X5qfE-&YW5`umlBy~Sl5JXoOy(N0iQq#%SJMaG9ux#Bz-j)yOSe^4+! z@5n@@7zTSnP~dF_axoZ8SmM3`G!#8@@W%qMHwKYr^Zj{$n(tWAeh}bDKq_=L{+tTG zP=>+79Su+lGotu-76wAN`y8UrL@!O_z_1J7x`K+oX2Z)M=g?n{cq-$+g999NG{EBW zkl#=s`LRKzobi1;fI+@607GEJD}w@K)GGKf`6!ZNzc_aqv!8aortb;n> z<9t8`@mCD!G4g-KCk(XpH?IJ+3^f&8kO(%+6$5k-#&?K77r^l0`miDS5P1JE9<>4T z4juC2^$l^am@$Wj4`#Tb{ENX+4SH!RF?6I!qI1|m!-wyS5e~i`!{Zg50WTJY_vdhb zkiWPsLU)A33H?0W)72z_HJ|}58g6UA=4hY`@axWD!w3%Ehxvdu!{7iip`ikLjsv|9 zCdM(~sz9Y+CmbYDN)SBO;&Q{EKL)a(n&AxxlNkw)i|8wSBKZcw#fZLhsIKBPQs5;P zBMfL9)NOvBh2BK4(W`e{C$VVXQ1D9x6;!$(GJV+h#*abmr(FeJOp$!hRz2kY{x~=i zjhe85c^EwL=jRUx{&3(A2mWy24+s8m;137>aNrLI{&3(A2mX)YfFS<9nuKBNMnIck zhvjv{n=XK{008ePeuE2RXwOpsu0!v*Qs4}apx9|Rqx{$KTW~RO2;f(b2h#l)0+_fk z>@VM+95@s3zdo!HaA~5tf!ye+4%I^fxb_p+`xQ-`@DKer@E(3h{(Em>PkZ!|o_{=k z2Yc-gNgI8H_xwZht3W=~CUy81zyJvx>Eifl*biEs@Z!Y_thKcjYier3%FD~KjT<*& zCMG5r%&%fPIy%^_S+lT-6DMNQ($cVPwHPck?!%7av;Pni@jpBMPbv4luMaT@PH-{g z7bS2c)GtF!T|SiM5xfv<99f!r@vg^xk^;7?_5?^ zWgjFUNn8(yXQA6e%l&X(hEVi48=H8+uctHmH8xp>;J@Cz$FC6tK*P#)lax`pqtX~A zd+MmPj5PX%k4jtt_^)?3Tp9nxPz0-U(z{8rSx2R10UaQemZ62lW=W5S`npN@HGX&euyQ>{W1wZI_8y}pMi%h51R%yH z9ylH7E^+-G(nIO&hQ?sBsCK~2|3hX6*QbE7=q_B3hbI0NpRo?A9pSTd_8{;{16Hpd zNC7^$w(fEIOZ#MKzS2t2O4PpbSy_1deBl?9O#(Q&3%HY{vA@HI^N^3#66Y;o(!n`Q z@Yz`BcYGw&09KzCJz<>cyL^NObF|EXeVEG!^#mB*w+Kb?nTH6w>o46xQ#4v8qm#Q

J|9Ynr` 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 G!Tv8>sM9|H diff --git a/dist/twython-0.5.tar.gz b/dist/twython-0.5.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..d2a262ee93e5c63fab078be9e7171a6fedbaf2c9 GIT binary patch literal 2713 zcmajR`6JT}0|0PGxsM7@ZbF7e&Zp_YN<&0)b`0e$T>NoFS%Rl%qN3 z%$Xxcay+hK3ES-Z{s-^-`ROBv<>8s|LRxTe28E!4eEgrQYij6e1i8Lo+mZHrIoJH{ z@zCRs<><8R^uq>EqVHt&$e+t^zowd6{F9GQ;!?QDt7o#uHk!x^54o9`Au|(r;ZA|e z$4q@Y`Q``PVgLDxo0+%tXj`mk+WJT8HCI<4DgV@v!utDB{LG>N^6k*nO~&5FQdAFq zXa%z-L9s30mgmE^;e7LH4v&!d>HBx;uHW zw4l!AS|W{}g@CQ+mqJRf@3F6U#sYE5YZwNa0-Inr-amrN4n+BBr39<5^@LqT%lbJT zlw6Bq#)tGY)xq4hr!?xg+Q(@^PUO*JyY_E2M9F06eb%x0Q={p``?`@?#()U4q1Fuxx-5%bfV5Gei5_y7SXjMg* zmCnD&#<*USc0)k=&V5w4)Mq245YuA0uWEQn4J{#czFb=SE9_>6KT0W6HFe0EtVSlY z^)D5Hw39i$R7d(BT9 zS`fzDioDZHSy#GYZCyIQDtbURmu|Py^Hd`_K-2ANPU*b%(NbUdqLqT)(h#*D92@%9 z;ZCX*#TzyWdy7Fr0tlS4ioRV<*Nd?={e9Orj}m9Gvu;{NKc=QC?V@!=`Sil0OYeVP zje1*=W=)`ls%llQ_G}jWPo6WM+>p9~*A+k%w3~Sp#fjaq>X*M=mz$$C`(*e#-2Mi^ zOIuUkO*!wZLmJmZq*RGMxT~gBP(<0l!9QC?|-BGH}Q%0jluJ5>6MXfyE$7VaDP zxZ;F=U#e0`@mh$$%YTPnlD8MMFpM7CvssuDk9Y8&GbsUJ*GKPqd2-yd8KHg%W1G@c zWaehOZQ;w@zK1y#Ghnx#-y>xdMS;PI)upvELUtbXy3d3ROMlto0q9(DA+b^gMNTr_ z!fGAVN5RL3RysR}w8u_V--~O#7tgxg2{D1E3O@e!@VCUTXq`v}9+{Kii9>m;4OXR= zx~YAAkeB8kB`uqoic$7YnKq4uf(weqQu z@8STBzyB9UJ9|$T*hsT+Fge{EXXCZf7yBZL%#NZ=iy#Lc&uvuttGw=_4mdwV#-EM& z>C%VtaE1*wp26F~*X%?ael}4^@fRKC#eREdam&}Db`i>5!&aG|;@ABAUeDgTp31NC zyphVLiB$}dtD6$cO?E?iCZJOLK2~!iNUR zdPe4)o$iI>gG^MFk>k1sGG3>aZU`}d`dnm`I$44^OpmtIZ0z6-myoQUtR@8WtSbzW zBDtVzKWgww)q zByD43BeeY6Q<(*N9&=#pBdOQ9Ul}l$e5l)&Sh{2IivgLnrM%Y$~3gZu~k6_aMAVS$6RqkrKk4 zc6mWyq^oriuQ(%}FCGJqJ!S`IG{d;EtG7{gOVe%W#EM5`OFu9tp0B&9Ugk_qtY9`7 z#r218_>AKZ(RK^Zjd(!BLb7+S=>gEvP@U%Y-{_=s)(dMBT~IFPYQ^(tm_9| z`LIYKUo@RrJOJfoJZlQ`#;>}eeW2Q#In&SZUS|ia6I)lQj2nrgFl|#R>Gi-AE3B;M zCcJ7wQ-=SKny${B;?~t{bfRV{UnO&mTD{SaXBwIinN3Hc5R$R+X6BrxZSC#KNTcIW z!{*jki*gaF^-h%Tk{+dp8yw~>e5;;T-mwa9Pzz2YOU-<5cO!!3R@aR(X)2vO$Bw1U z{ZNOpQer-}z*&@m^rvIv3yB^tBYy?=847GIV&?ILPuNKN0@lWsNYP*r&_?Fg{!F0-t7&=+NFB%&lo)hO4Gc+)3GS7@T*Sd3sc5 z6ag`$0!1g0lPvhoS~$a>Fb@FUSJJ|5f2z}i7BEz{wq8Pa*jV)~>Nhspsz^< E0ifA-(*OVf literal 0 HcmV?d00001 diff --git a/dist/tango-0.6.win32.exe b/dist/twython-0.5.win32.exe similarity index 90% rename from dist/tango-0.6.win32.exe rename to dist/twython-0.5.win32.exe index 2b48facf30d3e0d4c5bfa871ba80bc2bbb3eb0d8..90e480b6dd136bd09497b07c2a4da6aaf4ee0eca 100644 GIT binary patch delta 5237 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 delta 5784 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 HSD=3ZI;%=! diff --git a/setup.py b/setup.py index 527d347..d73134f 100644 --- a/setup.py +++ b/setup.py @@ -3,22 +3,22 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '0.8.0.1' +__version__ = '0.5' # For the love of god, use Pip to install this. # Distutils version METADATA = dict( - name = "tango", + name = "twython", version = __version__, - py_modules = ['tango/tango'], + py_modules = ['twython/twython'], author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', long_description= open("README").read(), license='MIT License', - url='http://github.com/ryanmcgrath/tango/tree/master', - keywords='twitter search api tweet tango', + url='http://github.com/ryanmcgrath/twython/tree/master', + keywords='twitter search api tweet twython', ) # Setuptools version diff --git a/tango.egg-info/SOURCES.txt b/tango.egg-info/SOURCES.txt deleted file mode 100644 index ff5194e..0000000 --- a/tango.egg-info/SOURCES.txt +++ /dev/null @@ -1,8 +0,0 @@ -README -setup.py -tango/tango.py -tango.egg-info/PKG-INFO -tango.egg-info/SOURCES.txt -tango.egg-info/dependency_links.txt -tango.egg-info/requires.txt -tango.egg-info/top_level.txt \ No newline at end of file diff --git a/tango.egg-info/top_level.txt b/tango.egg-info/top_level.txt deleted file mode 100644 index 67deecb..0000000 --- a/tango.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -tango/tango diff --git a/tango.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO similarity index 84% rename from tango.egg-info/PKG-INFO rename to twython.egg-info/PKG-INFO index 1763131..f0758a0 100644 --- a/tango.egg-info/PKG-INFO +++ b/twython.egg-info/PKG-INFO @@ -1,12 +1,18 @@ Metadata-Version: 1.0 -Name: tango -Version: 0.8.0.1 +Name: twython +Version: 0.5 Summary: A new and easy way to access Twitter data with Python. -Home-page: http://github.com/ryanmcgrath/tango/tree/master +Home-page: http://github.com/ryanmcgrath/twython/tree/master Author: Ryan McGrath Author-email: ryan@venodesigns.net License: MIT License -Description: Tango - Easy Twitter utilities in Python +Description: NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". In renaming the GitHub repo, most watchers/followers + will be lost, so please take note of this date if you wish to continue following development! + + There should (hopefully) be no further disruptions after that. Eventually, I'll get around to creating a setup.py file + that works correctly. ;) + + Tango - Easy Twitter utilities in Python ----------------------------------------------------------------------------------------------------- I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at @@ -55,7 +61,7 @@ Description: Tango - Easy Twitter utilities in Python Tango is released under an MIT License - see the LICENSE file for more information. -Keywords: twitter search api tweet tango +Keywords: twitter search api tweet twython Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt new file mode 100644 index 0000000..0339cbe --- /dev/null +++ b/twython.egg-info/SOURCES.txt @@ -0,0 +1,7 @@ +README +setup.py +twython.egg-info/PKG-INFO +twython.egg-info/SOURCES.txt +twython.egg-info/dependency_links.txt +twython.egg-info/requires.txt +twython.egg-info/top_level.txt \ No newline at end of file diff --git a/tango.egg-info/dependency_links.txt b/twython.egg-info/dependency_links.txt similarity index 100% rename from tango.egg-info/dependency_links.txt rename to twython.egg-info/dependency_links.txt diff --git a/tango.egg-info/requires.txt b/twython.egg-info/requires.txt similarity index 100% rename from tango.egg-info/requires.txt rename to twython.egg-info/requires.txt diff --git a/twython.egg-info/top_level.txt b/twython.egg-info/top_level.txt new file mode 100644 index 0000000..54fa13c --- /dev/null +++ b/twython.egg-info/top_level.txt @@ -0,0 +1 @@ +twython/twython diff --git a/tango/tango.py b/twython/tango.py similarity index 100% rename from tango/tango.py rename to twython/tango.py diff --git a/tango/tango3k.py b/twython/tango3k.py similarity index 100% rename from tango/tango3k.py rename to twython/tango3k.py From fb41d4100c4a55b52012d7f090f8ed0ed361f4dd Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:38:59 -0400 Subject: [PATCH 085/687] Changing references to Tango --- README | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/README b/README index e150c66..0a73895 100644 --- a/README +++ b/README @@ -1,12 +1,6 @@ -NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". In renaming the GitHub repo, most watchers/followers -will be lost, so please take note of this date if you wish to continue following development! - -There should (hopefully) be no further disruptions after that. Eventually, I'll get around to creating a setup.py file -that works correctly. ;) - -Tango - Easy Twitter utilities in Python +Twython - Easy Twitter utilities in Python ----------------------------------------------------------------------------------------------------- -I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain +I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at a library that offers more coverage. @@ -14,17 +8,17 @@ This is my first library I've ever written in Python, so there could be some stu make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, and I'm open to anything that'll improve the library as a whole. -OAuth support is in the works, but every other part of the Twitter API should be covered. Tango +OAuth support is in the works, but every other part of the Twitter API should be covered. Twython handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for -Authentication. To override this, specify 'authtype="Basic"' in your tango.setup() call. +Authentication. To override this, specify 'authtype="Basic"' in your twython.setup() call. -Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All +Documentation is forthcoming, but Twython attempts to mirror the Twitter API in a large way. All parameters for API calls should translate over as function arguments. Requirements ----------------------------------------------------------------------------------------------------- -Tango requires (much like Python-Twitter, because they had the right idea :D) a library called +Twython requires (much like Python-Twitter, because they had the right idea :D) a library called "simplejson". You can grab it at the following link: http://pypi.python.org/pypi/simplejson @@ -32,23 +26,23 @@ http://pypi.python.org/pypi/simplejson Example Use ----------------------------------------------------------------------------------------------------- -import tango +import twython -twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(authtype="Basic", username="example", password="example") twitter.updateStatus("See how easy this was?") -Tango 3k +Twython 3k ----------------------------------------------------------------------------------------------------- -There's an experimental version of Tango that's made for Python 3k. This is currently not guaranteed +There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- -My hope is that Tango is so simple that you'd never *have* to ask any questions, but if +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. -Tango is released under an MIT License - see the LICENSE file for more information. +Twython is released under an MIT License - see the LICENSE file for more information. From 3b22ff34ce107c50cf250d7ed88d51303c2f7417 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:45:29 -0400 Subject: [PATCH 086/687] Properly porting files... --- twython/{tango.py => twython.py} | 4 +- twython/{tango3k.py => twython3k.py} | 174 ++++++++++++++------------- 2 files changed, 90 insertions(+), 88 deletions(-) rename twython/{tango.py => twython.py} (99%) rename twython/{tango3k.py => twython3k.py} (74%) diff --git a/twython/tango.py b/twython/twython.py similarity index 99% rename from twython/tango.py rename to twython/twython.py index aa731ae..2d37f50 100644 --- a/twython/tango.py +++ b/twython/twython.py @@ -90,13 +90,13 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: + try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError, e: raise TangoError("shortenURL() failed with a %s error code." % `e.code`) def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) def getRateLimitStatus(self, rate_for = "requestingIP"): try: diff --git a/twython/tango3k.py b/twython/twython3k.py similarity index 74% rename from twython/tango3k.py rename to twython/twython3k.py index 77f930e..fdcc081 100644 --- a/twython/tango3k.py +++ b/twython/twython3k.py @@ -1,7 +1,7 @@ #!/usr/bin/python """ - Tango is an up-to-date library for Python that wraps the Twitter API. + Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -23,14 +23,14 @@ except ImportError: try: import json as simplejson except: - raise Exception("Tango requires a json library to work. http://www.undefined.org/python/") + raise Exception("Twython requires a json library to work. http://www.undefined.org/python/") try: import oauth except ImportError: pass -class TangoError(Exception): +class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg if error_code == 400: @@ -38,7 +38,7 @@ class TangoError(Exception): def __str__(self): return repr(self.msg) -class APILimit(TangoError): +class APILimit(TwythonError): def __init__(self, msg): self.msg = msg def __str__(self): @@ -65,11 +65,13 @@ class setup: self.opener = urllib.request.build_opener(self.handler) if headers is not None: self.opener.addheaders = [('User-agent', headers)] + """ try: test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + """ # OAuth functions; shortcuts for verifying the credentials. def fetch_response_oauth(self, oauth_request): @@ -89,7 +91,7 @@ class setup: try: return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError as e: - raise TangoError("shortenURL() failed with a %s error code." % repr(e.code)) + raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) @@ -102,15 +104,15 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") + raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) + raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) def getPublicTimeline(self): try: return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError as e: - raise TangoError("getPublicTimeline() failed with a %s error code." % repr(e.code)) + raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -118,9 +120,9 @@ class setup: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError as e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) + raise TwythonError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") + raise TwythonError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: @@ -136,7 +138,7 @@ class setup: else: return simplejson.load(urllib.request.urlopen(userTimelineURL)) except HTTPError as e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % repr(e.code), e.code) def getUserMentions(self, **kwargs): @@ -145,9 +147,9 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError as e: - raise TangoError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getUserMentions() requires you to be authenticated.") + raise TwythonError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: @@ -156,28 +158,28 @@ class setup: else: return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError as e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") + raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: - raise TangoError("updateStatus() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("updateStatus() requires you to be authenticated.") + raise TwythonError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) except HTTPError as e: - raise TangoError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("destroyStatus() requires you to be authenticated.") + raise TwythonError("destroyStatus() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -185,9 +187,9 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError as e: - raise TangoError("endSession failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("endSession failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") + raise TwythonError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -202,9 +204,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getDirectMessages() requires you to be authenticated.") + raise TwythonError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -219,9 +221,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getSentMessages() requires you to be authenticated.") + raise TwythonError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -229,20 +231,20 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) except HTTPError as e: - raise TangoError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("Your message must not be longer than 140 characters") + raise TwythonError("Your message must not be longer than 140 characters") else: - raise TangoError("You must be authenticated to send a new direct message.") + raise TwythonError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") except HTTPError as e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("You must be authenticated to destroy a direct message.") + raise TwythonError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: @@ -258,10 +260,10 @@ class setup: except HTTPError as e: # Rate limiting is done differently here for API reasons... if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") + raise TwythonError("createFriendship() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("createFriendship() requires you to be authenticated.") + raise TwythonError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -275,36 +277,36 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: - raise TangoError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("destroyFriendship() requires you to be authenticated.") + raise TwythonError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError as e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + raise TwythonError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) except HTTPError as e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + raise TwythonError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError as e: - raise TangoError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("updateProfileColors() requires you to be authenticated.") + raise TwythonError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -315,7 +317,7 @@ class setup: updateProfileQueryString += "name=" + name useAmpersands = True else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") if email is not None and "@" in email: if len(list(email)) < 40: if useAmpersands is True: @@ -324,7 +326,7 @@ class setup: updateProfileQueryString += "email=" + email useAmpersands = True else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") if url is not None: if len(list(url)) < 100: if useAmpersands is True: @@ -333,7 +335,7 @@ class setup: updateProfileQueryString += urllib.parse.urlencode({"url": url}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: @@ -342,7 +344,7 @@ class setup: updateProfileQueryString += urllib.parse.urlencode({"location": location}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: @@ -350,42 +352,42 @@ class setup: else: updateProfileQueryString += urllib.parse.urlencode({"description": description}) else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError as e: - raise TangoError("updateProfile() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfile() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("updateProfile() requires you to be authenticated.") + raise TwythonError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError as e: - raise TangoError("getFavorites() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getFavorites() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getFavorites() requires you to be authenticated.") + raise TwythonError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError as e: - raise TangoError("createFavorite() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("createFavorite() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("createFavorite() requires you to be authenticated.") + raise TwythonError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("destroyFavorite() requires you to be authenticated.") + raise TwythonError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -399,9 +401,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - raise TangoError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("notificationFollow() requires you to be authenticated.") + raise TwythonError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -415,9 +417,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: - raise TangoError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("notificationLeave() requires you to be authenticated.") + raise TwythonError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -430,7 +432,7 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -443,25 +445,25 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError as e: - raise TangoError("createBlock() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("createBlock() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("createBlock() requires you to be authenticated.") + raise TwythonError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("destroyBlock() requires you to be authenticated.") + raise TwythonError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" @@ -474,32 +476,32 @@ class setup: try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError as e: - raise TangoError("getBlocking() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getBlocking() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getBlocking() requires you to be authenticated") + raise TwythonError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError as e: - raise TangoError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") + raise TwythonError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": search_query}) try: return simplejson.load(urllib.request.urlopen(searchURL)) except HTTPError as e: - raise TangoError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) def getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" @@ -508,7 +510,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -524,7 +526,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -540,43 +542,43 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError as e: - raise TangoError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("getSavedSearches() requires you to be authenticated.") + raise TwythonError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError as e: - raise TangoError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("showSavedSearch() requires you to be authenticated.") + raise TwythonError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError as e: - raise TangoError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("createSavedSearch() requires you to be authenticated.") + raise TwythonError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError as e: - raise TangoError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") + raise TwythonError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -589,9 +591,9 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") + raise TwythonError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -603,9 +605,9 @@ class setup: r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TangoError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") + raise TwythonError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() From 999adbe2ba8f7e0749332215bc9f78369ec63c4e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:46:26 -0400 Subject: [PATCH 087/687] New build, representing 0.5 release - fixed bad indentation errors in Twython, small cleanup here and there --- build/lib/twython/twython.py | 636 +++++++++++++++++++++++++++++++++++ 1 file changed, 636 insertions(+) create mode 100644 build/lib/twython/twython.py diff --git a/build/lib/twython/twython.py b/build/lib/twython/twython.py new file mode 100644 index 0000000..2d37f50 --- /dev/null +++ b/build/lib/twython/twython.py @@ -0,0 +1,636 @@ +#!/usr/bin/python + +""" + NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. + + Twython is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools + +from urllib2 import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.8.0.1" + +"""Twython - Easy Twitter utilities in Python""" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + self.oauth_keys = oauth_keys + if self.username is not None and self.password is not None: + if self.authtype == "OAuth": + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + # Do OAuth type stuff here - how should this be handled? Seems like a framework question... + elif self.authtype == "Basic": + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + + # OAuth functions; shortcuts for verifying the credentials. + def fetch_response_oauth(self, oauth_request): + pass + + def get_unauthorized_request_token(self): + pass + + def get_authorization_url(self, token): + pass + + def exchange_tokens(self, request_token): + pass + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError, e: + raise TangoError("shortenURL() failed with a %s error code." % `e.code`) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TangoError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError, e: + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self): + try: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError, e: + raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + else: + raise TangoError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`, e.code) + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError, e: + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getUserMentions() requires you to be authenticated.") + + def showStatus(self, id): + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError, e: + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError, e: + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From 0a2d8456af6fb0f69bf8c4c86967e98105e81e26 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:46:53 -0400 Subject: [PATCH 088/687] More package change info --- twython.egg-info/PKG-INFO | 30 ++++++++++++------------------ twython.egg-info/SOURCES.txt | 1 + 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/twython.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO index f0758a0..11e1b16 100644 --- a/twython.egg-info/PKG-INFO +++ b/twython.egg-info/PKG-INFO @@ -6,15 +6,9 @@ Home-page: http://github.com/ryanmcgrath/twython/tree/master Author: Ryan McGrath Author-email: ryan@venodesigns.net License: MIT License -Description: NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". In renaming the GitHub repo, most watchers/followers - will be lost, so please take note of this date if you wish to continue following development! - - There should (hopefully) be no further disruptions after that. Eventually, I'll get around to creating a setup.py file - that works correctly. ;) - - Tango - Easy Twitter utilities in Python +Description: Twython - Easy Twitter utilities in Python ----------------------------------------------------------------------------------------------------- - I wrote Tango because I found that other Python Twitter libraries weren't that up to date. Certain + I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at a library that offers more coverage. @@ -22,17 +16,17 @@ Description: NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". I make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, and I'm open to anything that'll improve the library as a whole. - OAuth support is in the works, but every other part of the Twitter API should be covered. Tango + OAuth support is in the works, but every other part of the Twitter API should be covered. Twython handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for - Authentication. To override this, specify 'authtype="Basic"' in your tango.setup() call. + Authentication. To override this, specify 'authtype="Basic"' in your twython.setup() call. - Documentation is forthcoming, but Tango attempts to mirror the Twitter API in a large way. All + Documentation is forthcoming, but Twython attempts to mirror the Twitter API in a large way. All parameters for API calls should translate over as function arguments. Requirements ----------------------------------------------------------------------------------------------------- - Tango requires (much like Python-Twitter, because they had the right idea :D) a library called + Twython requires (much like Python-Twitter, because they had the right idea :D) a library called "simplejson". You can grab it at the following link: http://pypi.python.org/pypi/simplejson @@ -40,26 +34,26 @@ Description: NOTICE: On 08/01/2009, Tango is going to be renamed to "Twython". I Example Use ----------------------------------------------------------------------------------------------------- - import tango + import twython - twitter = tango.setup(authtype="Basic", username="example", password="example") + twitter = twython.setup(authtype="Basic", username="example", password="example") twitter.updateStatus("See how easy this was?") - Tango 3k + Twython 3k ----------------------------------------------------------------------------------------------------- - There's an experimental version of Tango that's made for Python 3k. This is currently not guaranteed + There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- - My hope is that Tango is so simple that you'd never *have* to ask any questions, but if + My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. - Tango is released under an MIT License - see the LICENSE file for more information. + Twython is released under an MIT License - see the LICENSE file for more information. Keywords: twitter search api tweet twython Platform: UNKNOWN diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt index 0339cbe..696cd92 100644 --- a/twython.egg-info/SOURCES.txt +++ b/twython.egg-info/SOURCES.txt @@ -1,5 +1,6 @@ README setup.py +twython/twython.py twython.egg-info/PKG-INFO twython.egg-info/SOURCES.txt twython.egg-info/dependency_links.txt From e0700558163b1b0c94dae2aedf01e454e689074c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:51:27 -0400 Subject: [PATCH 089/687] Incremental release fixes... --- dist/twython-0.5-py2.5.egg | Bin 0 -> 15177 bytes dist/twython-0.5.tar.gz | Bin 2713 -> 7975 bytes dist/twython-0.5.win32.exe | Bin 67389 -> 71547 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dist/twython-0.5-py2.5.egg diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg new file mode 100644 index 0000000000000000000000000000000000000000..ceb90be23b95b55527e9c6691991e0126c08d923 GIT binary patch literal 15177 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 literal 2713 zcmajR`6JT}0|0PGxsM7@ZbF7e&Zp_YN<&0)b`0e$T>NoFS%Rl%qN3 z%$Xxcay+hK3ES-Z{s-^-`ROBv<>8s|LRxTe28E!4eEgrQYij6e1i8Lo+mZHrIoJH{ z@zCRs<><8R^uq>EqVHt&$e+t^zowd6{F9GQ;!?QDt7o#uHk!x^54o9`Au|(r;ZA|e z$4q@Y`Q``PVgLDxo0+%tXj`mk+WJT8HCI<4DgV@v!utDB{LG>N^6k*nO~&5FQdAFq zXa%z-L9s30mgmE^;e7LH4v&!d>HBx;uHW zw4l!AS|W{}g@CQ+mqJRf@3F6U#sYE5YZwNa0-Inr-amrN4n+BBr39<5^@LqT%lbJT zlw6Bq#)tGY)xq4hr!?xg+Q(@^PUO*JyY_E2M9F06eb%x0Q={p``?`@?#()U4q1Fuxx-5%bfV5Gei5_y7SXjMg* zmCnD&#<*USc0)k=&V5w4)Mq245YuA0uWEQn4J{#czFb=SE9_>6KT0W6HFe0EtVSlY z^)D5Hw39i$R7d(BT9 zS`fzDioDZHSy#GYZCyIQDtbURmu|Py^Hd`_K-2ANPU*b%(NbUdqLqT)(h#*D92@%9 z;ZCX*#TzyWdy7Fr0tlS4ioRV<*Nd?={e9Orj}m9Gvu;{NKc=QC?V@!=`Sil0OYeVP zje1*=W=)`ls%llQ_G}jWPo6WM+>p9~*A+k%w3~Sp#fjaq>X*M=mz$$C`(*e#-2Mi^ zOIuUkO*!wZLmJmZq*RGMxT~gBP(<0l!9QC?|-BGH}Q%0jluJ5>6MXfyE$7VaDP zxZ;F=U#e0`@mh$$%YTPnlD8MMFpM7CvssuDk9Y8&GbsUJ*GKPqd2-yd8KHg%W1G@c zWaehOZQ;w@zK1y#Ghnx#-y>xdMS;PI)upvELUtbXy3d3ROMlto0q9(DA+b^gMNTr_ z!fGAVN5RL3RysR}w8u_V--~O#7tgxg2{D1E3O@e!@VCUTXq`v}9+{Kii9>m;4OXR= zx~YAAkeB8kB`uqoic$7YnKq4uf(weqQu z@8STBzyB9UJ9|$T*hsT+Fge{EXXCZf7yBZL%#NZ=iy#Lc&uvuttGw=_4mdwV#-EM& z>C%VtaE1*wp26F~*X%?ael}4^@fRKC#eREdam&}Db`i>5!&aG|;@ABAUeDgTp31NC zyphVLiB$}dtD6$cO?E?iCZJOLK2~!iNUR zdPe4)o$iI>gG^MFk>k1sGG3>aZU`}d`dnm`I$44^OpmtIZ0z6-myoQUtR@8WtSbzW zBDtVzKWgww)q zByD43BeeY6Q<(*N9&=#pBdOQ9Ul}l$e5l)&Sh{2IivgLnrM%Y$~3gZu~k6_aMAVS$6RqkrKk4 zc6mWyq^oriuQ(%}FCGJqJ!S`IG{d;EtG7{gOVe%W#EM5`OFu9tp0B&9Ugk_qtY9`7 z#r218_>AKZ(RK^Zjd(!BLb7+S=>gEvP@U%Y-{_=s)(dMBT~IFPYQ^(tm_9| z`LIYKUo@RrJOJfoJZlQ`#;>}eeW2Q#In&SZUS|ia6I)lQj2nrgFl|#R>Gi-AE3B;M zCcJ7wQ-=SKny${B;?~t{bfRV{UnO&mTD{SaXBwIinN3Hc5R$R+X6BrxZSC#KNTcIW z!{*jki*gaF^-h%Tk{+dp8yw~>e5;;T-mwa9Pzz2YOU-<5cO!!3R@aR(X)2vO$Bw1U z{ZNOpQer-}z*&@m^rvIv3yB^tBYy?=847GIV&?ILPuNKN0@lWsNYP*r&_?Fg{!F0-t7&=+NFB%&lo)hO4Gc+)3GS7@T*Sd3sc5 z6ag`$0!1g0lPvhoS~$a>Fb@FUSJJ|5f2z}i7BEz{wq8Pa*jV)~>Nhspsz^< E0ifA-(*OVf diff --git a/dist/twython-0.5.win32.exe b/dist/twython-0.5.win32.exe index 90e480b6dd136bd09497b07c2a4da6aaf4ee0eca..a61a4b0b6eacd5a5df73e5e8bdf07736b9c7d2b2 100644 GIT binary patch delta 6618 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 G*?$0NXd>GH From b0b345d013348298b69f9016ce95c7adef9594ce Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 3 Aug 2009 00:56:23 -0400 Subject: [PATCH 090/687] Final 0.5 release fixes for packaging things... --- dist/twython-0.5-py2.5.egg | Bin 15177 -> 15177 bytes dist/twython-0.5.tar.gz | Bin 7975 -> 7977 bytes dist/twython-0.5.win32.exe | Bin 71547 -> 71547 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg index ceb90be23b95b55527e9c6691991e0126c08d923..dc20f75564b5a5734470e0a392907e562c5b718b 100644 GIT binary patch delta 99 zcmX?EcCw5+z?+#xgn@y9gW=;uZd=BW8$GjG89z?m%IXOu|Fcd5lGE567(Z^l&gLKl qq$DIo6o6v7mR>-z!qN^%Znty-lD{k+fuxO;S!EbV~gc1tH9`ODG~NZMFA1IY%fIw1Mq$_oG{t{ct( diff --git a/dist/twython-0.5.tar.gz b/dist/twython-0.5.tar.gz index 9edf6cbbc172d31bbc20e850b86f5a1c08b0d001..83e61dff7498ef0dafa7725b8a5f14af47ee454d 100644 GIT binary patch delta 7424 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 delta 7422 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 w`~T|sFMZnh|2F48n|pg4XvvZpB}SkD m=$|5ti$U}}QO0Bt%`C^%=AP delta 101 zcmeypj^+0{mJQo}vznM&nHp~1{o7NB=_l)SHa^BMAiX_~kFkXnB=AI-F$qL_h%l}P m(LY5P7lY_|qKwHPnpuo797M;6F-C*vonnkpAX-$MF%1Bk&?6xL From 5ba7e99e6ee567ee2d91de9f5dcabb941b206f83 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 4 Aug 2009 02:20:49 -0400 Subject: [PATCH 091/687] Some more work on OAuth, borrowing ideas from existing implementations - parsing of responses, etc. Feel free to critique/contribute. --- twython/twython.py | 60 ++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 2d37f50..ea0ec31 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -14,10 +14,11 @@ import httplib, urllib, urllib2, mimetypes, mimetools +from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.8.0.1" +__version__ = "0.5" """Twython - Easy Twitter utilities in Python""" @@ -49,20 +50,25 @@ class APILimit(TangoError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password - self.oauth_keys = oauth_keys + # OAuth specific variables below + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.request_token = None + self.access_token = None + # Check and set up authentication if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": + if self.authtype == "Basic": + # Basic authentication ritual self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) @@ -74,19 +80,31 @@ class setup: self.authenticated = True except HTTPError, e: raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + else: + # Awesome OAuth authentication ritual + if consumer_secret is not None and consumer_key is not None: + #req = oauth.OAuthRequest.from_consumer_and_token + #req.sign_request(self.signature_method, self.consumer_key, self.token) + #self.opener = urllib2.build_opener() + pass + else: + raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass + def getRequestToken(self): + response = self.oauth_request(self.request_token_url) + token = self.parseOAuthResponse(response) + self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) + return self.request_token - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass + def parseOAuthResponse(self, response_string): + # Partial credit goes to Harper Reed for this gem. + lol = {} + for param in response_string.split("&"): + pair = param.split("=") + if(len(pair) != 2): + break + lol[pair[0]] = pair[1] + return lol # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): From c0b56f33a74cd06a20b2a19746e1dd451209aba5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 6 Aug 2009 01:48:14 -0400 Subject: [PATCH 092/687] Reorganizing structure, finally made the setup.py install function actually work - thanks to the guys in #python for their help. --- {twython => build/lib}/twython.py | 0 dist/twython-0.5-py2.5.egg | Bin 15177 -> 29112 bytes setup.py | 2 +- twython.egg-info/SOURCES.txt | 1 + twython.egg-info/top_level.txt | 2 +- twython.py | 654 +++++++++++++++++++++++++++ twython/twython3k.py => twython3k.py | 2 +- 7 files changed, 658 insertions(+), 3 deletions(-) rename {twython => build/lib}/twython.py (100%) create mode 100644 twython.py rename twython/twython3k.py => twython3k.py (99%) diff --git a/twython/twython.py b/build/lib/twython.py similarity index 100% rename from twython/twython.py rename to build/lib/twython.py diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg index dc20f75564b5a5734470e0a392907e562c5b718b..c81e15bb706bc0594009b0ea771a40a94d5fca7c 100644 GIT binary patch delta 14468 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

}L0hgv%@1-l~7EQ@CU0OqsqY5bs%6#G7( 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 YG)z7bp9c>C4gd#$`n!XfcK?X`KXg7SfdBvi delta 508 zcmdn-nDJy8Z-6&5iwFY)0|&!LHs;B8Cf3YAdUK6QCM$?B`GuuBFOatWtg3n<6{u{_ zWE(4^`pfN?Je;alzI?fBW@YBjUyS$O?V4AY_iu*L!G8-K&#gOHr7EgAW0K}d)|Hi- zD=SY<{b)I}^5)SSp`uI0rlpB3eKbM$q<+VFPhIb`jGoL4C{D1xv-ad3UZ70^Kr9Bt zuI}!-o_=or`X%`V@j0nwsX2O+-&%1AU)FED5QD0BGP||@=4KmyA+R5&`Uxq3{UDt$ z&B!p>$3&q%z?+dtgctm0v9kOL*U~mCiCv+nW;(+*xw}AqvTmUq(?7P!zJ(B<7Zge}@k&naEtKWs0LHK|69a>S HCCCf_%GQl$ diff --git a/setup.py b/setup.py index d73134f..94f70e4 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ __version__ = '0.5' METADATA = dict( name = "twython", version = __version__, - py_modules = ['twython/twython'], + py_modules = ['twython'], author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt index 696cd92..08928a4 100644 --- a/twython.egg-info/SOURCES.txt +++ b/twython.egg-info/SOURCES.txt @@ -1,5 +1,6 @@ README setup.py +twython.py twython/twython.py twython.egg-info/PKG-INFO twython.egg-info/SOURCES.txt diff --git a/twython.egg-info/top_level.txt b/twython.egg-info/top_level.txt index 54fa13c..292a670 100644 --- a/twython.egg-info/top_level.txt +++ b/twython.egg-info/top_level.txt @@ -1 +1 @@ -twython/twython +twython diff --git a/twython.py b/twython.py new file mode 100644 index 0000000..ea0ec31 --- /dev/null +++ b/twython.py @@ -0,0 +1,654 @@ +#!/usr/bin/python + +""" + NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. + + Twython is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools + +from urlparse import urlparse +from urllib2 import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.5" + +"""Twython - Easy Twitter utilities in Python""" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TangoError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TangoError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + self.authtype = authtype + self.authenticated = False + self.username = username + self.password = password + # OAuth specific variables below + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.request_token = None + self.access_token = None + # Check and set up authentication + if self.username is not None and self.password is not None: + if self.authtype == "Basic": + # Basic authentication ritual + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + else: + # Awesome OAuth authentication ritual + if consumer_secret is not None and consumer_key is not None: + #req = oauth.OAuthRequest.from_consumer_and_token + #req.sign_request(self.signature_method, self.consumer_key, self.token) + #self.opener = urllib2.build_opener() + pass + else: + raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") + + def getRequestToken(self): + response = self.oauth_request(self.request_token_url) + token = self.parseOAuthResponse(response) + self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) + return self.request_token + + def parseOAuthResponse(self, response_string): + # Partial credit goes to Harper Reed for this gem. + lol = {} + for param in response_string.split("&"): + pair = param.split("=") + if(len(pair) != 2): + break + lol[pair[0]] = pair[1] + return lol + + # URL Shortening function huzzah + def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + except HTTPError, e: + raise TangoError("shortenURL() failed with a %s error code." % `e.code`) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + try: + if rate_for == "requestingIP": + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TangoError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError, e: + raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self): + try: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError, e: + raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getFriendsTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + else: + raise TangoError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`, e.code) + + def getUserMentions(self, **kwargs): + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError, e: + raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getUserMentions() requires you to be authenticated.") + + def showStatus(self, id): + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError, e: + raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + if self.authenticated is True: + if len(list(status)) > 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError, e: + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError, e: + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/twython/twython3k.py b/twython3k.py similarity index 99% rename from twython/twython3k.py rename to twython3k.py index fdcc081..93e2e06 100644 --- a/twython/twython3k.py +++ b/twython3k.py @@ -15,7 +15,7 @@ import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetype from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.6" +__version__ = "0.5" try: import simplejson From adab94b240bf10d8aa7e89fcdf6fae41e21281a9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 6 Aug 2009 02:43:40 -0400 Subject: [PATCH 093/687] There, *now* this build should be fixed. --- __init__.py | 1 + twython.py => build/lib/twython2k.py | 2 +- dist/twython-0.5-py2.5.egg | Bin 29112 -> 42140 bytes setup.py | 2 +- twython.egg-info/SOURCES.txt | 2 +- twython.egg-info/top_level.txt | 2 +- twython2k.py | 654 +++++++++++++++++++++++++++ 7 files changed, 659 insertions(+), 4 deletions(-) create mode 100644 __init__.py rename twython.py => build/lib/twython2k.py (99%) create mode 100644 twython2k.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..8829c09 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +import twython2k as twython diff --git a/twython.py b/build/lib/twython2k.py similarity index 99% rename from twython.py rename to build/lib/twython2k.py index ea0ec31..051884c 100644 --- a/twython.py +++ b/build/lib/twython2k.py @@ -60,7 +60,6 @@ class setup: self.access_token_url = 'https://twitter.com/oauth/access_token' self.authorization_url = 'http://twitter.com/oauth/authorize' self.signin_url = 'http://twitter.com/oauth/authenticate' - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.request_token = None @@ -81,6 +80,7 @@ class setup: except HTTPError, e: raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) else: + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() # Awesome OAuth authentication ritual if consumer_secret is not None and consumer_key is not None: #req = oauth.OAuthRequest.from_consumer_and_token diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg index c81e15bb706bc0594009b0ea771a40a94d5fca7c..9911338a46920b14c5f533d0001589fcb3c05669 100644 GIT binary patch delta 13116 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 KNNmA>1pY7LE5}p- delta 838 zcmbPpl4-|d#tmA+OiOq->j?8JGA-tvT$N!1q&Dx!$YKR?Rq{M|m+-P#=c+a>Ol4qT zI6k>D&xG|=GS|zclQ-oB@-7C-rFmFA;sVNXOcuyj;eX7;5a7+sA_5X%Si(EmHQ%5f zWP+Or(+X3dpg9l=193@tWl2VUo_>6MW?p7Ve7s&kWq1GiGrA}AwL1H+2WXw}>(}?v zJ*Thh<)<5V>58|f?>SE$uio?CXMA<_Jbe7FcCc@L{MeKc*)lfkNgpOH+6pxN5fFo% z0Jfzb)tY42USF<51_G^U$@7}JXfT?K=IM7R%w2LTPZ6JF5~poFdoA93VN8d9{1&l&Uis$lVST~U^-(9YR@P{g zs`l&`{T#Vd>q^q*v 140: + raise TangoError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateStatus() requires you to be authenticated.") + + def destroyStatus(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + except HTTPError, e: + raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + if since_id is not None: + apiURL += "&since_id=" + since_id + if max_id is not None: + apiURL += "&max_id=" + max_id + if count is not None: + apiURL += "&count=" + count + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("Your message must not be longer than 140 characters") + else: + raise TangoError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + if user_id is not None: + apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + if screen_name is not None: + apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") + raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + except HTTPError, e: + raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def updateDeliveryDevice(self, device_name = "none"): + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + except HTTPError, e: + raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + else: + updateProfileQueryString += urllib.urlencode({"url": url}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + else: + updateProfileQueryString += urllib.urlencode({"location": location}) + useAmpersands = True + else: + raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + else: + updateProfileQueryString += urllib.urlencode({"description": description}) + else: + raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + except HTTPError, e: + raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + except HTTPError, e: + raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=" + date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + except HTTPError, e: + raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + except HTTPError, e: + raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + except HTTPError, e: + raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + if self.authenticated is True: + try: + files = [("image", filename, open(filename).read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise TangoError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From c62c41a45ee4b8c09ac882490df75a1872f5ffbd Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 6 Aug 2009 02:57:02 -0400 Subject: [PATCH 094/687] Alright, now import twython works as it should. --- __init__.py | 2 +- build/lib/twython.py | 4 ++-- dist/twython-0.5-py2.5.egg | Bin 42140 -> 15702 bytes dist/twython-0.6-py2.5.egg | Bin 0 -> 15685 bytes dist/twython-0.6.tar.gz | Bin 0 -> 7130 bytes dist/twython-0.6.win32.exe | Bin 0 -> 71809 bytes setup.py | 4 ++-- twython.egg-info/PKG-INFO | 2 +- twython.egg-info/SOURCES.txt | 1 - twython.egg-info/top_level.txt | 2 +- twython2k.py => twython.py | 2 +- twython3k.py | 2 +- 12 files changed, 9 insertions(+), 10 deletions(-) create mode 100644 dist/twython-0.6-py2.5.egg create mode 100644 dist/twython-0.6.tar.gz create mode 100644 dist/twython-0.6.win32.exe rename twython2k.py => twython.py (99%) diff --git a/__init__.py b/__init__.py index 8829c09..73b19e2 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -import twython2k as twython +import twython as twython diff --git a/build/lib/twython.py b/build/lib/twython.py index ea0ec31..456b6c8 100644 --- a/build/lib/twython.py +++ b/build/lib/twython.py @@ -18,7 +18,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.5" +__version__ = "0.6" """Twython - Easy Twitter utilities in Python""" @@ -60,7 +60,6 @@ class setup: self.access_token_url = 'https://twitter.com/oauth/access_token' self.authorization_url = 'http://twitter.com/oauth/authorize' self.signin_url = 'http://twitter.com/oauth/authenticate' - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.request_token = None @@ -81,6 +80,7 @@ class setup: except HTTPError, e: raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) else: + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() # Awesome OAuth authentication ritual if consumer_secret is not None and consumer_key is not None: #req = oauth.OAuthRequest.from_consumer_and_token diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg index 9911338a46920b14c5f533d0001589fcb3c05669..8b246282df73b8dd5a616430e2b46606401f9c4a 100644 GIT binary patch delta 307 zcmbPplIdF2W3u)Yb}xj@)O;KHGt~DU`eB@FatwL zd1XmPex6abUO{DYfHxzP2(t)KnuFocO2kPRGUnGwMZVw;aV)q z^xSf?=3-?96lVZ6f;7Xxl14+T$#r>Ryr|~(19cy_n!IkYiYG|-WVY)@%s?%SKr9H; u3Z$1bvRgASxVpRRdiuHf>sMtK=oTlYr3QGjvVl}_03kcjXO=)k3=9AcNK(@P delta 26061 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 X18;*13->n*`R%xef`Ew5`&;`z037@> diff --git a/dist/twython-0.6-py2.5.egg b/dist/twython-0.6-py2.5.egg new file mode 100644 index 0000000000000000000000000000000000000000..64d9ac7b2b597bfa6f5536012f02d65daae6498c GIT binary patch literal 15685 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 literal 0 HcmV?d00001 diff --git a/dist/twython-0.6.tar.gz b/dist/twython-0.6.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..e2e40b45339fd7eeeb7a816e2fb0d8e35392828d GIT binary patch literal 7130 zcmV<08ztl)iwFoletJp*19W$JbZBpGEif)NE_7jX0PH<&bJ|GK`D*=&S*3P?90uF5 zohsk?a^BdvxHgV`vGZ(QrBXT}4WP4-SdC=DUgf_(-90mULtwx__O766Vph4?eGsj^y8C`FHQ&Q2v#lXZ!mHFOLsjy@G+y_V)K*9lm_V zj-GAclZe=k7<(3t?O@h87}o7R`=9&7IsV`L^7q}#4{xv6!T;mK!)g3KKH57h@&D!C z;n6d;x8?uZKUX}qT|2gS|Bpw)3xgB3-`X=i*aLpTlx!I3BM=hfHW>`;XnexXSincj z4qV1CmucOx&3dBhMjgX=!KAy_TrT{z&oL%Rp?`*A!xX}5duIO(*U zaL`6344htM$Njciy&XrKw+FTWaK>2zx1tmF5yr7A=kGXB-Q48^+w)Htg81754?>p< zuNR0`z+>aR=kP$ll2?~^OnouVxo{$H7}M77flUhHwfyg+TiVCu8G ziBG!PB^yOy%u_@=+_4kE;eIzvfC+Kmj#(J@0cnY6x(DCuM0SLG81aY)wOEc#hR82q z5O1+JJc?~E+gjZBf}Vhx_Z+S{yXAJ|^x4_XWs_Yay_<~3PLsJIf)kt%5LSn?ZsPl6 z=7bNhlG|c;eGe!n*kH_Tz-2Iu;cvQ%txy{))pMGj`yi$t6P zxvV{K#$g^%3U?l4JZmx$;zkH^;=72d06}CTPP$zjN6!$An(wDe4Q$xI4g0f27;sk; z_JPBm9gsaB2_n=GxB!suQ! zStp5!SYstLLpwqi&=4&-z;7n{nqV1MM@2>G1M)tIR|g=mpKaketp4uq?xw*I1s=qn zW22VQCYAW4mxvjz>GH0f_%TooV7nmHQ3}5Z1=bH4q7r#7lEZ_|h#_~p?wHkV7#fd< z{L~~o%o^@`3^doqtR;Ay4C@VIQ3ch`Lnj%)ViIA%4G@X@V4Hv!l4x21l|T#R*1(G* z;D4ERur|oJ9rZYx#}+%w$rjKXjBJb&4@=N+L`i6Z$B`WfAGDM9imdJ?fg{Nn^$15% zfq#1Ok^h!>5vOtA$z*9tMG_Uk>VpJShOCNIrCmj>W@^rWgkumWmuMMzy*`Mp%WZaY z-pJVtq!f44g-ik3;q!lkmYOa0&oF@j0RuPEVIEo>0@e+EKOCV-`Cf2;GD$Jzj)vo* z*BVM;38Nl;Z)Xe4Ao;~t8%MEE;9R~B#Q{rH_PC^=nO#@Hd0JA3vf4Bm@FEI8Ku=9B zcLn!`wh&-F-0WVX47rsI(WBgg{v@Jq-g3_R;YdaZsH&g`;&pCXg?U#(^YZ?CNiDhp zFxMbK{OgcM9+?9ltUg6SXd^X_WShYg*)As~zR{>>p3g}l@VPPggdmJrF9EF&VxE&- zupVfUrET#L9NKx1c~|-@?bSt5ronvnZReh$k&3~=bkWK&ryqu>L@*Jw1jdC)ocS2A z;r7S|KSyTXn`u7&k#K>=28`>Qa6ndseE#e2#FZ1x3DuS4BvrFQa$ApphL}t z*c7eii@yE9Uywf)_vlmEZ`r_y_sG6=GC~xCqVnmv1gU++IK+ zqwt8<@Z+y^w zuj_FLzR!WQemF##U{cZ9$q73Iw0?$UqA>IlWIaGpK1*DW2M))PYCec`VXWRAjJUqN zWM@N)cU=6QkJ|H7?!qC&8Zh8C?8YS7dE8DArVf4Ki_*@k&`ls(sRo3yo&+k=pjmG~ zrmDx{<|524{#WwR~PH4|IEn$504HHDE~h?JU)DN@Cx$(gJbx!&Hp#>Ss?G- zjKSYXp1a=scCNhE*gzh;f&BH!d8@JhJat|9=^FD=W9@n9YVuFRDCC)&F%fIeE0@YA zjXCp2!^r1}o1s`KFEm!42QHZZ8AdM8+mO_3Ca>E-J~x@i8OHQ{Z8OweUw*dQyo?!B z6Eb5x3D|#6{7*KZ<#cmQ7{(o?Ac$De?{vqoB;nDt9|2Oe@{zE$v zQM=;>?V$<|Rj??=;6FoQ7?x$z3A<&1-q-R+@9ZyCp7gIZn4+R8IR}KRW*E=eTa}ym zm=Hqn(hFURFxibqNjjvPF=n1BsCmxLJxFY053&*k&$zm{J3BwSI|Hz;=frhm2cuK? zW-3WGo5qd~=}uV=5x74bTLW4BgGZlhYDVn~Jd#^HEo`rr-nXc^;Ivk?T^!|RtpumF z7w&16s3fgk!L0@i%jT%8@WqSVWB%3Q!C^JpLa|F-}0JM-V=Lo}6vGq$kCC0g;0)&Fh(_qP9g+yA}o|K9e0Z~MQu{omXE?`{A0w*PzE|Gn-1-u8cQ z`@c7m|7*v+WZiG>`rpA(DgS?Uba=eY|2Of;9S~(P&bBPJt^c9^-(G+E_~zpFiR%BT z{vRBd>wmBIUT*9E8~MoA0zNSjKKk~j%HsC3 zfc~$D>W}*W{qp(mtK(M(Tm9c4|F_40Tm5fmwtuy2%+~+o$?j`ckQ4TVojpMv3449tO`zSYpQdeaDLd`@R>rbZUX<&_vh)7VZ1C zVEiHUu~b2WTkM_OUq2km0~|Uc@`r-V%~4gi*VQMQ z$RypVi-l|0MUy3wpZj~zWCL%&v92rBCFOZWHwp(h252m}t{Lm^=~INDAL3~a_4<{% zfz3I(fz47kunDHdBq^K!%$0U4I~#RlV^)x_>_{xOLE<<(Ldnfijf^-bvOS=~1$_rB z>*mD41&UHOHn3J zpk`O8jP#(MqKi$R*a2!;Uf{)+RTtdv0>4GC$v6(R9N@?4hcMtcbO#@|;3=>I{zV#f zbK|fF^Kfqu2P(RNXukIbUR;OK4ZRd7x>|`~Mvy*4NUvd3*YFz}=@dYQ{dAlwQCxgY z%4ez<(Ptefilm3R(+;?Ybjpr8(i;vaWisHACAb4CPu{=h&yHj zb;ikY)wi}U(uXSB|R1UtH_i+dd}YTxpPkkIv}*zm0=f-yQP8_#Q+=%o+cp(H8C#< zxu>MYdU_^(Ao=o~kDdb#^3cAxFpIo6v3(km&bJ13VE5<*QMrf~`i2{|_Ej(X)rFiPau*5fz1~M z2=qFfC2!aBtH?<}S*_}2q8A|IUNf8UxnBDOyV-@sgSeJji^dn4ryLgoNF7eg4{f)e z2PoeQsStFKvM>qaHh5gGJGMYLU1TX(lNKG1HySzY%0>rYtf0rr5v6V)Jc&X|`h=Vx zGD|%kUEA{k81;Nn<3)O$na2f|8Z*0Rd%)qAa8|(Jn*}f9S=j?36ssBfs$?1X*94;$= zS@{a~V^R#BgH0%6_fZ;&76w{YIvYSq+mKD8U0TYf)GPO`)FF?9x<#-Qv!Bl)-D)UB z8#@JpYo5!XNOAqw(C*7f9)o(<9kAyACF8Cs>myo06Z{tJr~=@lz>n)4fyAU zqXmJ|j`ZR^9xKA#;m-1LfEb2&aCj;=PLwWEZNkmrX0;(!_5?y-#T^N*uHiPO8TTAZ z-zfUm>DlL+#6kMFFU`rDN|tJ08d)GSxrW|)1#&W!(I{Fn?;>(MXE%^Kfs&EFLGIEE zIrS*Lv!fxf|0AAkr)SwcK7hPv#}CnQ{^uK9;v?!!!sxrSKubWL5!cP1Oj-(r#fvEL zfZ_Vtso9WDrw-{;9Y-|S52x%v&e-Wd#&eHXu;gca`g!lm7lHwo`(HA0z;oRHbN1=u zdv*){KIQ>tN@*8VKl%N4yDupuFV1V00m zk1zr-@ijaEAUCP)u|iAh4*Vq>oB{q5GhdtVuN;8`c!1`s{vQH-Qo`w5L%+^vOt-g_ z);71(M#0wF49qCS@eEgOqTfjhP1Ir_&$Z7p5xzjQyXL(av!bcQA6A;I|GtDezKGMqkDySJcR&7kH zv02Eu&qSxfa_8g+o8xqI3-$vQFvW%!8I2NA87;x&5{&vcnL>dK^8#XfDZ7MK^G;d; z3ek(9R*xdkxmH>N=wSs0T|`N$|IDz-gM0_oi!uOsPj*hCOX={}QzD5}=x&mZ?>Tqg zfcuc0XYQn=@5@MyBp4U%A+2G>YLOh5=BL~ws!QlS0C^jEJa9!}!%g<$#r?>RdQ#;d z7vD5E?G~Y-jXupnk-UkSNgxHLM4={ANEwSL%p^)mJ*UO8m9#FCX(3`~sy8R>w7`Vo zTcMG~_y@Cw692rcLXokONnU#HM64=DKoTP`QmmVpRau^EHgF`B94dzi2qhqB!Tc*X zls1~=a9UOh2n#I&ZFDk#I(V8%X@C)WU+dXZmQGJsg7s{1l6*QTD-=_UJ+7% zDZT`vJ$If+B(fc@CQ2)aV|rWzOSx>){%89|G%{BR8-(eJPGX?IyalY66;lo4DW0K( z$1-lAK$ZD@LcWAC+c%UJSimtr)H5ms&YoM2kXNNxGdYELw0~5L0vZ4s%PHP6t888| z|M|E^s)%HoGP1|CvG6X1t_b z_onFqz0;twfdLo7?olb4-cN#Qc-9-)4!+WaB^+;ne!vY!!4I+|klKZnh?^u1$-!&0 zS&?;{a;SNd63win8Vsns4+;9BhN8f*m0O7t^aV_W3mI_~j;pN10_ObbY-=k-QUwN{ z++5$@70ku_sCS=u&mExCo`Q z{j8eE>SaC1*)eRvcWvb|5G~S+n;2kX3Nult=3 zrGfo5|CK12JN)g@>F~ZeC*y1iWOAj&JfB=b$WjJV3jS+c;uOPvywF}+BRO%1py2-r z7WhezqS^Y3f}nOabG{ryv*o3sk2#P8GpEuJnn_pqd?^DVVJ;Jfm6*GZKx|@(#d7pp zxc|o$zP7-Ys;vYrFM$RAu0#Gx3oMcdfCVi`uRaSA_=0uJzs5^MhBU3g{*uLo*#UL_ z;Oa4NRTjj6kF>X{2;C56h|47ACgK3!@Xt40<6hu^dXg^7@hzXpRQa(e4T=Yeh)Ie& z_*<6&_DjTlJ17Oo<{Yk(AXU}rg>;wr6D*RndhyAyuSzDCkN%cnb~)^$a4!v07LWK= z!Odj}uTX$>rqbPsIE+@+-o7`StktR-W%@l=j7;yz-zz}7-o=P*ad%)UU7lf&tDQzS z#ay)IUM7mNbQ~x#By%h;)q;9=PhC|yDg}*}(U9>Na@y*7TCN6z97O5!aw|;5WIaCV zIs2F%%u&O%z~A8EAdC3pIr4`FsLrZ?Q$)R?-fFe<38hp4+2{Fk);S?|bKv{z*!(JU zPjw)eb`QuNlw7|C9`+s_vOX9?(NamsBaS8Zy`3cE7Z zR0~Yi&6R3lVX+x%xXB=JQaILCd01S2mH)vr{L5}O{o*Txav~QFTDJPz*<=wY&sk0= zJrV+1wh96fUwNma?{t=^&rKuD>g~_8g_LgLJT!oV*@t9SPl_P+CIh4OGSz;}bVojyiiB%TRK?&g| z3h}2|-h_S_J@qtg8kgoiQ!BAnKLN4W;LT)*}6!*I(wv-oHmMe-Mg`k^$L^5;o zQFiTzsykTU$o)?LVjdsN*VzDn{>H|`pfZac^Y4DcXF7v@bwL{Hk>^Ool_5+o$ zPlt;&mDHVTKRStE))qdivrPAH_32j$(==DbjA0k>p~w;67Sw$X6TQANLM0}YdsocA znHXNmpZT2FHQlCd>pF!BcXB%^HsZ}D%TMzE_<)3=D>)}efZ5Y4XRnK}hZ$id>1@{p zeGvHCpB>1b(knbjv)c#T_uNVzpyB|)meFoHZk^f~zeZ6Eg3-Ts>CKqx8rO+R* zl@qz;C|*jxt$-ho!G5CTA7`}B!LA^`wlmYP{{fxeQ!38Y;Z7EzPFZ;I`f@R-SZ~m? z@Oji*`ys@pdmVi)e7v-p%ui!carU%Smu|YNbsWLup)Znl%c7H-{9W2CQaVqo+fvbe z;k3E9v2|7JGWd(Pw){b;KCSw;6xqvLdz9`wOO~0IB0M-te6yUq>szXN|FMgtd3;4% z*6CMUC9PaR{#jqO^wCAvQ#D;mpesaX<+h}9jU~nUJ+~kDf0I=Ei10dvS}s8UZc3yl zmi&VXv=o-L%ClZ#!3ae36xf^!JY=VH%?i$XI@<4NBSMvVT-L z%S*>nl9o@de)dD>{=3BZ9b9=GX_eg4nsO=)eDQ?pyHk1{snnWiU4p*&Nj=Rh9MUfy zZa&#+D!R*q`KlmtSsPgZJS%K`ibbqgWOksq3Hw*U-Y1wqu%-mb3wqzRn5RhHQMjzA zq?c}OS@fGZ zrDh5FXpT3D<-k_q1vGi9_!hDJIh*P;nBVZl!5l8%8jBl^6ye3=Ci#C{T;gXa{=dCz zZEYKdqJPDB3@5N8`JdRY*oA0nhxC+CK z&4m9q6O8sdEET>4zrrQ=;NkN>IiwifPeu6l4-IB5I+^QMh|rYvB0v3QCbb(tGc`r= zy{R$q#fm*S?&ScD|8bXrr$?k0#9p7SLuH}fO*}-l{x;KTJ5+^e!Rc^Z*FYCvZ8~j* z${3GqhF7Q+tLX~!bf}a#3Io#9CM^XVe#si!XFMcH9#~L4PQrLkEQ>7Z?QC~(7NHvy zy9iKRJTnMTQ&bTx4{JUT?l!9gHQ_&O^9i50hxxNJSem%-+SGia06Jj?Yd1pQuI`sf z_{6H@HWj4S-=i?55q{UajBeCZe>)&G^98nNJ<(%4o zyeYdJEWo8VcFYKB?>zjm%GMro!X!jci?RFtznp;@H?rUwZZeSmRsru5I@}vE+;An} z0@pRC{C;p?f1(k@wjPRK%!W=T%R({Z=r4F(?cGcsV`+$pnY7Nu|}iSQ0G#-X}f zy1Se>{F(f9>Uw}QL@S)`nd@8mGu)EwnX)mT` zMJqZ~;Y8_14Ocr7EirJpe*4XVX7`{@u^5^%y1&c9Y><+mm1ority!~X&6+i9)~s2x QX6@+gAE*%j!T`ts01l52KmY&$ literal 0 HcmV?d00001 diff --git a/dist/twython-0.6.win32.exe b/dist/twython-0.6.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..2a43765fe94e4f4436a29435688727294866d0bd GIT binary patch literal 71809 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 3a8516d..038ab6a 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '0.5' +__version__ = '0.6' # For the love of god, use Pip to install this. @@ -11,7 +11,7 @@ __version__ = '0.5' METADATA = dict( name = "twython", version = __version__, - py_modules = ['twython2k'], + py_modules = ['twython'], author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', diff --git a/twython.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO index 11e1b16..9d7210c 100644 --- a/twython.egg-info/PKG-INFO +++ b/twython.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: twython -Version: 0.5 +Version: 0.6 Summary: A new and easy way to access Twitter data with Python. Home-page: http://github.com/ryanmcgrath/twython/tree/master Author: Ryan McGrath diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt index dae5317..3a6ce67 100644 --- a/twython.egg-info/SOURCES.txt +++ b/twython.egg-info/SOURCES.txt @@ -1,7 +1,6 @@ README setup.py twython.py -twython2k.py twython.egg-info/PKG-INFO twython.egg-info/SOURCES.txt twython.egg-info/dependency_links.txt diff --git a/twython.egg-info/top_level.txt b/twython.egg-info/top_level.txt index c2e6662..292a670 100644 --- a/twython.egg-info/top_level.txt +++ b/twython.egg-info/top_level.txt @@ -1 +1 @@ -twython2k +twython diff --git a/twython2k.py b/twython.py similarity index 99% rename from twython2k.py rename to twython.py index 051884c..456b6c8 100644 --- a/twython2k.py +++ b/twython.py @@ -18,7 +18,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.5" +__version__ = "0.6" """Twython - Easy Twitter utilities in Python""" diff --git a/twython3k.py b/twython3k.py index 93e2e06..fdcc081 100644 --- a/twython3k.py +++ b/twython3k.py @@ -15,7 +15,7 @@ import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetype from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.5" +__version__ = "0.6" try: import simplejson From 0d37e5be404f5aaf36ae88e999ef06ffc5e5ccf8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 14 Aug 2009 03:42:42 -0400 Subject: [PATCH 095/687] Added getHomeTimeline() support - this isn't a supported feature of the Twitter API just yet, but it's not bad to throw support for it in Twython now. (Twython3k will get an update soon that has this) --- twython.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/twython.py b/twython.py index 456b6c8..073656b 100644 --- a/twython.py +++ b/twython.py @@ -134,6 +134,16 @@ class setup: except HTTPError, e: raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + def getHomeTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TangoError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) + else: + raise TangoError("getHomeTimeline() requires you to be authenticated.") + def getFriendsTimeline(self, **kwargs): if self.authenticated is True: try: From 458dc6dc17b56328be73292e260b75147ec56b0f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 18 Aug 2009 03:30:15 -0400 Subject: [PATCH 096/687] ReTweet API (POST) is supported by Twython (still waiting on Twitter to finish implementation, of course). All errors raised in Twython (related to Twython, of course) are now raised as 'TwythonError', essentially replacing the old 'TangoError' method. --- twython.py | 181 ++++++++++++++++++++++++++++------------------------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/twython.py b/twython.py index 073656b..f2f7af7 100644 --- a/twython.py +++ b/twython.py @@ -35,7 +35,7 @@ try: except ImportError: pass -class TangoError(Exception): +class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg if error_code == 400: @@ -43,7 +43,7 @@ class TangoError(Exception): def __str__(self): return repr(self.msg) -class APILimit(TangoError): +class APILimit(TwythonError): def __init__(self, msg): self.msg = msg def __str__(self): @@ -78,7 +78,7 @@ class setup: simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) else: self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() # Awesome OAuth authentication ritual @@ -111,7 +111,7 @@ class setup: try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) + raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) @@ -124,15 +124,15 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") + raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) def getPublicTimeline(self): try: return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) + raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) def getHomeTimeline(self, **kwargs): if self.authenticated is True: @@ -140,9 +140,9 @@ class setup: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: - raise TangoError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) + raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) else: - raise TangoError("getHomeTimeline() requires you to be authenticated.") + raise TwythonError("getHomeTimeline() requires you to be authenticated.") def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -150,9 +150,9 @@ class setup: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") + raise TwythonError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: @@ -168,7 +168,7 @@ class setup: else: return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % `e.code`, e.code) def getUserMentions(self, **kwargs): @@ -177,9 +177,9 @@ class setup: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getUserMentions() requires you to be authenticated.") + raise TwythonError("getUserMentions() requires you to be authenticated.") def showStatus(self, id): try: @@ -188,28 +188,37 @@ class setup: else: return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) def updateStatus(self, status, in_reply_to_status_id = None): if self.authenticated is True: if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") + raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateStatus() requires you to be authenticated.") + raise TwythonError("updateStatus() requires you to be authenticated.") def destroyStatus(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyStatus() requires you to be authenticated.") + raise TwythonError("destroyStatus() requires you to be authenticated.") + + def reTweet(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + except HTTPError, e: + raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("reTweet() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -217,9 +226,9 @@ class setup: self.opener.open("http://twitter.com/account/end_session.json", "") self.authenticated = False except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") + raise TwythonError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -234,9 +243,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getDirectMessages() requires you to be authenticated.") + raise TwythonError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: @@ -251,9 +260,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getSentMessages() requires you to be authenticated.") + raise TwythonError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -261,20 +270,20 @@ class setup: try: return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("Your message must not be longer than 140 characters") + raise TwythonError("Your message must not be longer than 140 characters") else: - raise TangoError("You must be authenticated to send a new direct message.") + raise TwythonError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You must be authenticated to destroy a direct message.") + raise TwythonError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: @@ -290,10 +299,10 @@ class setup: except HTTPError, e: # Rate limiting is done differently here for API reasons... if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") + raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createFriendship() requires you to be authenticated.") + raise TwythonError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -307,36 +316,36 @@ class setup: try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyFriendship() requires you to be authenticated.") + raise TwythonError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + raise TwythonError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") + raise TwythonError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateProfileColors() requires you to be authenticated.") + raise TwythonError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -347,7 +356,7 @@ class setup: updateProfileQueryString += "name=" + name useAmpersands = True else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") if email is not None and "@" in email: if len(list(email)) < 40: if useAmpersands is True: @@ -356,7 +365,7 @@ class setup: updateProfileQueryString += "email=" + email useAmpersands = True else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") if url is not None: if len(list(url)) < 100: if useAmpersands is True: @@ -365,7 +374,7 @@ class setup: updateProfileQueryString += urllib.urlencode({"url": url}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: @@ -374,7 +383,7 @@ class setup: updateProfileQueryString += urllib.urlencode({"location": location}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: @@ -382,42 +391,42 @@ class setup: else: updateProfileQueryString += urllib.urlencode({"description": description}) else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") + raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateProfile() requires you to be authenticated.") + raise TwythonError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getFavorites() requires you to be authenticated.") + raise TwythonError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createFavorite() requires you to be authenticated.") + raise TwythonError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyFavorite() requires you to be authenticated.") + raise TwythonError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -431,9 +440,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("notificationFollow() requires you to be authenticated.") + raise TwythonError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: @@ -447,9 +456,9 @@ class setup: try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("notificationLeave() requires you to be authenticated.") + raise TwythonError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -462,7 +471,7 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" @@ -475,25 +484,25 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) def createBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createBlock() requires you to be authenticated.") + raise TwythonError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyBlock() requires you to be authenticated.") + raise TwythonError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" @@ -506,32 +515,32 @@ class setup: try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) def getBlocking(self, page = "1"): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getBlocking() requires you to be authenticated") + raise TwythonError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") + raise TwythonError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: - raise TangoError("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 getCurrentTrends(self, excludeHashTags = False): apiURL = "http://search.twitter.com/trends/current.json" @@ -540,7 +549,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) def getDailyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -556,7 +565,7 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) def getWeeklyTrends(self, date = None, exclude = False): apiURL = "http://search.twitter.com/trends/daily.json" @@ -572,43 +581,43 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) def getSavedSearches(self): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getSavedSearches() requires you to be authenticated.") + raise TwythonError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("showSavedSearch() requires you to be authenticated.") + raise TwythonError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createSavedSearch() requires you to be authenticated.") + raise TwythonError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") + raise TwythonError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -621,9 +630,9 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") + raise TwythonError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -635,9 +644,9 @@ class setup: r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") + raise TwythonError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() From 035dcdb264a53528df19cf3135de20ef5e993bc0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 19 Aug 2009 02:57:08 -0400 Subject: [PATCH 097/687] Retweeting API is now supported in full - Twython3k is also up to date and on the same level as the Twython2k build. --- twython.py | 30 +++++++++++++ twython3k.py | 117 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 123 insertions(+), 24 deletions(-) diff --git a/twython.py b/twython.py index f2f7af7..10b15a7 100644 --- a/twython.py +++ b/twython.py @@ -181,6 +181,36 @@ class setup: else: raise TwythonError("getUserMentions() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("retweetedOfMe() requires you to be authenticated.") + + def retweetedByMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("retweetedByMe() requires you to be authenticated.") + + def retweetedToMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("retweetedToMe() requires you to be authenticated.") + def showStatus(self, id): try: if self.authenticated is True: diff --git a/twython3k.py b/twython3k.py index fdcc081..0ab38a7 100644 --- a/twython3k.py +++ b/twython3k.py @@ -1,6 +1,8 @@ #!/usr/bin/python """ + NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. + Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -12,18 +14,21 @@ import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools +from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " __version__ = "0.6" +"""Twython - Easy Twitter utilities in Python""" + try: import simplejson except ImportError: try: import json as simplejson except: - raise Exception("Twython requires a json library to work. http://www.undefined.org/python/") + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: import oauth @@ -45,46 +50,61 @@ class APILimit(TwythonError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): + def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username self.password = password - self.oauth_keys = oauth_keys + # OAuth specific variables below + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.request_token = None + self.access_token = None + # Check and set up authentication if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": + if self.authtype == "Basic": + # Basic authentication ritual self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) if headers is not None: self.opener.addheaders = [('User-agent', headers)] - """ try: - test_verify = simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) - """ + else: + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + # Awesome OAuth authentication ritual + if consumer_secret is not None and consumer_key is not None: + #req = oauth.OAuthRequest.from_consumer_and_token + #req.sign_request(self.signature_method, self.consumer_key, self.token) + #self.opener = urllib2.build_opener() + pass + else: + raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass + def getRequestToken(self): + response = self.oauth_request(self.request_token_url) + token = self.parseOAuthResponse(response) + self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) + return self.request_token - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass + def parseOAuthResponse(self, response_string): + # Partial credit goes to Harper Reed for this gem. + lol = {} + for param in response_string.split("&"): + pair = param.split("=") + if(len(pair) != 2): + break + lol[pair[0]] = pair[1] + return lol # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -114,6 +134,16 @@ class setup: except HTTPError as e: raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) + def getHomeTimeline(self, **kwargs): + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError as e: + raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) + else: + raise TwythonError("getHomeTimeline() requires you to be authenticated.") + def getFriendsTimeline(self, **kwargs): if self.authenticated is True: try: @@ -151,6 +181,36 @@ class setup: else: raise TwythonError("getUserMentions() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError as e: + raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) + else: + raise TwythonError("retweetedOfMe() requires you to be authenticated.") + + def retweetedByMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError as e: + raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) + else: + raise TwythonError("retweetedByMe() requires you to be authenticated.") + + def retweetedToMe(self, **kwargs): + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError as e: + raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) + else: + raise TwythonError("retweetedToMe() requires you to be authenticated.") + def showStatus(self, id): try: if self.authenticated is True: @@ -181,6 +241,15 @@ class setup: else: raise TwythonError("destroyStatus() requires you to be authenticated.") + def reTweet(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + except HTTPError as e: + raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) + else: + raise TwythonError("reTweet() requires you to be authenticated.") + def endSession(self): if self.authenticated is True: try: From 6ab69d4636709bf5b2e681c62d59eda19cf954cd Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 23 Aug 2009 04:14:23 -0400 Subject: [PATCH 098/687] Incremental commit; started decorator function for auth checking, new exception (twython.AuthError) which will (in the future) be raised in the event of authentication failure. --- twython.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/twython.py b/twython.py index 10b15a7..ca8ed8f 100644 --- a/twython.py +++ b/twython.py @@ -49,6 +49,18 @@ class APILimit(TwythonError): def __str__(self): return repr(self.msg) +class AuthError(TwythonError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +# A simple decorator to clean up some authentication checks that exist all over. +# Not implemented yet - +def requires_authentication(func): + print func + # raise AuthError("This function requires you to be authenticated. Double check that and try again!") + class setup: def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): self.authtype = authtype @@ -222,15 +234,12 @@ class setup: % `e.code`, e.code) def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("updateStatus() requires you to be authenticated.") + if len(list(status)) > 140: + raise TwythonError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) def destroyStatus(self, id): if self.authenticated is True: From 90789b73eb071b11fb3d9ff60c152989d9b9c3af Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 24 Aug 2009 02:47:02 -0400 Subject: [PATCH 099/687] Fairly large commit - this should fix a slew of issues with passing results from functions (follower ids, for instance) to other functions. Before, they were returned as Numbers, but most functions expect Strings, so there's an extra conversion layer now which should help out on that front. urlencode also properly encodes to utf-8 now (major thanks to contributions from Maatsu on this). Password is also no longer stored as an instance variable. These changes are mirrored in Twython3k, but I've not yet had time to test that in full - as with anything Python3k related, proceed with caution. (There are also some changes relating to how string concatenation is done, but that's all minor in scope) --- twython.py | 197 ++++++++++++++++++++++++----------------------- twython3k.py | 210 ++++++++++++++++++++++++++------------------------- 2 files changed, 207 insertions(+), 200 deletions(-) diff --git a/twython.py b/twython.py index ca8ed8f..5dfe4a5 100644 --- a/twython.py +++ b/twython.py @@ -1,8 +1,6 @@ #!/usr/bin/python """ - NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. - Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -18,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.6" +__version__ = "0.7a" """Twython - Easy Twitter utilities in Python""" @@ -55,18 +53,11 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) -# A simple decorator to clean up some authentication checks that exist all over. -# Not implemented yet - -def requires_authentication(func): - print func - # raise AuthError("This function requires you to be authenticated. Double check that and try again!") - class setup: def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username - self.password = password # OAuth specific variables below self.request_token_url = 'https://twitter.com/oauth/request_token' self.access_token_url = 'https://twitter.com/oauth/access_token' @@ -77,11 +68,11 @@ class setup: self.request_token = None self.access_token = None # Check and set up authentication - if self.username is not None and self.password is not None: + if self.username is not None and password is not None: if self.authtype == "Basic": # Basic authentication ritual self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.auth_manager.add_password(None, "http://twitter.com", self.username, password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) if headers is not None: @@ -121,7 +112,7 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() except HTTPError, e: raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) @@ -154,7 +145,7 @@ class setup: except HTTPError, e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) else: - raise TwythonError("getHomeTimeline() requires you to be authenticated.") + raise AuthError("getHomeTimeline() requires you to be authenticated.") def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -164,13 +155,13 @@ class setup: except HTTPError, e: raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: - raise TwythonError("getFriendsTimeline() requires you to be authenticated.") + raise AuthError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % `id`, kwargs) elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) else: userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) try: @@ -191,7 +182,7 @@ class setup: except HTTPError, e: raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getUserMentions() requires you to be authenticated.") + raise AuthError("getUserMentions() requires you to be authenticated.") def retweetedOfMe(self, **kwargs): if self.authenticated is True: @@ -201,7 +192,7 @@ class setup: except HTTPError, e: raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("retweetedOfMe() requires you to be authenticated.") + raise AuthError("retweetedOfMe() requires you to be authenticated.") def retweetedByMe(self, **kwargs): if self.authenticated is True: @@ -211,7 +202,7 @@ class setup: except HTTPError, e: raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("retweetedByMe() requires you to be authenticated.") + raise AuthError("retweetedByMe() requires you to be authenticated.") def retweetedToMe(self, **kwargs): if self.authenticated is True: @@ -221,7 +212,7 @@ class setup: except HTTPError, e: raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("retweetedToMe() requires you to be authenticated.") + raise AuthError("retweetedToMe() requires you to be authenticated.") def showStatus(self, id): try: @@ -237,7 +228,7 @@ class setup: if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) @@ -248,7 +239,7 @@ class setup: except HTTPError, e: raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("destroyStatus() requires you to be authenticated.") + raise AuthError("destroyStatus() requires you to be authenticated.") def reTweet(self, id): if self.authenticated is True: @@ -257,7 +248,7 @@ class setup: except HTTPError, e: raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("reTweet() requires you to be authenticated.") + raise AuthError("reTweet() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -267,41 +258,41 @@ class setup: except HTTPError, e: raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("You can't end a session when you're not authenticated to begin with.") + raise AuthError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page + apiURL = "http://twitter.com/direct_messages.json?page=%s" % `page` if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % `since_id` if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % `max_id` if count is not None: - apiURL += "&count=" + count + apiURL += "&count=%s" % `count` try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getDirectMessages() requires you to be authenticated.") + raise AuthError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % `page` if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % `since_id` if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % `max_id` if count is not None: - apiURL += "&count=" + count + apiURL += "&count=%s" % `count` try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getSentMessages() requires you to be authenticated.") + raise AuthError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -313,7 +304,7 @@ class setup: else: raise TwythonError("Your message must not be longer than 140 characters") else: - raise TwythonError("You must be authenticated to send a new direct message.") + raise AuthError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: @@ -322,17 +313,17 @@ class setup: except HTTPError, e: raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("You must be authenticated to destroy a direct message.") + raise AuthError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + apiURL = "http://twitter.com/friendships/create/%s.json?follow=%s" %(id, follow) if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + apiURL = "http://twitter.com/friendships/create.json?user_id=%s&follow=%s" %(`user_id`, follow) if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + apiURL = "http://twitter.com/friendships/create.json?screen_name=%s&follow=%s" %(screen_name, follow) try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: @@ -341,23 +332,23 @@ class setup: raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("createFriendship() requires you to be authenticated.") + raise AuthError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + apiURL = "http://twitter.com/friendships/destroy/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + apiURL = "http://twitter.com/friendships/destroy.json?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("destroyFriendship() requires you to be authenticated.") + raise AuthError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: @@ -366,16 +357,16 @@ class setup: except HTTPError, e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": self.unicode2utf8(device_name)})) except HTTPError, e: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("updateDeliveryDevice() requires you to be authenticated.") + raise AuthError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: @@ -384,7 +375,7 @@ class setup: except HTTPError, e: raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("updateProfileColors() requires you to be authenticated.") + raise AuthError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -408,27 +399,27 @@ class setup: if url is not None: if len(list(url)) < 100: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) else: - updateProfileQueryString += urllib.urlencode({"url": url}) + updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) useAmpersands = True else: raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) else: - updateProfileQueryString += urllib.urlencode({"location": location}) + updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) useAmpersands = True else: raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) else: - updateProfileQueryString += urllib.urlencode({"description": description}) + updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) else: raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") @@ -438,75 +429,75 @@ class setup: except HTTPError, e: raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("updateProfile() requires you to be authenticated.") + raise AuthError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % `page`)) except HTTPError, e: raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getFavorites() requires you to be authenticated.") + raise AuthError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % `id`, "")) except HTTPError, e: raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("createFavorite() requires you to be authenticated.") + raise AuthError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % `id`, "")) except HTTPError, e: - raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("destroyFavorite() requires you to be authenticated.") + raise AuthError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + apiURL = "http://twitter.com/notifications/follow/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("notificationFollow() requires you to be authenticated.") + raise AuthError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + apiURL = "http://twitter.com/notifications/leave/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("notificationLeave() requires you to be authenticated.") + raise AuthError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, `page`) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: @@ -515,11 +506,11 @@ class setup: def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(`id`, `page`) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: @@ -528,29 +519,29 @@ class setup: def createBlock(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % `id`, "")) except HTTPError, e: raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("createBlock() requires you to be authenticated.") + raise AuthError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % `id`, "")) except HTTPError, e: raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("destroyBlock() requires you to be authenticated.") + raise AuthError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + apiURL = "http://twitter.com/blocks/exists/%s.json" % `id` if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: @@ -559,11 +550,11 @@ class setup: def getBlocking(self, page = "1"): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % `page`)) except HTTPError, e: raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getBlocking() requires you to be authenticated") + raise AuthError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: @@ -572,10 +563,10 @@ class setup: except HTTPError, e: raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getBlockedIDs() requires you to be authenticated.") + raise AuthError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: @@ -594,7 +585,7 @@ class setup: apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -610,7 +601,7 @@ class setup: apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -629,34 +620,34 @@ class setup: except HTTPError, e: raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("getSavedSearches() requires you to be authenticated.") + raise AuthError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % `id`)) except HTTPError, e: raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("showSavedSearch() requires you to be authenticated.") + raise AuthError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) except HTTPError, e: raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("createSavedSearch() requires you to be authenticated.") + raise AuthError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % `id`, "")) except HTTPError, e: raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("destroySavedSearch() requires you to be authenticated.") + raise AuthError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -671,7 +662,7 @@ class setup: except HTTPError, e: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("You realize you need to be authenticated to change a background image, right?") + raise AuthError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -685,7 +676,7 @@ class setup: except HTTPError, e: raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) else: - raise TwythonError("You realize you need to be authenticated to change a profile image, right?") + raise AuthError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() @@ -710,3 +701,11 @@ class setup: def get_content_type(self, filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + def unicode2utf8(self, text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text diff --git a/twython3k.py b/twython3k.py index 0ab38a7..6f59d70 100644 --- a/twython3k.py +++ b/twython3k.py @@ -1,8 +1,6 @@ #!/usr/bin/python """ - NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. - Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -18,7 +16,7 @@ from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.6" +__version__ = "0.7a" """Twython - Easy Twitter utilities in Python""" @@ -49,12 +47,17 @@ class APILimit(TwythonError): def __str__(self): return repr(self.msg) +class AuthError(TwythonError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + class setup: def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): self.authtype = authtype self.authenticated = False self.username = username - self.password = password # OAuth specific variables below self.request_token_url = 'https://twitter.com/oauth/request_token' self.access_token_url = 'https://twitter.com/oauth/access_token' @@ -65,11 +68,11 @@ class setup: self.request_token = None self.access_token = None # Check and set up authentication - if self.username is not None and self.password is not None: + if self.username is not None and password is not None: if self.authtype == "Basic": # Basic authentication ritual self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) + self.auth_manager.add_password(None, "http://twitter.com", self.username, password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) if headers is not None: @@ -109,7 +112,7 @@ class setup: # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): try: - return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() except HTTPError as e: raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) @@ -142,7 +145,7 @@ class setup: except HTTPError as e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) else: - raise TwythonError("getHomeTimeline() requires you to be authenticated.") + raise AuthError("getHomeTimeline() requires you to be authenticated.") def getFriendsTimeline(self, **kwargs): if self.authenticated is True: @@ -152,13 +155,13 @@ class setup: except HTTPError as e: raise TwythonError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) else: - raise TwythonError("getFriendsTimeline() requires you to be authenticated.") + raise AuthError("getFriendsTimeline() requires you to be authenticated.") def getUserTimeline(self, id = None, **kwargs): if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % repr(id), kwargs) elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) else: userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) try: @@ -179,7 +182,7 @@ class setup: except HTTPError as e: raise TwythonError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getUserMentions() requires you to be authenticated.") + raise AuthError("getUserMentions() requires you to be authenticated.") def retweetedOfMe(self, **kwargs): if self.authenticated is True: @@ -189,7 +192,7 @@ class setup: except HTTPError as e: raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("retweetedOfMe() requires you to be authenticated.") + raise AuthError("retweetedOfMe() requires you to be authenticated.") def retweetedByMe(self, **kwargs): if self.authenticated is True: @@ -199,7 +202,7 @@ class setup: except HTTPError as e: raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("retweetedByMe() requires you to be authenticated.") + raise AuthError("retweetedByMe() requires you to be authenticated.") def retweetedToMe(self, **kwargs): if self.authenticated is True: @@ -209,7 +212,7 @@ class setup: except HTTPError as e: raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("retweetedToMe() requires you to be authenticated.") + raise AuthError("retweetedToMe() requires you to be authenticated.") def showStatus(self, id): try: @@ -222,15 +225,12 @@ class setup: % repr(e.code), e.code) def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError as e: - raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise TwythonError("updateStatus() requires you to be authenticated.") + if len(list(status)) > 140: + raise TwythonError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError as e: + raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) def destroyStatus(self, id): if self.authenticated is True: @@ -239,7 +239,7 @@ class setup: except HTTPError as e: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("destroyStatus() requires you to be authenticated.") + raise AuthError("destroyStatus() requires you to be authenticated.") def reTweet(self, id): if self.authenticated is True: @@ -248,7 +248,7 @@ class setup: except HTTPError as e: raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("reTweet() requires you to be authenticated.") + raise AuthError("reTweet() requires you to be authenticated.") def endSession(self): if self.authenticated is True: @@ -258,41 +258,41 @@ class setup: except HTTPError as e: raise TwythonError("endSession failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("You can't end a session when you're not authenticated to begin with.") + raise AuthError("You can't end a session when you're not authenticated to begin with.") def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page + apiURL = "http://twitter.com/direct_messages.json?page=%s" % repr(page) if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % repr(since_id) if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % repr(max_id) if count is not None: - apiURL += "&count=" + count + apiURL += "&count=%s" % repr(count) try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getDirectMessages() requires you to be authenticated.") + raise AuthError("getDirectMessages() requires you to be authenticated.") def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % repr(page) if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % repr(since_id) if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % repr(max_id) if count is not None: - apiURL += "&count=" + count + apiURL += "&count=%s" % repr(count) try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getSentMessages() requires you to be authenticated.") + raise AuthError("getSentMessages() requires you to be authenticated.") def sendDirectMessage(self, user, text): if self.authenticated is True: @@ -304,7 +304,7 @@ class setup: else: raise TwythonError("Your message must not be longer than 140 characters") else: - raise TwythonError("You must be authenticated to send a new direct message.") + raise AuthError("You must be authenticated to send a new direct message.") def destroyDirectMessage(self, id): if self.authenticated is True: @@ -313,17 +313,17 @@ class setup: except HTTPError as e: raise TwythonError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("You must be authenticated to destroy a direct message.") + raise AuthError("You must be authenticated to destroy a direct message.") def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow + apiURL = "http://twitter.com/friendships/create/%s.json?follow=%s" %(id, follow) if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + apiURL = "http://twitter.com/friendships/create.json?user_id=%s&follow=%s" %(repr(user_id), follow) if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + apiURL = "http://twitter.com/friendships/create.json?screen_name=%s&follow=%s" %(screen_name, follow) try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: @@ -332,23 +332,23 @@ class setup: raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") raise TwythonError("createFriendship() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("createFriendship() requires you to be authenticated.") + raise AuthError("createFriendship() requires you to be authenticated.") def destroyFriendship(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" + apiURL = "http://twitter.com/friendships/destroy/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + apiURL = "http://twitter.com/friendships/destroy.json?user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + apiURL = "http://twitter.com/friendships/destroy.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("destroyFriendship() requires you to be authenticated.") + raise AuthError("destroyFriendship() requires you to be authenticated.") def checkIfFriendshipExists(self, user_a, user_b): if self.authenticated is True: @@ -357,16 +357,16 @@ class setup: except HTTPError as e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") def updateDeliveryDevice(self, device_name = "none"): if self.authenticated is True: try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": device_name})) + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": self.unicode2utf8(device_name)})) except HTTPError as e: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("updateDeliveryDevice() requires you to be authenticated.") + raise AuthError("updateDeliveryDevice() requires you to be authenticated.") def updateProfileColors(self, **kwargs): if self.authenticated is True: @@ -375,7 +375,7 @@ class setup: except HTTPError as e: raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("updateProfileColors() requires you to be authenticated.") + raise AuthError("updateProfileColors() requires you to be authenticated.") def updateProfile(self, name = None, email = None, url = None, location = None, description = None): if self.authenticated is True: @@ -399,27 +399,27 @@ class setup: if url is not None: if len(list(url)) < 100: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"url": url}) + updateProfileQueryString += "&" + urllib.parse.urlencode({"url": self.unicode2utf8(url)}) else: - updateProfileQueryString += urllib.parse.urlencode({"url": url}) + updateProfileQueryString += urllib.parse.urlencode({"url": self.unicode2utf8(url)}) useAmpersands = True else: raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"location": location}) + updateProfileQueryString += "&" + urllib.parse.urlencode({"location": self.unicode2utf8(location)}) else: - updateProfileQueryString += urllib.parse.urlencode({"location": location}) + updateProfileQueryString += urllib.parse.urlencode({"location": self.unicode2utf8(location)}) useAmpersands = True else: raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"description": description}) + updateProfileQueryString += "&" + urllib.parse.urlencode({"description": self.unicode2utf8(description)}) else: - updateProfileQueryString += urllib.parse.urlencode({"description": description}) + updateProfileQueryString += urllib.parse.urlencode({"description": self.unicode2utf8(description)}) else: raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") @@ -429,75 +429,75 @@ class setup: except HTTPError as e: raise TwythonError("updateProfile() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("updateProfile() requires you to be authenticated.") + raise AuthError("updateProfile() requires you to be authenticated.") def getFavorites(self, page = "1"): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % repr(page))) except HTTPError as e: raise TwythonError("getFavorites() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getFavorites() requires you to be authenticated.") + raise AuthError("getFavorites() requires you to be authenticated.") def createFavorite(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % repr(id), "")) except HTTPError as e: raise TwythonError("createFavorite() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("createFavorite() requires you to be authenticated.") + raise AuthError("createFavorite() requires you to be authenticated.") def destroyFavorite(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % repr(id), "")) except HTTPError as e: - raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("destroyFavorite() requires you to be authenticated.") + raise AuthError("destroyFavorite() requires you to be authenticated.") def notificationFollow(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + apiURL = "http://twitter.com/notifications/follow/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: raise TwythonError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("notificationFollow() requires you to be authenticated.") + raise AuthError("notificationFollow() requires you to be authenticated.") def notificationLeave(self, id = None, user_id = None, screen_name = None): if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + apiURL = "http://twitter.com/notifications/leave/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: raise TwythonError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("notificationLeave() requires you to be authenticated.") + raise AuthError("notificationLeave() requires you to be authenticated.") def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, repr(page)) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(repr(user_id), repr(page)) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, repr(page)) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: @@ -506,11 +506,11 @@ class setup: def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): apiURL = "" if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(repr(id), repr(page)) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(repr(user_id), repr(page)) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, repr(page)) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: @@ -519,29 +519,29 @@ class setup: def createBlock(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % repr(id), "")) except HTTPError as e: raise TwythonError("createBlock() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("createBlock() requires you to be authenticated.") + raise AuthError("createBlock() requires you to be authenticated.") def destroyBlock(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % repr(id), "")) except HTTPError as e: raise TwythonError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("destroyBlock() requires you to be authenticated.") + raise AuthError("destroyBlock() requires you to be authenticated.") def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): apiURL = "" if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + apiURL = "http://twitter.com/blocks/exists/%s.json" % repr(id) if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: @@ -550,11 +550,11 @@ class setup: def getBlocking(self, page = "1"): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % repr(page))) except HTTPError as e: raise TwythonError("getBlocking() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getBlocking() requires you to be authenticated") + raise AuthError("getBlocking() requires you to be authenticated") def getBlockedIDs(self): if self.authenticated is True: @@ -563,10 +563,10 @@ class setup: except HTTPError as e: raise TwythonError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getBlockedIDs() requires you to be authenticated.") + raise AuthError("getBlockedIDs() requires you to be authenticated.") def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": search_query}) + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) try: return simplejson.load(urllib.request.urlopen(searchURL)) except HTTPError as e: @@ -585,7 +585,7 @@ class setup: apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -601,7 +601,7 @@ class setup: apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -620,34 +620,34 @@ class setup: except HTTPError as e: raise TwythonError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("getSavedSearches() requires you to be authenticated.") + raise AuthError("getSavedSearches() requires you to be authenticated.") def showSavedSearch(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % repr(id))) except HTTPError as e: raise TwythonError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("showSavedSearch() requires you to be authenticated.") + raise AuthError("showSavedSearch() requires you to be authenticated.") def createSavedSearch(self, query): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) except HTTPError as e: raise TwythonError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("createSavedSearch() requires you to be authenticated.") + raise AuthError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id): if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % repr(id), "")) except HTTPError as e: raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("destroySavedSearch() requires you to be authenticated.") + raise AuthError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): @@ -662,7 +662,7 @@ class setup: except HTTPError as e: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("You realize you need to be authenticated to change a background image, right?") + raise AuthError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename): if self.authenticated is True: @@ -676,7 +676,7 @@ class setup: except HTTPError as e: raise TwythonError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) else: - raise TwythonError("You realize you need to be authenticated to change a profile image, right?") + raise AuthError("You realize you need to be authenticated to change a profile image, right?") def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() @@ -701,3 +701,11 @@ class setup: def get_content_type(self, filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + def unicode2utf8(self, text): + try: + if isinstance(text, str): + text = text.encode('utf-8') + except: + pass + return text From f760ba13584b0ef28424d8667d0a74a9be422b7e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 25 Aug 2009 01:05:19 -0400 Subject: [PATCH 100/687] Somehow I missed the showUser/friendsStatus/followersStatus methods up until now. Not sure how, but major thanks go to Chris Babcock for pointing this out to me. Any commits after this will be OAuth and Docs focused - might be nearing a 1.0 release! ;) --- twython.py | 71 ++++++++++++++++++++++++++++++++++++++++++++-------- twython3k.py | 71 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/twython.py b/twython.py index 5dfe4a5..d43c197 100644 --- a/twython.py +++ b/twython.py @@ -140,8 +140,8 @@ class setup: def getHomeTimeline(self, **kwargs): if self.authenticated is True: try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) + homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + return simplejson.load(self.opener.open(homeTimelineURL)) except HTTPError, e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) else: @@ -184,6 +184,15 @@ class setup: else: raise AuthError("getUserMentions() requires you to be authenticated.") + def reTweet(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + except HTTPError, e: + raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("reTweet() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): if self.authenticated is True: try: @@ -214,6 +223,55 @@ class setup: else: raise AuthError("retweetedToMe() requires you to be authenticated.") + def showUser(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/users/show/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("showUser() requires you to be authenticated.") + + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/friends/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFriendsStatus() requires you to be authenticated.") + + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/followers/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFollowersStatus() requires you to be authenticated.") + + def showStatus(self, id): try: if self.authenticated is True: @@ -241,15 +299,6 @@ class setup: else: raise AuthError("destroyStatus() requires you to be authenticated.") - def reTweet(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) - except HTTPError, e: - raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - def endSession(self): if self.authenticated is True: try: diff --git a/twython3k.py b/twython3k.py index 6f59d70..96ff348 100644 --- a/twython3k.py +++ b/twython3k.py @@ -140,8 +140,8 @@ class setup: def getHomeTimeline(self, **kwargs): if self.authenticated is True: try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) + homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + return simplejson.load(self.opener.open(homeTimelineURL)) except HTTPError as e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) else: @@ -184,6 +184,15 @@ class setup: else: raise AuthError("getUserMentions() requires you to be authenticated.") + def reTweet(self, id): + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + except HTTPError as e: + raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) + else: + raise AuthError("reTweet() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): if self.authenticated is True: try: @@ -214,6 +223,55 @@ class setup: else: raise AuthError("retweetedToMe() requires you to be authenticated.") + def showUser(self, id = None, user_id = None, screen_name = None): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/users/show/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/users/show.json?user_id=%s" % repr(user_id) + if screen_name is not None: + apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) + else: + raise AuthError("showUser() requires you to be authenticated.") + + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/friends/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % repr(user_id) + if screen_name is not None: + apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + except HTTPError as e: + raise TwythonError("getFriendsStatus() failed with a %s error code." % repr(e.code), e.code) + else: + raise AuthError("getFriendsStatus() requires you to be authenticated.") + + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/followers/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % repr(user_id) + if screen_name is not None: + apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + except HTTPError as e: + raise TwythonError("getFollowersStatus() failed with a %s error code." % repr(e.code), e.code) + else: + raise AuthError("getFollowersStatus() requires you to be authenticated.") + + def showStatus(self, id): try: if self.authenticated is True: @@ -241,15 +299,6 @@ class setup: else: raise AuthError("destroyStatus() requires you to be authenticated.") - def reTweet(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) - except HTTPError as e: - raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - def endSession(self): if self.authenticated is True: try: From 88d89f56520a353edd07542b073c5521bd4fd810 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 28 Aug 2009 02:01:25 -0400 Subject: [PATCH 101/687] Docstrings are now in place, thanks to some awesome work by Kulbir Saini. New 0.8 release is about to go out. :D --- twython.py | 622 ++++++++++++++++++++++++++++++++++++++++++++++----- twython3k.py | 622 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 1124 insertions(+), 120 deletions(-) diff --git a/twython.py b/twython.py index d43c197..423a2ca 100644 --- a/twython.py +++ b/twython.py @@ -16,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.7a" +__version__ = "0.8" """Twython - Easy Twitter utilities in Python""" @@ -55,6 +55,18 @@ class AuthError(TwythonError): class setup: def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + authtype - "OAuth"/"Basic" + username - Your twitter username + password - Password for your twitter account. + consumer_secret - Consumer secret in case you specified for OAuth as authtype. + consumer_key - Consumer key in case you specified for OAuth as authtype. + headers - User agent header. + """ self.authtype = authtype self.authenticated = False self.username = username @@ -92,13 +104,13 @@ class setup: pass else: raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - + def getRequestToken(self): response = self.oauth_request(self.request_token_url) token = self.parseOAuthResponse(response) self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) return self.request_token - + def parseOAuthResponse(self, response_string): # Partial credit goes to Harper Reed for this gem. lol = {} @@ -108,18 +120,34 @@ class setup: break lol[pair[0]] = pair[1] return lol - + # URL Shortening function huzzah def shortenURL(self, 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 url shorterning service other that is.gd. + """ try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() except HTTPError, e: raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) - + def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - + def getRateLimitStatus(self, rate_for = "requestingIP"): + """getRateLimitStatus() + + Returns the remaining number of API requests available to the requesting user before the + API limit is reached for the current hour. Calls to rate_limit_status do not count against + the rate limit. If authentication credentials are provided, the rate limit status for the + authenticating user is returned. Otherwise, the rate limit status for the requesting + IP address is returned. + """ try: if rate_for == "requestingIP": return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) @@ -130,14 +158,35 @@ class setup: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - + def getPublicTimeline(self): + """getPublicTimeline() + + Returns the 20 most recent statuses from non-protected users who have set a custom user icon. + The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + """ try: return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError, e: raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) - + def getHomeTimeline(self, **kwargs): + """getHomeTimeline(**kwargs) + + Returns the 20 most recent statuses, including retweets, posted by the authenticating user + and that user's friends. This is the equivalent of /timeline/home on the Web. + + Usage note: This home_timeline is identical to statuses/friends_timeline, except it also + contains retweets, which statuses/friends_timeline does not (for backwards compatibility + reasons). In a future version of the API, statuses/friends_timeline will go away and + be replaced by home_timeline. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) @@ -146,8 +195,19 @@ class setup: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) else: raise AuthError("getHomeTimeline() requires you to be authenticated.") - + def getFriendsTimeline(self, **kwargs): + """getFriendsTimeline(**kwargs) + + Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. + This is the equivalent of /timeline/home on the Web. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) @@ -156,8 +216,23 @@ class setup: raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: raise AuthError("getFriendsTimeline() requires you to be authenticated.") - + def getUserTimeline(self, id = None, **kwargs): + """getUserTimeline(id = None, **kwargs) + + Returns the 20 most recent statuses posted from the authenticating user. It's also + possible to request another user's timeline via the id parameter. This is the + equivalent of the Web / page for your own user, or the profile page for a third party. + + Parameters: + id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. + user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. + screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % `id`, kwargs) elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: @@ -173,8 +248,18 @@ class setup: except HTTPError, e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % `e.code`, e.code) - + def getUserMentions(self, **kwargs): + """getUserMentions(**kwargs) + + Returns the 20 most recent mentions (status containing @username) for the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) @@ -183,8 +268,15 @@ class setup: raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getUserMentions() requires you to be authenticated.") - + def reTweet(self, id): + """reTweet(id) + + Retweets a tweet. Requires the id parameter of the tweet you are retweeting. + + Parameters: + id - Required. The numerical ID of the tweet you are retweeting. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) @@ -192,8 +284,18 @@ class setup: raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - + def retweetedOfMe(self, **kwargs): + """retweetedOfMe(**kwargs) + + Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) @@ -202,8 +304,18 @@ class setup: raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - + def retweetedByMe(self, **kwargs): + """retweetedByMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) @@ -212,8 +324,18 @@ class setup: raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - + def retweetedToMe(self, **kwargs): + """retweetedToMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user's friends. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) @@ -222,8 +344,25 @@ class setup: raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - + def showUser(self, id = None, user_id = None, screen_name = None): + """showUser(id = None, user_id = None, screen_name = None) + + Returns extended information of a given user. The author's most recent status will be returned inline. + + Parameters: + ** Note: One of the following must always be specified. + id - The ID or screen name of a user. + user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + + Usage Notes: + Requests for protected users without credentials from + 1) the user requested or + 2) a user that is following the protected user will omit the nested status element. + + ...will result in only publicly available data being returned. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -238,8 +377,23 @@ class setup: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("showUser() requires you to be authenticated.") - + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. + (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access + older friends. With no user specified, the request defaults to the authenticated users friends. + + It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + + Parameters: + ** Note: One of the following is required. (id, user_id, or screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of friends. + user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page of friends to receive. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -254,8 +408,23 @@ class setup: raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns the authenticating user's followers, each with current status inline. + They are ordered by the order in which they joined Twitter, 100 at a time. + (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) + + Use the page option to access earlier followers. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of followers. + user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page to retrieve. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -270,9 +439,17 @@ class setup: raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - + def showStatus(self, id): + """showStatus(id) + + Returns a single status, specified by the id parameter below. + The status's author will be returned inline. + + Parameters: + id - Required. The numerical ID of the status to retrieve. + """ try: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) @@ -281,16 +458,37 @@ class setup: except HTTPError, e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) - + def updateStatus(self, status, in_reply_to_status_id = None): + """updateStatus(status, in_reply_to_status_id = None) + + Updates the authenticating user's status. Requires the status parameter specified below. + A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. + + Parameters: + status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. + in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + + ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references + is mentioned within the status text. Therefore, you must include @username, where username is + the author of the referenced tweet, within the update. + """ if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - + def destroyStatus(self, id): + """destroyStatus(id) + + Destroys the status specified by the required ID parameter. + The authenticating user must be the author of the specified status. + + Parameters: + id - Required. The ID of the status to destroy. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) @@ -298,8 +496,13 @@ class setup: raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyStatus() requires you to be authenticated.") - + def endSession(self): + """endSession() + + Ends the session of the authenticating user, returning a null cookie. + Use this method to sign users out of client-facing applications (widgets, etc). + """ if self.authenticated is True: try: self.opener.open("http://twitter.com/account/end_session.json", "") @@ -308,8 +511,18 @@ class setup: raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You can't end a session when you're not authenticated to begin with.") - + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent to the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: apiURL = "http://twitter.com/direct_messages.json?page=%s" % `page` if since_id is not None: @@ -318,15 +531,25 @@ class setup: apiURL += "&max_id=%s" % `max_id` if count is not None: apiURL += "&count=%s" % `count` - + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getDirectMessages() requires you to be authenticated.") - + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getSentMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % `page` if since_id is not None: @@ -335,15 +558,24 @@ class setup: apiURL += "&max_id=%s" % `max_id` if count is not None: apiURL += "&count=%s" % `count` - + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getSentMessages() requires you to be authenticated.") - + def sendDirectMessage(self, user, text): + """sendDirectMessage(user, text) + + Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. + Returns the sent message in the requested format when successful. + + Parameters: + user - Required. The ID or screen name of the recipient user. + text - Required. The text of your direct message. Be sure to keep it under 140 characters. + """ if self.authenticated is True: if len(list(text)) < 140: try: @@ -354,8 +586,16 @@ class setup: raise TwythonError("Your message must not be longer than 140 characters") else: raise AuthError("You must be authenticated to send a new direct message.") - + def destroyDirectMessage(self, id): + """destroyDirectMessage(id) + + Destroys the direct message specified in the required ID parameter. + The authenticating user must be the recipient of the specified direct message. + + Parameters: + id - Required. The ID of the direct message to destroy. + """ if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") @@ -363,8 +603,22 @@ class setup: raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You must be authenticated to destroy a direct message.") - + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") + + Allows the authenticating users to follow the user specified in the ID parameter. + Returns the befriended user in the requested format when successful. Returns a + string describing the failure condition when unsuccessful. If you are already + friends with the user an HTTP 403 will be returned. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to befriend. + user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. + follow - Optional. Enable notifications for the target user in addition to becoming friends. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -382,8 +636,19 @@ class setup: raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createFriendship() requires you to be authenticated.") - + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + """destroyFriendship(id = None, user_id = None, screen_name = None) + + Allows the authenticating users to unfollow the user specified in the ID parameter. + Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to unfollow. + user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -398,8 +663,17 @@ class setup: raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyFriendship() requires you to be authenticated.") - + def checkIfFriendshipExists(self, user_a, user_b): + """checkIfFriendshipExists(user_a, user_b) + + Tests for the existence of friendship between two users. + Will return true if user_a follows user_b; otherwise, it'll return false. + + Parameters: + user_a - Required. The ID or screen_name of the subject user. + user_b - Required. The ID or screen_name of the user to test for following. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) @@ -407,8 +681,16 @@ class setup: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - + def updateDeliveryDevice(self, device_name = "none"): + """updateDeliveryDevice(device_name = "none") + + Sets which device Twitter delivers updates to for the authenticating user. + Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) + + Parameters: + device - Required. Must be one of: sms, im, none. + """ if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": self.unicode2utf8(device_name)})) @@ -416,8 +698,22 @@ class setup: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - + def updateProfileColors(self, **kwargs): + """updateProfileColors(**kwargs) + + Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + + Parameters: + ** Note: One or more of the following parameters must be present. Each parameter's value must + be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). + + profile_background_color - Optional. + profile_text_color - Optional. + profile_link_color - Optional. + profile_sidebar_fill_color - Optional. + profile_sidebar_border_color - Optional. + """ if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) @@ -425,8 +721,23 @@ class setup: raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + """updateProfile(name = None, email = None, url = None, location = None, description = None) + + Sets values that users are able to set under the "Account" tab of their settings page. + Only the parameters specified will be updated. + + Parameters: + One or more of the following parameters must be present. Each parameter's value + should be a string. See the individual parameter descriptions below for further constraints. + + name - Optional. Maximum of 20 characters. + email - Optional. Maximum of 40 characters. Must be a valid email address. + url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. + location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. + description - Optional. Maximum of 160 characters. + """ if self.authenticated is True: useAmpersands = False updateProfileQueryString = "" @@ -471,7 +782,7 @@ class setup: updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) else: raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - + if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) @@ -479,8 +790,15 @@ class setup: raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateProfile() requires you to be authenticated.") - + def getFavorites(self, page = "1"): + """getFavorites(page = "1") + + Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. + + Parameters: + page - Optional. Specifies the page of favorites to retrieve. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % `page`)) @@ -488,8 +806,15 @@ class setup: raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFavorites() requires you to be authenticated.") - + def createFavorite(self, id): + """createFavorite(id) + + Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. + + Parameters: + id - Required. The ID of the status to favorite. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % `id`, "")) @@ -497,8 +822,15 @@ class setup: raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createFavorite() requires you to be authenticated.") - + def destroyFavorite(self, id): + """destroyFavorite(id) + + Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. + + Parameters: + id - Required. The ID of the status to un-favorite. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % `id`, "")) @@ -506,8 +838,18 @@ class setup: raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyFavorite() requires you to be authenticated.") - + def notificationFollow(self, id = None, user_id = None, screen_name = None): + """notificationFollow(id = None, user_id = None, screen_name = None) + + Enables device notifications for updates from the specified user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -522,8 +864,18 @@ class setup: raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("notificationFollow() requires you to be authenticated.") - + def notificationLeave(self, id = None, user_id = None, screen_name = None): + """notificationLeave(id = None, user_id = None, screen_name = None) + + Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -538,8 +890,19 @@ class setup: raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("notificationLeave() requires you to be authenticated.") - + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user the specified user is following. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, `page`) @@ -551,8 +914,19 @@ class setup: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user following the specified user. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(`id`, `page`) @@ -564,8 +938,16 @@ class setup: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - + def createBlock(self, id): + """createBlock(id) + + Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. + Returns the blocked user in the requested format when successful. + + Parameters: + id - The ID or screen name of a user to block. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % `id`, "")) @@ -573,8 +955,16 @@ class setup: raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createBlock() requires you to be authenticated.") - + def destroyBlock(self, id): + """destroyBlock(id) + + Un-blocks the user specified in the ID parameter for the authenticating user. + Returns the un-blocked user in the requested format when successful. + + Parameters: + id - Required. The ID or screen_name of the user to un-block + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % `id`, "")) @@ -582,8 +972,19 @@ class setup: raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyBlock() requires you to be authenticated.") - + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + """checkIfBlockExists(id = None, user_id = None, screen_name = None) + + Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and + error with an HTTP 404 response code otherwise. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen_name of the potentially blocked user. + user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/blocks/exists/%s.json" % `id` @@ -595,8 +996,15 @@ class setup: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - + def getBlocking(self, page = "1"): + """getBlocking(page = "1") + + Returns an array of user objects that the authenticating user is blocking. + + Parameters: + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % `page`)) @@ -604,8 +1012,12 @@ class setup: raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getBlocking() requires you to be authenticated") - + def getBlockedIDs(self): + """getBlockedIDs() + + Returns an array of numeric user ids the authenticating user is blocking. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) @@ -613,15 +1025,47 @@ class setup: raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getBlockedIDs() requires you to be authenticated.") - + def searchTwitter(self, search_query, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. + lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. + locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. + rpp - Optional. The number of tweets to return per page, up to a max of 100. + page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) + since_id - Optional. Returns tweets with status ids greater than the given id. + geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. + show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. + + Usage Notes: + Queries are limited 140 URL encoded characters. + Some users may be absent from search results. + The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. + This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. + + Applications must have a meaningful and unique User Agent when using this method. + An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than + applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. + """ searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - + def getCurrentTrends(self, excludeHashTags = False): + """getCurrentTrends(excludeHashTags = False) + + Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used + on Twitter Search results page for that topic. + + Parameters: + excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/current.json" if excludeHashTags is True: apiURL += "?exclude=hashtags" @@ -629,8 +1073,16 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - + def getDailyTrends(self, date = None, exclude = False): + """getDailyTrends(date = None, exclude = False) + + Returns the top 20 trending topics for each hour in a given day. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: @@ -645,8 +1097,16 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - + def getWeeklyTrends(self, date = None, exclude = False): + """getWeeklyTrends(date = None, exclude = False) + + Returns the top 30 trending topics for each day in a given week. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: @@ -661,8 +1121,12 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - + def getSavedSearches(self): + """getSavedSearches() + + Returns the authenticated user's saved search queries. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) @@ -670,8 +1134,15 @@ class setup: raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getSavedSearches() requires you to be authenticated.") - + def showSavedSearch(self, id): + """showSavedSearch(id) + + Retrieve the data for a saved search owned by the authenticating user specified by the given id. + + Parameters: + id - Required. The id of the saved search to be retrieved. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % `id`)) @@ -679,8 +1150,15 @@ class setup: raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("showSavedSearch() requires you to be authenticated.") - + def createSavedSearch(self, query): + """createSavedSearch(query) + + Creates a saved search for the authenticated user. + + Parameters: + query - Required. The query of the search the user would like to save. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) @@ -688,8 +1166,16 @@ class setup: raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createSavedSearch() requires you to be authenticated.") - + def destroySavedSearch(self, id): + """destroySavedSearch(id) + + Destroys a saved search for the authenticated user. + The search specified by id must be owned by the authenticating user. + + Parameters: + id - Required. The id of the saved search to be deleted. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % `id`, "")) @@ -697,9 +1183,18 @@ class setup: raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") - + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): + """updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + """ if self.authenticated is True: try: files = [("image", filename, open(filename).read())] @@ -712,8 +1207,15 @@ class setup: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") - + def updateProfileImage(self, filename): + """updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ if self.authenticated is True: try: files = [("image", filename, open(filename).read())] @@ -726,7 +1228,7 @@ class setup: raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You realize you need to be authenticated to change a profile image, right?") - + def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() CRLF = '\r\n' @@ -747,10 +1249,10 @@ class setup: body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - + def get_content_type(self, filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - + def unicode2utf8(self, text): try: if isinstance(text, unicode): diff --git a/twython3k.py b/twython3k.py index 96ff348..baf3bea 100644 --- a/twython3k.py +++ b/twython3k.py @@ -16,7 +16,7 @@ from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.7a" +__version__ = "0.8" """Twython - Easy Twitter utilities in Python""" @@ -55,6 +55,18 @@ class AuthError(TwythonError): class setup: def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + authtype - "OAuth"/"Basic" + username - Your twitter username + password - Password for your twitter account. + consumer_secret - Consumer secret in case you specified for OAuth as authtype. + consumer_key - Consumer key in case you specified for OAuth as authtype. + headers - User agent header. + """ self.authtype = authtype self.authenticated = False self.username = username @@ -92,13 +104,13 @@ class setup: pass else: raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - + def getRequestToken(self): response = self.oauth_request(self.request_token_url) token = self.parseOAuthResponse(response) self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) return self.request_token - + def parseOAuthResponse(self, response_string): # Partial credit goes to Harper Reed for this gem. lol = {} @@ -108,18 +120,34 @@ class setup: break lol[pair[0]] = pair[1] return lol - + # URL Shortening function huzzah def shortenURL(self, 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 url shorterning service other that is.gd. + """ try: return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() except HTTPError as e: raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - + def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) - + def getRateLimitStatus(self, rate_for = "requestingIP"): + """getRateLimitStatus() + + Returns the remaining number of API requests available to the requesting user before the + API limit is reached for the current hour. Calls to rate_limit_status do not count against + the rate limit. If authentication credentials are provided, the rate limit status for the + authenticating user is returned. Otherwise, the rate limit status for the requesting + IP address is returned. + """ try: if rate_for == "requestingIP": return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) @@ -130,14 +158,35 @@ class setup: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) - + def getPublicTimeline(self): + """getPublicTimeline() + + Returns the 20 most recent statuses from non-protected users who have set a custom user icon. + The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + """ try: return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) except HTTPError as e: raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) - + def getHomeTimeline(self, **kwargs): + """getHomeTimeline(**kwargs) + + Returns the 20 most recent statuses, including retweets, posted by the authenticating user + and that user's friends. This is the equivalent of /timeline/home on the Web. + + Usage note: This home_timeline is identical to statuses/friends_timeline, except it also + contains retweets, which statuses/friends_timeline does not (for backwards compatibility + reasons). In a future version of the API, statuses/friends_timeline will go away and + be replaced by home_timeline. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) @@ -146,8 +195,19 @@ class setup: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) else: raise AuthError("getHomeTimeline() requires you to be authenticated.") - + def getFriendsTimeline(self, **kwargs): + """getFriendsTimeline(**kwargs) + + Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. + This is the equivalent of /timeline/home on the Web. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) @@ -156,8 +216,23 @@ class setup: raise TwythonError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) else: raise AuthError("getFriendsTimeline() requires you to be authenticated.") - + def getUserTimeline(self, id = None, **kwargs): + """getUserTimeline(id = None, **kwargs) + + Returns the 20 most recent statuses posted from the authenticating user. It's also + possible to request another user's timeline via the id parameter. This is the + equivalent of the Web / page for your own user, or the profile page for a third party. + + Parameters: + id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. + user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. + screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % repr(id), kwargs) elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: @@ -173,8 +248,18 @@ class setup: except HTTPError as e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % repr(e.code), e.code) - + def getUserMentions(self, **kwargs): + """getUserMentions(**kwargs) + + Returns the 20 most recent mentions (status containing @username) for the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) @@ -183,8 +268,15 @@ class setup: raise TwythonError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getUserMentions() requires you to be authenticated.") - + def reTweet(self, id): + """reTweet(id) + + Retweets a tweet. Requires the id parameter of the tweet you are retweeting. + + Parameters: + id - Required. The numerical ID of the tweet you are retweeting. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) @@ -192,8 +284,18 @@ class setup: raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - + def retweetedOfMe(self, **kwargs): + """retweetedOfMe(**kwargs) + + Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) @@ -202,8 +304,18 @@ class setup: raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - + def retweetedByMe(self, **kwargs): + """retweetedByMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) @@ -212,8 +324,18 @@ class setup: raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - + def retweetedToMe(self, **kwargs): + """retweetedToMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user's friends. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: try: retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) @@ -222,8 +344,25 @@ class setup: raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - + def showUser(self, id = None, user_id = None, screen_name = None): + """showUser(id = None, user_id = None, screen_name = None) + + Returns extended information of a given user. The author's most recent status will be returned inline. + + Parameters: + ** Note: One of the following must always be specified. + id - The ID or screen name of a user. + user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + + Usage Notes: + Requests for protected users without credentials from + 1) the user requested or + 2) a user that is following the protected user will omit the nested status element. + + ...will result in only publicly available data being returned. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -238,8 +377,23 @@ class setup: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("showUser() requires you to be authenticated.") - + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. + (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access + older friends. With no user specified, the request defaults to the authenticated users friends. + + It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + + Parameters: + ** Note: One of the following is required. (id, user_id, or screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of friends. + user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page of friends to receive. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -254,8 +408,23 @@ class setup: raise TwythonError("getFriendsStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns the authenticating user's followers, each with current status inline. + They are ordered by the order in which they joined Twitter, 100 at a time. + (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) + + Use the page option to access earlier followers. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of followers. + user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page to retrieve. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -270,9 +439,17 @@ class setup: raise TwythonError("getFollowersStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - + def showStatus(self, id): + """showStatus(id) + + Returns a single status, specified by the id parameter below. + The status's author will be returned inline. + + Parameters: + id - Required. The numerical ID of the status to retrieve. + """ try: if self.authenticated is True: return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) @@ -281,16 +458,37 @@ class setup: except HTTPError as e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) - + def updateStatus(self, status, in_reply_to_status_id = None): + """updateStatus(status, in_reply_to_status_id = None) + + Updates the authenticating user's status. Requires the status parameter specified below. + A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. + + Parameters: + status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. + in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + + ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references + is mentioned within the status text. Therefore, you must include @username, where username is + the author of the referenced tweet, within the update. + """ if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) - + def destroyStatus(self, id): + """destroyStatus(id) + + Destroys the status specified by the required ID parameter. + The authenticating user must be the author of the specified status. + + Parameters: + id - Required. The ID of the status to destroy. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) @@ -298,8 +496,13 @@ class setup: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyStatus() requires you to be authenticated.") - + def endSession(self): + """endSession() + + Ends the session of the authenticating user, returning a null cookie. + Use this method to sign users out of client-facing applications (widgets, etc). + """ if self.authenticated is True: try: self.opener.open("http://twitter.com/account/end_session.json", "") @@ -308,8 +511,18 @@ class setup: raise TwythonError("endSession failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You can't end a session when you're not authenticated to begin with.") - + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent to the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: apiURL = "http://twitter.com/direct_messages.json?page=%s" % repr(page) if since_id is not None: @@ -318,15 +531,25 @@ class setup: apiURL += "&max_id=%s" % repr(max_id) if count is not None: apiURL += "&count=%s" % repr(count) - + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getDirectMessages() requires you to be authenticated.") - + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getSentMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ if self.authenticated is True: apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % repr(page) if since_id is not None: @@ -335,15 +558,24 @@ class setup: apiURL += "&max_id=%s" % repr(max_id) if count is not None: apiURL += "&count=%s" % repr(count) - + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getSentMessages() requires you to be authenticated.") - + def sendDirectMessage(self, user, text): + """sendDirectMessage(user, text) + + Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. + Returns the sent message in the requested format when successful. + + Parameters: + user - Required. The ID or screen name of the recipient user. + text - Required. The text of your direct message. Be sure to keep it under 140 characters. + """ if self.authenticated is True: if len(list(text)) < 140: try: @@ -354,8 +586,16 @@ class setup: raise TwythonError("Your message must not be longer than 140 characters") else: raise AuthError("You must be authenticated to send a new direct message.") - + def destroyDirectMessage(self, id): + """destroyDirectMessage(id) + + Destroys the direct message specified in the required ID parameter. + The authenticating user must be the recipient of the specified direct message. + + Parameters: + id - Required. The ID of the direct message to destroy. + """ if self.authenticated is True: try: return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") @@ -363,8 +603,22 @@ class setup: raise TwythonError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You must be authenticated to destroy a direct message.") - + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") + + Allows the authenticating users to follow the user specified in the ID parameter. + Returns the befriended user in the requested format when successful. Returns a + string describing the failure condition when unsuccessful. If you are already + friends with the user an HTTP 403 will be returned. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to befriend. + user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. + follow - Optional. Enable notifications for the target user in addition to becoming friends. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -382,8 +636,19 @@ class setup: raise TwythonError("createFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createFriendship() requires you to be authenticated.") - + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + """destroyFriendship(id = None, user_id = None, screen_name = None) + + Allows the authenticating users to unfollow the user specified in the ID parameter. + Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to unfollow. + user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -398,8 +663,17 @@ class setup: raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyFriendship() requires you to be authenticated.") - + def checkIfFriendshipExists(self, user_a, user_b): + """checkIfFriendshipExists(user_a, user_b) + + Tests for the existence of friendship between two users. + Will return true if user_a follows user_b; otherwise, it'll return false. + + Parameters: + user_a - Required. The ID or screen_name of the subject user. + user_b - Required. The ID or screen_name of the user to test for following. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) @@ -407,8 +681,16 @@ class setup: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - + def updateDeliveryDevice(self, device_name = "none"): + """updateDeliveryDevice(device_name = "none") + + Sets which device Twitter delivers updates to for the authenticating user. + Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) + + Parameters: + device - Required. Must be one of: sms, im, none. + """ if self.authenticated is True: try: return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": self.unicode2utf8(device_name)})) @@ -416,8 +698,22 @@ class setup: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - + def updateProfileColors(self, **kwargs): + """updateProfileColors(**kwargs) + + Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + + Parameters: + ** Note: One or more of the following parameters must be present. Each parameter's value must + be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). + + profile_background_color - Optional. + profile_text_color - Optional. + profile_link_color - Optional. + profile_sidebar_fill_color - Optional. + profile_sidebar_border_color - Optional. + """ if self.authenticated is True: try: return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) @@ -425,8 +721,23 @@ class setup: raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + """updateProfile(name = None, email = None, url = None, location = None, description = None) + + Sets values that users are able to set under the "Account" tab of their settings page. + Only the parameters specified will be updated. + + Parameters: + One or more of the following parameters must be present. Each parameter's value + should be a string. See the individual parameter descriptions below for further constraints. + + name - Optional. Maximum of 20 characters. + email - Optional. Maximum of 40 characters. Must be a valid email address. + url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. + location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. + description - Optional. Maximum of 160 characters. + """ if self.authenticated is True: useAmpersands = False updateProfileQueryString = "" @@ -471,7 +782,7 @@ class setup: updateProfileQueryString += urllib.parse.urlencode({"description": self.unicode2utf8(description)}) else: raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - + if updateProfileQueryString != "": try: return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) @@ -479,8 +790,15 @@ class setup: raise TwythonError("updateProfile() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateProfile() requires you to be authenticated.") - + def getFavorites(self, page = "1"): + """getFavorites(page = "1") + + Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. + + Parameters: + page - Optional. Specifies the page of favorites to retrieve. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % repr(page))) @@ -488,8 +806,15 @@ class setup: raise TwythonError("getFavorites() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFavorites() requires you to be authenticated.") - + def createFavorite(self, id): + """createFavorite(id) + + Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. + + Parameters: + id - Required. The ID of the status to favorite. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % repr(id), "")) @@ -497,8 +822,15 @@ class setup: raise TwythonError("createFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createFavorite() requires you to be authenticated.") - + def destroyFavorite(self, id): + """destroyFavorite(id) + + Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. + + Parameters: + id - Required. The ID of the status to un-favorite. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % repr(id), "")) @@ -506,8 +838,18 @@ class setup: raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyFavorite() requires you to be authenticated.") - + def notificationFollow(self, id = None, user_id = None, screen_name = None): + """notificationFollow(id = None, user_id = None, screen_name = None) + + Enables device notifications for updates from the specified user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -522,8 +864,18 @@ class setup: raise TwythonError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("notificationFollow() requires you to be authenticated.") - + def notificationLeave(self, id = None, user_id = None, screen_name = None): + """notificationLeave(id = None, user_id = None, screen_name = None) + + Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ if self.authenticated is True: apiURL = "" if id is not None: @@ -538,8 +890,19 @@ class setup: raise TwythonError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("notificationLeave() requires you to be authenticated.") - + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user the specified user is following. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, repr(page)) @@ -551,8 +914,19 @@ class setup: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) - + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user following the specified user. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(repr(id), repr(page)) @@ -564,8 +938,16 @@ class setup: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) - + def createBlock(self, id): + """createBlock(id) + + Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. + Returns the blocked user in the requested format when successful. + + Parameters: + id - The ID or screen name of a user to block. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % repr(id), "")) @@ -573,8 +955,16 @@ class setup: raise TwythonError("createBlock() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createBlock() requires you to be authenticated.") - + def destroyBlock(self, id): + """destroyBlock(id) + + Un-blocks the user specified in the ID parameter for the authenticating user. + Returns the un-blocked user in the requested format when successful. + + Parameters: + id - Required. The ID or screen_name of the user to un-block + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % repr(id), "")) @@ -582,8 +972,19 @@ class setup: raise TwythonError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyBlock() requires you to be authenticated.") - + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + """checkIfBlockExists(id = None, user_id = None, screen_name = None) + + Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and + error with an HTTP 404 response code otherwise. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen_name of the potentially blocked user. + user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + """ apiURL = "" if id is not None: apiURL = "http://twitter.com/blocks/exists/%s.json" % repr(id) @@ -595,8 +996,15 @@ class setup: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) - + def getBlocking(self, page = "1"): + """getBlocking(page = "1") + + Returns an array of user objects that the authenticating user is blocking. + + Parameters: + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % repr(page))) @@ -604,8 +1012,12 @@ class setup: raise TwythonError("getBlocking() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getBlocking() requires you to be authenticated") - + def getBlockedIDs(self): + """getBlockedIDs() + + Returns an array of numeric user ids the authenticating user is blocking. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) @@ -613,15 +1025,47 @@ class setup: raise TwythonError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getBlockedIDs() requires you to be authenticated.") - + def searchTwitter(self, search_query, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. + lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. + locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. + rpp - Optional. The number of tweets to return per page, up to a max of 100. + page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) + since_id - Optional. Returns tweets with status ids greater than the given id. + geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. + show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. + + Usage Notes: + Queries are limited 140 URL encoded characters. + Some users may be absent from search results. + The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. + This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. + + Applications must have a meaningful and unique User Agent when using this method. + An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than + applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. + """ searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) try: return simplejson.load(urllib.request.urlopen(searchURL)) except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - + def getCurrentTrends(self, excludeHashTags = False): + """getCurrentTrends(excludeHashTags = False) + + Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used + on Twitter Search results page for that topic. + + Parameters: + excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/current.json" if excludeHashTags is True: apiURL += "?exclude=hashtags" @@ -629,8 +1073,16 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) - + def getDailyTrends(self, date = None, exclude = False): + """getDailyTrends(date = None, exclude = False) + + Returns the top 20 trending topics for each hour in a given day. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: @@ -645,8 +1097,16 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) - + def getWeeklyTrends(self, date = None, exclude = False): + """getWeeklyTrends(date = None, exclude = False) + + Returns the top 30 trending topics for each day in a given week. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: @@ -661,8 +1121,12 @@ class setup: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) - + def getSavedSearches(self): + """getSavedSearches() + + Returns the authenticated user's saved search queries. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) @@ -670,8 +1134,15 @@ class setup: raise TwythonError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getSavedSearches() requires you to be authenticated.") - + def showSavedSearch(self, id): + """showSavedSearch(id) + + Retrieve the data for a saved search owned by the authenticating user specified by the given id. + + Parameters: + id - Required. The id of the saved search to be retrieved. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % repr(id))) @@ -679,8 +1150,15 @@ class setup: raise TwythonError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("showSavedSearch() requires you to be authenticated.") - + def createSavedSearch(self, query): + """createSavedSearch(query) + + Creates a saved search for the authenticated user. + + Parameters: + query - Required. The query of the search the user would like to save. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) @@ -688,8 +1166,16 @@ class setup: raise TwythonError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createSavedSearch() requires you to be authenticated.") - + def destroySavedSearch(self, id): + """destroySavedSearch(id) + + Destroys a saved search for the authenticated user. + The search specified by id must be owned by the authenticating user. + + Parameters: + id - Required. The id of the saved search to be deleted. + """ if self.authenticated is True: try: return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % repr(id), "")) @@ -697,9 +1183,18 @@ class setup: raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") - + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. def updateProfileBackgroundImage(self, filename, tile="true"): + """updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + """ if self.authenticated is True: try: files = [("image", filename, open(filename).read())] @@ -712,8 +1207,15 @@ class setup: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") - + def updateProfileImage(self, filename): + """updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ if self.authenticated is True: try: files = [("image", filename, open(filename).read())] @@ -726,7 +1228,7 @@ class setup: raise TwythonError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You realize you need to be authenticated to change a profile image, right?") - + def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() CRLF = '\r\n' @@ -747,10 +1249,10 @@ class setup: body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - + def get_content_type(self, filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - + def unicode2utf8(self, text): try: if isinstance(text, str): From bbfd874643f58ea691e050f07d49b78d0297f07c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 28 Aug 2009 02:53:48 -0400 Subject: [PATCH 102/687] Twython 0.8 release. Docstrings, RT API, and a host of other bugfixes. Next, and hopefully final, large part to tackle is OAuth. Getting closer... --- dist/twython-0.8.macosx-10.5-i386.tar.gz | Bin 0 -> 35001 bytes dist/twython-0.8.tar.gz | Bin 0 -> 12644 bytes dist/twython-0.8.win32.exe | Bin 0 -> 81487 bytes setup.py | 2 +- twython.egg-info/PKG-INFO | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 dist/twython-0.8.macosx-10.5-i386.tar.gz create mode 100644 dist/twython-0.8.tar.gz create mode 100644 dist/twython-0.8.win32.exe diff --git a/dist/twython-0.8.macosx-10.5-i386.tar.gz b/dist/twython-0.8.macosx-10.5-i386.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..96743b196539dba2c15a50fb8162d7973c2b01a4 GIT binary patch literal 35001 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 O@BarJ@vu$+AOirE?qPNS literal 0 HcmV?d00001 diff --git a/dist/twython-0.8.tar.gz b/dist/twython-0.8.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..ab90687a29a548f0114f54f940c764bc8fbc9301 GIT binary patch literal 12644 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 literal 0 HcmV?d00001 diff --git a/dist/twython-0.8.win32.exe b/dist/twython-0.8.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..57f21944493abc5fd9dd6a6bcc18d11b34761789 GIT binary patch literal 81487 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

FxT$O>sVRO6w?qs?vxN~ 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 bZvP(xRY?{a=1l<(^vwhX3~&4^4f+28^F0~- literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 038ab6a..8747b12 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '0.6' +__version__ = '0.8' # For the love of god, use Pip to install this. diff --git a/twython.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO index 9d7210c..ade4232 100644 --- a/twython.egg-info/PKG-INFO +++ b/twython.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: twython -Version: 0.6 +Version: 0.8 Summary: A new and easy way to access Twitter data with Python. Home-page: http://github.com/ryanmcgrath/twython/tree/master Author: Ryan McGrath From 0934d7146ab340b3b039caa9688d8d68870ef490 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 29 Aug 2009 01:30:27 -0400 Subject: [PATCH 103/687] Removing the ugly authtype parameter requirement from .setup() - now, by passing username and password, it automatically defaults to Basic (HTTP) authentication. In the future, providing consumer key/secrets will automatically tender OAuth login. As always, .setup() by itself remains a login-less method to pull down Twitter data (search, etc). This'll be included in the 0.9 release; 0.8 users still need to specify authtype=Basic in their .setup() calls if they want Basic Auth. --- twython.py | 56 +++++++++++++++++++++++--------------------------- twython3k.py | 58 ++++++++++++++++++++++++---------------------------- 2 files changed, 53 insertions(+), 61 deletions(-) diff --git a/twython.py b/twython.py index 423a2ca..6cff785 100644 --- a/twython.py +++ b/twython.py @@ -60,14 +60,12 @@ class setup: Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). Parameters: - authtype - "OAuth"/"Basic" - username - Your twitter username - password - Password for your twitter account. - consumer_secret - Consumer secret in case you specified for OAuth as authtype. - consumer_key - Consumer key in case you specified for OAuth as authtype. + username - Your Twitter username, if you want Basic (HTTP) Authentication. + password - Password for your twitter account, if you want Basic (HTTP) Authentication. + consumer_secret - Consumer secret, if you want OAuth. + consumer_key - Consumer key, if you want OAuth. headers - User agent header. """ - self.authtype = authtype self.authenticated = False self.username = username # OAuth specific variables below @@ -81,30 +79,28 @@ class setup: self.access_token = None # Check and set up authentication if self.username is not None and password is not None: - if self.authtype == "Basic": - # Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - else: - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - if consumer_secret is not None and consumer_key is not None: - #req = oauth.OAuthRequest.from_consumer_and_token - #req.sign_request(self.signature_method, self.consumer_key, self.token) - #self.opener = urllib2.build_opener() - pass - else: - raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - + # Assume Basic authentication ritual + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + elif consumer_secret is not None and consumer_key is not None: + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + # Awesome OAuth authentication ritual + # req = oauth.OAuthRequest.from_consumer_and_token + # req.sign_request(self.signature_method, self.consumer_key, self.token) + # self.opener = urllib2.build_opener() + pass + else: + pass + def getRequestToken(self): response = self.oauth_request(self.request_token_url) token = self.parseOAuthResponse(response) diff --git a/twython3k.py b/twython3k.py index baf3bea..07820fe 100644 --- a/twython3k.py +++ b/twython3k.py @@ -54,20 +54,18 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + def __init__(self, username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). Parameters: - authtype - "OAuth"/"Basic" - username - Your twitter username - password - Password for your twitter account. - consumer_secret - Consumer secret in case you specified for OAuth as authtype. - consumer_key - Consumer key in case you specified for OAuth as authtype. + username - Your twitter username, if you want Basic (HTTP) Authentication + password - Password for your twitter account, if you want Basic (HTTP) Authentication + consumer_secret - Consumer secret, if you want OAuth. + consumer_key - Consumer key, if you want OAuth. headers - User agent header. """ - self.authtype = authtype self.authenticated = False self.username = username # OAuth specific variables below @@ -81,30 +79,28 @@ class setup: self.access_token = None # Check and set up authentication if self.username is not None and password is not None: - if self.authtype == "Basic": - # Basic authentication ritual - self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, password) - self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib.request.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError as e: - raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) - else: - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - if consumer_secret is not None and consumer_key is not None: - #req = oauth.OAuthRequest.from_consumer_and_token - #req.sign_request(self.signature_method, self.consumer_key, self.token) - #self.opener = urllib2.build_opener() - pass - else: - raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - + # Basic authentication ritual + self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, password) + self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib.request.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError as e: + raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + elif consumer_secret is not None and consumer_key is not None: + self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() + # Awesome OAuth authentication ritual + # req = oauth.OAuthRequest.from_consumer_and_token + # req.sign_request(self.signature_method, self.consumer_key, self.token) + # self.opener = urllib2.build_opener() + pass + else: + pass + def getRequestToken(self): response = self.oauth_request(self.request_token_url) token = self.parseOAuthResponse(response) From 80bc6f9fd0908b6b3f660da57e122ed900de9f61 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 31 Aug 2009 02:15:57 -0400 Subject: [PATCH 104/687] Fixing id error in .destroyStatus(). For some reason, this method is still returning consistent 400 HTTP response codes, but I get the feeling this is moreso a bug with Twitter than with Twython. The request works, and deletes whatever status was specified, but there's no proper values returned from Twitter that are in line with their specs. --- twython.py | 2 +- twython3k.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/twython.py b/twython.py index 6cff785..15c4a0b 100644 --- a/twython.py +++ b/twython.py @@ -487,7 +487,7 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "DELETE")) except HTTPError, e: raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: diff --git a/twython3k.py b/twython3k.py index 07820fe..338effd 100644 --- a/twython3k.py +++ b/twython3k.py @@ -487,7 +487,7 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "POST")) except HTTPError as e: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: From 8f975506d5dba5bb836265837405796431c047f6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 1 Sep 2009 02:11:46 -0400 Subject: [PATCH 105/687] checkIfFriendshipExists() was previously throwing a POST, when it should've been doing a GET request. Up until this point, it would have returned HTTP 400 errors on all calls - fixed now (both Twython2k and Twython3k). Thanks to Risto Haukioja for pointing this out to me. ;) --- twython.py | 5 +++-- twython3k.py | 15 ++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/twython.py b/twython.py index 15c4a0b..3ef51ba 100644 --- a/twython.py +++ b/twython.py @@ -275,7 +275,7 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % `id`, "POST")) except HTTPError, e: raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) else: @@ -672,7 +672,8 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.urlencode({"user_a": user_a, "user_b": user_b}) + return simplejson.load(self.opener.open(friendshipURL)) except HTTPError, e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: diff --git a/twython3k.py b/twython3k.py index 338effd..e51ab6b 100644 --- a/twython3k.py +++ b/twython3k.py @@ -54,14 +54,14 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). Parameters: - username - Your twitter username, if you want Basic (HTTP) Authentication - password - Password for your twitter account, if you want Basic (HTTP) Authentication + username - Your Twitter username, if you want Basic (HTTP) Authentication. + password - Password for your twitter account, if you want Basic (HTTP) Authentication. consumer_secret - Consumer secret, if you want OAuth. consumer_key - Consumer key, if you want OAuth. headers - User agent header. @@ -79,7 +79,7 @@ class setup: self.access_token = None # Check and set up authentication if self.username is not None and password is not None: - # Basic authentication ritual + # Assume Basic authentication ritual self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://twitter.com", self.username, password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) @@ -275,7 +275,7 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json", "POST" % id)) + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % repr(id), "POST")) except HTTPError as e: raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) else: @@ -487,7 +487,7 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "POST")) + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % repr(id), "DELETE")) except HTTPError as e: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: @@ -672,7 +672,8 @@ class setup: """ if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}))) + friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}) + return simplejson.load(self.opener.open(friendshipURL)) except HTTPError as e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: From 0331eb0612d4ac1786c84a7189a2146e847ca2c3 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 2 Sep 2009 01:16:20 -0400 Subject: [PATCH 106/687] Added a showFriendship() method that works with Twitter's API, and fixed the way createFriendship() and destroyFriendship() were making requests (was making GET requests in both instances, requires POST - go figure). Thanks to @tetsunosuke for spotting these and bringing them to my attention. ;) --- twython.py | 58 ++++++++++++++++++++++++++++++++++++++++++---------- twython3k.py | 58 ++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/twython.py b/twython.py index 3ef51ba..8315a41 100644 --- a/twython.py +++ b/twython.py @@ -617,18 +617,19 @@ class setup: """ if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/%s.json?follow=%s" %(id, follow) if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=%s&follow=%s" %(`user_id`, follow) + apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=%s&follow=%s" %(screen_name, follow) + apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: - return simplejson.load(self.opener.open(apiURL)) + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % `id`, "?folow=%s" % follow)) + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) except HTTPError, e: # Rate limiting is done differently here for API reasons... if e.code == 403: - raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") + raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createFriendship() requires you to be authenticated.") @@ -647,14 +648,15 @@ class setup: """ if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=%s" % `user_id` + apiURL = "?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=%s" % screen_name + apiURL = "?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL)) + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % `id`, "lol=1")) # Random string appended for POST reasons, quick hack ;P + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) except HTTPError, e: raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: @@ -678,7 +680,41 @@ class setup: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): + """showFriendship(source_id, source_screen_name, target_id, target_screen_name) + Returns detailed information about the relationship between two users. + + Parameters: + ** Note: One of the following is required if the request is unauthenticated + source_id - The user_id of the subject user. + source_screen_name - The screen_name of the subject user. + + ** Note: One of the following is required at all times + target_id - The user_id of the target user. + target_screen_name - The screen_name of the target user. + """ + apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D + if source_id is not None: + apiURL += "&source_id=%s" % `source_id` + if source_screen_name is not None: + apiURL += "&source_screen_name=%s" % source_screen_name + if target_id is not None: + apiURL += "&target_id=%s" % `target_id` + if target_screen_name is not None: + apiURL += "&target_screen_name=%s" % target_screen_name + try: + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + # Catch this for now + if e.code == 403: + raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") + raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) + def updateDeliveryDevice(self, device_name = "none"): """updateDeliveryDevice(device_name = "none") diff --git a/twython3k.py b/twython3k.py index e51ab6b..6c20250 100644 --- a/twython3k.py +++ b/twython3k.py @@ -617,18 +617,19 @@ class setup: """ if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/%s.json?follow=%s" %(id, follow) if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=%s&follow=%s" %(repr(user_id), follow) + apiURL = "?user_id=%s&follow=%s" %(repr(user_id), follow) if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=%s&follow=%s" %(screen_name, follow) + apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: - return simplejson.load(self.opener.open(apiURL)) + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % repr(id), "?folow=%s" % follow)) + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) except HTTPError as e: # Rate limiting is done differently here for API reasons... if e.code == 403: - raise TwythonError("You've hit the update limit for this method. Try again in 24 hours.") + raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") raise TwythonError("createFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createFriendship() requires you to be authenticated.") @@ -647,14 +648,15 @@ class setup: """ if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/%s.json" % id if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=%s" % repr(user_id) + apiURL = "?user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=%s" % screen_name + apiURL = "?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL)) + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % repr(id), "lol=1")) # Random string appended for POST reasons, quick hack ;P + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) except HTTPError as e: raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: @@ -678,7 +680,41 @@ class setup: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): + """showFriendship(source_id, source_screen_name, target_id, target_screen_name) + Returns detailed information about the relationship between two users. + + Parameters: + ** Note: One of the following is required if the request is unauthenticated + source_id - The user_id of the subject user. + source_screen_name - The screen_name of the subject user. + + ** Note: One of the following is required at all times + target_id - The user_id of the target user. + target_screen_name - The screen_name of the target user. + """ + apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D + if source_id is not None: + apiURL += "&source_id=%s" % repr(source_id) + if source_screen_name is not None: + apiURL += "&source_screen_name=%s" % source_screen_name + if target_id is not None: + apiURL += "&target_id=%s" % repr(target_id) + if target_screen_name is not None: + apiURL += "&target_screen_name=%s" % target_screen_name + try: + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib.request.urlopen(apiURL)) + except HTTPError as e: + # Catch this for now + if e.code == 403: + raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") + raise TwythonError("showFriendship() failed with a %s error code." % repr(e.code), e.code) + def updateDeliveryDevice(self, device_name = "none"): """updateDeliveryDevice(device_name = "none") From f7df3c3baeeb54b8002e1218452903ba286b2ace Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 4 Sep 2009 00:16:52 -0400 Subject: [PATCH 107/687] Some very experimental, probably not working yet, OAuth related pieces. Heavily inspired by henriklied's 'django-twitter-oauth' work. --- twython.py | 60 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/twython.py b/twython.py index 8315a41..360b60a 100644 --- a/twython.py +++ b/twython.py @@ -54,7 +54,7 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -64,6 +64,7 @@ class setup: password - Password for your twitter account, if you want Basic (HTTP) Authentication. consumer_secret - Consumer secret, if you want OAuth. consumer_key - Consumer key, if you want OAuth. + signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. """ self.authenticated = False @@ -77,6 +78,9 @@ class setup: self.consumer_secret = consumer_secret self.request_token = None self.access_token = None + self.consumer = None + self.connection = None + self.signature_method = None # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual @@ -92,31 +96,49 @@ class setup: except HTTPError, e: raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) elif consumer_secret is not None and consumer_key is not None: + self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) + self.connection = httplib.HTTPSConnection(SERVER) self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - # req = oauth.OAuthRequest.from_consumer_and_token - # req.sign_request(self.signature_method, self.consumer_key, self.token) - # self.opener = urllib2.build_opener() pass else: pass - def getRequestToken(self): - response = self.oauth_request(self.request_token_url) - token = self.parseOAuthResponse(response) - self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) - return self.request_token + def getOAuthResource(self, url, access_token, params, http_method="GET"): + """getOAuthResource(self, url, access_token, params, http_method="GET") - def parseOAuthResponse(self, response_string): - # Partial credit goes to Harper Reed for this gem. - lol = {} - for param in response_string.split("&"): - pair = param.split("=") - if(len(pair) != 2): - break - lol[pair[0]] = pair[1] - return lol + Returns a signed OAuth object for use in requests. + """ + newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) + oauth_request.sign_request(self.signature_method, consumer, access_token) + return oauth_request + + def getResponse(self, oauth_request, connection): + """getResponse(self, oauth_request, connection) + Returns a JSON-ified list of results. + """ + url = oauth_request.to_url() + connection.request(oauth_request.http_method, url) + response = connection.getresponse() + return simplejson.load(response.read()) + + def getUnauthorisedRequestToken(self, consumer, connection, signature_method=self.signature_method): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) + oauth_request.sign_request(signature_method, consumer, None) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + + def getAuthorizationURL(self, consumer, token, signature_method=self.signature_method): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) + oauth_request.sign_request(signature_method, consumer, token) + return oauth_request.to_url() + + def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token=self.request_token, signature_method=self.signature_method): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=request_token, http_url=self.access_token_url) + oauth_request.sign_request(signature_method, consumer, request_token) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): """shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl") From b54782c744c63a9cd4d221b08030c9d9935a62e4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 4 Sep 2009 00:21:00 -0400 Subject: [PATCH 108/687] Including a version (1.0) of oauth.py for Twython experiments. Override at your own discretion/risk. :D --- oauth.py | 524 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 oauth.py diff --git a/oauth.py b/oauth.py new file mode 100644 index 0000000..4bc47f5 --- /dev/null +++ b/oauth.py @@ -0,0 +1,524 @@ +import cgi +import urllib +import time +import random +import urlparse +import hmac +import binascii + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured.'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='~') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + from_string = staticmethod(from_string) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + if k[:6] == 'oauth_': + auth_header += ', %s="%s"' % (k, escape(str(v))) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): + # combine multiple parameter sources + if parameters is None: + parameters = {} + + # headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # check that the authorization header is OAuth + if auth_header.index('OAuth') > -1: + try: + # get the parameters from the header + header_params = OAuthRequest._split_header(auth_header) + parameters.update(header_params) + except: + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') + + # GET or POST query string + if query_string: + query_params = OAuthRequest._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters + param_str = urlparse.urlparse(http_url)[4] # query + url_params = OAuthRequest._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + return None + from_request = staticmethod(from_request) + + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + from_consumer_and_token = staticmethod(from_consumer_and_token) + + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return OAuthRequest(http_method, http_url, parameters) + from_token_and_callback = staticmethod(from_token_and_callback) + + # util function: turn Authorization: header into parameters, has to do some unescaping + def _split_header(header): + params = {} + parts = header.split(',') + for param in parts: + # ignore realm parameter + if param.find('OAuth realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + _split_header = staticmethod(_split_header) + + # util function: turn url string into parameters, has to do some unescaping + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + _split_url_string = staticmethod(_split_url_string) + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except OAuthError: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token, user): + return self.data_store.authorize_request_token(token, user) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported.' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key.') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer.') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + token = self.data_store.lookup_token(token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature.') + # validate the signature + valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) + if not valid_sig: + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) + raise OAuthError('Invalid signature. Expected signature base string: %s' % base) + built = signature_method.build_signature(oauth_request, consumer, token) + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + nonce = self.data_store.lookup_nonce(consumer, token, nonce) + if nonce: + raise OAuthError('Nonce already used: %s' % str(nonce)) + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token, user): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(self): + # -> str + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): + # -> str key, str raw + raise NotImplementedError + + def build_signature(self, oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + + def check_signature(self, oauth_request, consumer, token, signature): + built = self.build_signature(oauth_request, consumer, token) + return built == signature + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + # build the base signature string + key, raw = self.build_signature_base_string(oauth_request, consumer, token) + + # hmac object + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except: + import sha # deprecated + hashed = hmac.new(key, raw, sha) + + # calculate the digest base 64 + return binascii.b2a_base64(hashed.digest())[:-1] + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature_base_string(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + '&' + if token: + sig = sig + escape(token.secret) + return sig + + def build_signature(self, oauth_request, consumer, token): + return self.build_signature_base_string(oauth_request, consumer, token) \ No newline at end of file From 3369f7d81df788f9a01072d7efe29e9de443d548 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 4 Sep 2009 00:33:57 -0400 Subject: [PATCH 109/687] Fixed import errors with Twython, cleaned up some OAuth self.references junk, and fixed showUser to make requests even if there's no authentication being done (thanks to Risto for tipping me off to the last one ;) --- twython.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/twython.py b/twython.py index 360b60a..5c4a6b8 100644 --- a/twython.py +++ b/twython.py @@ -98,7 +98,6 @@ class setup: elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) self.connection = httplib.HTTPSConnection(SERVER) - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() pass else: pass @@ -122,19 +121,21 @@ class setup: response = connection.getresponse() return simplejson.load(response.read()) - def getUnauthorisedRequestToken(self, consumer, connection, signature_method=self.signature_method): + def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) oauth_request.sign_request(signature_method, consumer, None) resp = fetch_response(oauth_request, connection) return oauth.OAuthToken.from_string(resp) - def getAuthorizationURL(self, consumer, token, signature_method=self.signature_method): + def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) oauth_request.sign_request(signature_method, consumer, token) return oauth_request.to_url() - def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token=self.request_token, signature_method=self.signature_method): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=request_token, http_url=self.access_token_url) + def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + # May not be needed... + self.request_token = request_token + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) oauth_request.sign_request(signature_method, consumer, request_token) resp = fetch_response(oauth_request, connection) return oauth.OAuthToken.from_string(resp) @@ -302,7 +303,7 @@ class setup: raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - + def retweetedOfMe(self, **kwargs): """retweetedOfMe(**kwargs) @@ -322,7 +323,7 @@ class setup: raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - + def retweetedByMe(self, **kwargs): """retweetedByMe(**kwargs) @@ -342,7 +343,7 @@ class setup: raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - + def retweetedToMe(self, **kwargs): """retweetedToMe(**kwargs) @@ -362,7 +363,7 @@ class setup: raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - + def showUser(self, id = None, user_id = None, screen_name = None): """showUser(id = None, user_id = None, screen_name = None) @@ -381,21 +382,22 @@ class setup: ...will result in only publicly available data being returned. """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/users/show/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/users/show/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + if apiURL != "": try: - return simplejson.load(self.opener.open(apiURL)) + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("showUser() requires you to be authenticated.") - + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") From 69fd3cc7dbf728dba61c98b59e546b2f88e19a03 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 4 Sep 2009 00:36:36 -0400 Subject: [PATCH 110/687] Check for whether or not we can load simplejson from django.utils as a last ditch effort in import statements --- twython.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/twython.py b/twython.py index 5c4a6b8..5afadf0 100644 --- a/twython.py +++ b/twython.py @@ -25,8 +25,11 @@ try: except ImportError: try: import json as simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: import oauth From 34340248de1b858ef23446a1d733d9fcefa9daaf Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 5 Sep 2009 01:50:30 -0400 Subject: [PATCH 111/687] Porting various things over to the Twython3k build - purely experimental, as usual --- twython3k.py | 105 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 39 deletions(-) diff --git a/twython3k.py b/twython3k.py index 6c20250..2c34ab9 100644 --- a/twython3k.py +++ b/twython3k.py @@ -25,11 +25,14 @@ try: except ImportError: try: import json as simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") try: - import oauth + from . import oauth except ImportError: pass @@ -54,7 +57,7 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): + def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -64,6 +67,7 @@ class setup: password - Password for your twitter account, if you want Basic (HTTP) Authentication. consumer_secret - Consumer secret, if you want OAuth. consumer_key - Consumer key, if you want OAuth. + signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. """ self.authenticated = False @@ -77,6 +81,9 @@ class setup: self.consumer_secret = consumer_secret self.request_token = None self.access_token = None + self.consumer = None + self.connection = None + self.signature_method = None # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual @@ -92,31 +99,50 @@ class setup: except HTTPError as e: raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) elif consumer_secret is not None and consumer_key is not None: - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - # req = oauth.OAuthRequest.from_consumer_and_token - # req.sign_request(self.signature_method, self.consumer_key, self.token) - # self.opener = urllib2.build_opener() + self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) + self.connection = http.client.HTTPSConnection(SERVER) pass else: pass - def getRequestToken(self): - response = self.oauth_request(self.request_token_url) - token = self.parseOAuthResponse(response) - self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) - return self.request_token + def getOAuthResource(self, url, access_token, params, http_method="GET"): + """getOAuthResource(self, url, access_token, params, http_method="GET") - def parseOAuthResponse(self, response_string): - # Partial credit goes to Harper Reed for this gem. - lol = {} - for param in response_string.split("&"): - pair = param.split("=") - if(len(pair) != 2): - break - lol[pair[0]] = pair[1] - return lol + Returns a signed OAuth object for use in requests. + """ + newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) + oauth_request.sign_request(self.signature_method, consumer, access_token) + return oauth_request + + def getResponse(self, oauth_request, connection): + """getResponse(self, oauth_request, connection) + Returns a JSON-ified list of results. + """ + url = oauth_request.to_url() + connection.request(oauth_request.http_method, url) + response = connection.getresponse() + return simplejson.load(response.read()) + + def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) + oauth_request.sign_request(signature_method, consumer, None) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + + def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) + oauth_request.sign_request(signature_method, consumer, token) + return oauth_request.to_url() + + def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + # May not be needed... + self.request_token = request_token + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) + oauth_request.sign_request(signature_method, consumer, request_token) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): """shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl") @@ -280,7 +306,7 @@ class setup: raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - + def retweetedOfMe(self, **kwargs): """retweetedOfMe(**kwargs) @@ -300,7 +326,7 @@ class setup: raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - + def retweetedByMe(self, **kwargs): """retweetedByMe(**kwargs) @@ -320,7 +346,7 @@ class setup: raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - + def retweetedToMe(self, **kwargs): """retweetedToMe(**kwargs) @@ -340,7 +366,7 @@ class setup: raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - + def showUser(self, id = None, user_id = None, screen_name = None): """showUser(id = None, user_id = None, screen_name = None) @@ -359,21 +385,22 @@ class setup: ...will result in only publicly available data being returned. """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/users/show/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/users/show.json?user_id=%s" % repr(user_id) - if screen_name is not None: - apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/users/show/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/users/show.json?user_id=%s" % repr(user_id) + if screen_name is not None: + apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + if apiURL != "": try: - return simplejson.load(self.opener.open(apiURL)) + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("showUser() requires you to be authenticated.") - + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") From d6f6df20455e9c346bd68f78eaff5327fcb5e8b8 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 6 Sep 2009 15:48:58 -0400 Subject: [PATCH 112/687] Force any methods that handle image uploading to open the image (for encoding) as a Binary file, was doing ASCII before (oddly, this wasn't caught until now. Major thanks to Yoav Aviram for pointing this out. :D --- twython.py | 4 ++-- twython3k.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/twython.py b/twython.py index 5afadf0..dd0cd27 100644 --- a/twython.py +++ b/twython.py @@ -1257,7 +1257,7 @@ class setup: """ if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} @@ -1278,7 +1278,7 @@ class setup: """ if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} diff --git a/twython3k.py b/twython3k.py index 2c34ab9..08db6ad 100644 --- a/twython3k.py +++ b/twython3k.py @@ -1257,7 +1257,7 @@ class setup: """ if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} @@ -1278,7 +1278,7 @@ class setup: """ if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} From a1c4b17c6d9d24ea2986f25fa85cf8aaf3ae2c29 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 11 Sep 2009 02:32:17 -0400 Subject: [PATCH 113/687] Raise AuthError() instead of a generic TwythonError() when first-time authentication fails. Not sure why this was being done generic before, but it makes no sense now... --- twython.py | 2 +- twython3k.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/twython.py b/twython.py index dd0cd27..7015fa8 100644 --- a/twython.py +++ b/twython.py @@ -97,7 +97,7 @@ class setup: simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: - raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) self.connection = httplib.HTTPSConnection(SERVER) diff --git a/twython3k.py b/twython3k.py index 08db6ad..bc8fdb9 100644 --- a/twython3k.py +++ b/twython3k.py @@ -97,7 +97,7 @@ class setup: simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: - raise TwythonError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) self.connection = http.client.HTTPSConnection(SERVER) From a90595f241be6813efe8e00c7b65fbfa9274b554 Mon Sep 17 00:00:00 2001 From: jlin Date: Sun, 13 Sep 2009 19:35:45 -0400 Subject: [PATCH 114/687] Don't cast id to a string as it's, much of the time, already a friggin' string. By casting, we end up causing 404's all over the place. --- twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython.py b/twython.py index 7015fa8..f14f4a8 100644 --- a/twython.py +++ b/twython.py @@ -650,7 +650,7 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % `id`, "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % id, "?folow=%s" % follow)) else: return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) except HTTPError, e: From d7d099b52dcaa8d645b7f443e168745104294dd5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Sep 2009 01:58:23 -0400 Subject: [PATCH 115/687] Moving README to Markdown --- README => README.markdown | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename README => README.markdown (90%) diff --git a/README b/README.markdown similarity index 90% rename from README rename to README.markdown index 0a73895..c55ccb8 100644 --- a/README +++ b/README.markdown @@ -1,5 +1,5 @@ Twython - Easy Twitter utilities in Python ------------------------------------------------------------------------------------------------------ +========================================================================================= I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at a library that offers more coverage. @@ -21,15 +21,15 @@ Requirements Twython requires (much like Python-Twitter, because they had the right idea :D) a library called "simplejson". You can grab it at the following link: -http://pypi.python.org/pypi/simplejson +> http://pypi.python.org/pypi/simplejson Example Use ----------------------------------------------------------------------------------------------------- -import twython - -twitter = twython.setup(authtype="Basic", username="example", password="example") -twitter.updateStatus("See how easy this was?") +> import twython +> +> twitter = twython.setup(authtype="Basic", username="example", password="example") +> twitter.updateStatus("See how easy this was?") Twython 3k From f6e655eb6d3b28d18e3f6b6abfa84b3b8eed753d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Sep 2009 02:00:35 -0400 Subject: [PATCH 116/687] Minor README updates --- README.markdown | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.markdown b/README.markdown index c55ccb8..336b5d1 100644 --- a/README.markdown +++ b/README.markdown @@ -9,12 +9,11 @@ make a seasoned Python vet scratch his head, or possibly call me insane. It's op and I'm open to anything that'll improve the library as a whole. OAuth support is in the works, but every other part of the Twitter API should be covered. Twython -handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for -Authentication. To override this, specify 'authtype="Basic"' in your twython.setup() call. - -Documentation is forthcoming, but Twython attempts to mirror the Twitter API in a large way. All -parameters for API calls should translate over as function arguments. +handles both Basic (HTTP) Authentication and OAuth. Older versions of Twython need Basic Auth specified - +to override this, specify 'authtype="Basic"' in your twython.setup() call. +Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or +Twitter's API Wiki (Twython calls mirror most of the methods listed there). Requirements ----------------------------------------------------------------------------------------------------- From 93ea27f1b36a3107a96d25276097e27dea4a1f38 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 20 Sep 2009 13:09:37 -0400 Subject: [PATCH 117/687] Fix for issue #7 (filed by kumar303), wherein setup.py reads the wrong README file for description purposes --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8747b12..fb1900c 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ METADATA = dict( author='Ryan McGrath', author_email='ryan@venodesigns.net', description='A new and easy way to access Twitter data with Python.', - long_description= open("README").read(), + long_description= open("README.markdown").read(), license='MIT License', url='http://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython', From 9007722c230d91c51dc8741198f9d53180210810 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 21 Sep 2009 23:32:15 -0400 Subject: [PATCH 118/687] Fixed AuthError issue (#8 in the issue tracker) - AuthError was being passed one argument too many... --- twython.py | 2 +- twython3k.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/twython.py b/twython.py index f14f4a8..fccc2ac 100644 --- a/twython.py +++ b/twython.py @@ -97,7 +97,7 @@ class setup: simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError, e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) self.connection = httplib.HTTPSConnection(SERVER) diff --git a/twython3k.py b/twython3k.py index bc8fdb9..7761f61 100644 --- a/twython3k.py +++ b/twython3k.py @@ -97,7 +97,7 @@ class setup: simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) self.authenticated = True except HTTPError as e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code), e.code) + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) self.connection = http.client.HTTPSConnection(SERVER) From df38e3312a013c9dbb59bc954518f2b60e1a2fea Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 24 Sep 2009 04:04:10 -0400 Subject: [PATCH 119/687] Skeleton for a basic OAuth example, using Django. (Currently not functioning, just wanted it in the repo) --- twython_oauth_example/__init__.py | 0 twython_oauth_example/manage.py | 11 + twython_oauth_example/settings.py | 79 + .../twythonoauth/__init__.py | 0 twython_oauth_example/twythonoauth/models.py | 3 + twython_oauth_example/twythonoauth/tests.py | 23 + twython_oauth_example/twythonoauth/twython.py | 1322 +++++++++++++++++ twython_oauth_example/twythonoauth/views.py | 6 + twython_oauth_example/urls.py | 17 + 9 files changed, 1461 insertions(+) create mode 100644 twython_oauth_example/__init__.py create mode 100755 twython_oauth_example/manage.py create mode 100644 twython_oauth_example/settings.py create mode 100644 twython_oauth_example/twythonoauth/__init__.py create mode 100644 twython_oauth_example/twythonoauth/models.py create mode 100644 twython_oauth_example/twythonoauth/tests.py create mode 100644 twython_oauth_example/twythonoauth/twython.py create mode 100644 twython_oauth_example/twythonoauth/views.py create mode 100644 twython_oauth_example/urls.py diff --git a/twython_oauth_example/__init__.py b/twython_oauth_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/twython_oauth_example/manage.py b/twython_oauth_example/manage.py new file mode 100755 index 0000000..5e78ea9 --- /dev/null +++ b/twython_oauth_example/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/twython_oauth_example/settings.py b/twython_oauth_example/settings.py new file mode 100644 index 0000000..0558139 --- /dev/null +++ b/twython_oauth_example/settings.py @@ -0,0 +1,79 @@ +# Django settings for twython_oauth_example project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@domain.com'), +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = '' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 't@!+0t5ke88p6n^4flyi!s*)^#q65e^36kto#p%vm^m2rj6k=f' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', +) + +ROOT_URLCONF = 'twython_oauth_example.urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', +) diff --git a/twython_oauth_example/twythonoauth/__init__.py b/twython_oauth_example/twythonoauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/twython_oauth_example/twythonoauth/models.py b/twython_oauth_example/twythonoauth/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/twython_oauth_example/twythonoauth/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/twython_oauth_example/twythonoauth/tests.py b/twython_oauth_example/twythonoauth/tests.py new file mode 100644 index 0000000..2247054 --- /dev/null +++ b/twython_oauth_example/twythonoauth/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/twython_oauth_example/twythonoauth/twython.py b/twython_oauth_example/twythonoauth/twython.py new file mode 100644 index 0000000..7015fa8 --- /dev/null +++ b/twython_oauth_example/twythonoauth/twython.py @@ -0,0 +1,1322 @@ +#!/usr/bin/python + +""" + Twython is an up-to-date library for Python that wraps the Twitter API. + Other Python Twitter libraries seem to have fallen a bit behind, and + Twitter's API has evolved a bit. Here's hoping this helps. + + TODO: OAuth, Streaming API? + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools + +from urlparse import urlparse +from urllib2 import HTTPError + +__author__ = "Ryan McGrath " +__version__ = "0.8" + +"""Twython - Easy Twitter utilities in Python""" + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +try: + import oauth +except ImportError: + pass + +class TwythonError(Exception): + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + def __str__(self): + return repr(self.msg) + +class APILimit(TwythonError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class AuthError(TwythonError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class setup: + def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): + """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + username - Your Twitter username, if you want Basic (HTTP) Authentication. + password - Password for your twitter account, if you want Basic (HTTP) Authentication. + consumer_secret - Consumer secret, if you want OAuth. + consumer_key - Consumer key, if you want OAuth. + signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() + headers - User agent header. + """ + self.authenticated = False + self.username = username + # OAuth specific variables below + self.request_token_url = 'https://twitter.com/oauth/request_token' + self.access_token_url = 'https://twitter.com/oauth/access_token' + self.authorization_url = 'http://twitter.com/oauth/authorize' + self.signin_url = 'http://twitter.com/oauth/authenticate' + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.request_token = None + self.access_token = None + self.consumer = None + self.connection = None + self.signature_method = None + # Check and set up authentication + if self.username is not None and password is not None: + # Assume Basic authentication ritual + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://twitter.com", self.username, password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + self.authenticated = True + except HTTPError, e: + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) + elif consumer_secret is not None and consumer_key is not None: + self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) + self.connection = httplib.HTTPSConnection(SERVER) + pass + else: + pass + + def getOAuthResource(self, url, access_token, params, http_method="GET"): + """getOAuthResource(self, url, access_token, params, http_method="GET") + + Returns a signed OAuth object for use in requests. + """ + newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) + oauth_request.sign_request(self.signature_method, consumer, access_token) + return oauth_request + + def getResponse(self, oauth_request, connection): + """getResponse(self, oauth_request, connection) + + Returns a JSON-ified list of results. + """ + url = oauth_request.to_url() + connection.request(oauth_request.http_method, url) + response = connection.getresponse() + return simplejson.load(response.read()) + + def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) + oauth_request.sign_request(signature_method, consumer, None) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + + def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) + oauth_request.sign_request(signature_method, consumer, token) + return oauth_request.to_url() + + def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + # May not be needed... + self.request_token = request_token + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) + oauth_request.sign_request(signature_method, consumer, request_token) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + + # URL Shortening function huzzah + def shortenURL(self, 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 url shorterning service other that is.gd. + """ + try: + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() + except HTTPError, e: + raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) + + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def getRateLimitStatus(self, rate_for = "requestingIP"): + """getRateLimitStatus() + + Returns the remaining number of API requests available to the requesting user before the + API limit is reached for the current hour. Calls to rate_limit_status do not count against + the rate limit. If authentication credentials are provided, the rate limit status for the + authenticating user is returned. Otherwise, the rate limit status for the requesting + IP address is returned. + """ + try: + if rate_for == "requestingIP": + return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + else: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + else: + raise TwythonError("You need to be authenticated to check a rate limit status on an account.") + except HTTPError, e: + raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self): + """getPublicTimeline() + + Returns the 20 most recent statuses from non-protected users who have set a custom user icon. + The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + """ + try: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + except HTTPError, e: + raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getHomeTimeline(self, **kwargs): + """getHomeTimeline(**kwargs) + + Returns the 20 most recent statuses, including retweets, posted by the authenticating user + and that user's friends. This is the equivalent of /timeline/home on the Web. + + Usage note: This home_timeline is identical to statuses/friends_timeline, except it also + contains retweets, which statuses/friends_timeline does not (for backwards compatibility + reasons). In a future version of the API, statuses/friends_timeline will go away and + be replaced by home_timeline. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + return simplejson.load(self.opener.open(homeTimelineURL)) + except HTTPError, e: + raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) + else: + raise AuthError("getHomeTimeline() requires you to be authenticated.") + + def getFriendsTimeline(self, **kwargs): + """getFriendsTimeline(**kwargs) + + Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. + This is the equivalent of /timeline/home on the Web. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + return simplejson.load(self.opener.open(friendsTimelineURL)) + except HTTPError, e: + raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) + else: + raise AuthError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, **kwargs): + """getUserTimeline(id = None, **kwargs) + + Returns the 20 most recent statuses posted from the authenticating user. It's also + possible to request another user's timeline via the id parameter. This is the + equivalent of the Web / page for your own user, or the profile page for a third party. + + Parameters: + id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. + user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. + screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % `id`, kwargs) + elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) + else: + userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + try: + # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user + if self.authenticated is True: + return simplejson.load(self.opener.open(userTimelineURL)) + else: + return simplejson.load(urllib2.urlopen(userTimelineURL)) + except HTTPError, e: + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + % `e.code`, e.code) + + def getUserMentions(self, **kwargs): + """getUserMentions(**kwargs) + + Returns the 20 most recent mentions (status containing @username) for the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + return simplejson.load(self.opener.open(mentionsFeedURL)) + except HTTPError, e: + raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getUserMentions() requires you to be authenticated.") + + def reTweet(self, id): + """reTweet(id) + + Retweets a tweet. Requires the id parameter of the tweet you are retweeting. + + Parameters: + id - Required. The numerical ID of the tweet you are retweeting. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % `id`, "POST")) + except HTTPError, e: + raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("reTweet() requires you to be authenticated.") + + def retweetedOfMe(self, **kwargs): + """retweetedOfMe(**kwargs) + + Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedOfMe() requires you to be authenticated.") + + def retweetedByMe(self, **kwargs): + """retweetedByMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedByMe() requires you to be authenticated.") + + def retweetedToMe(self, **kwargs): + """retweetedToMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user's friends. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedToMe() requires you to be authenticated.") + + def showUser(self, id = None, user_id = None, screen_name = None): + """showUser(id = None, user_id = None, screen_name = None) + + Returns extended information of a given user. The author's most recent status will be returned inline. + + Parameters: + ** Note: One of the following must always be specified. + id - The ID or screen name of a user. + user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + + Usage Notes: + Requests for protected users without credentials from + 1) the user requested or + 2) a user that is following the protected user will omit the nested status element. + + ...will result in only publicly available data being returned. + """ + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/users/show/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + if apiURL != "": + try: + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) + + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. + (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access + older friends. With no user specified, the request defaults to the authenticated users friends. + + It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + + Parameters: + ** Note: One of the following is required. (id, user_id, or screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of friends. + user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page of friends to receive. + """ + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/friends/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFriendsStatus() requires you to be authenticated.") + + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") + + Returns the authenticating user's followers, each with current status inline. + They are ordered by the order in which they joined Twitter, 100 at a time. + (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) + + Use the page option to access earlier followers. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of followers. + user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page to retrieve. + """ + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/statuses/followers/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFollowersStatus() requires you to be authenticated.") + + + def showStatus(self, id): + """showStatus(id) + + Returns a single status, specified by the id parameter below. + The status's author will be returned inline. + + Parameters: + id - Required. The numerical ID of the status to retrieve. + """ + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + else: + return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + except HTTPError, e: + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + % `e.code`, e.code) + + def updateStatus(self, status, in_reply_to_status_id = None): + """updateStatus(status, in_reply_to_status_id = None) + + Updates the authenticating user's status. Requires the status parameter specified below. + A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. + + Parameters: + status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. + in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + + ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references + is mentioned within the status text. Therefore, you must include @username, where username is + the author of the referenced tweet, within the update. + """ + if len(list(status)) > 140: + raise TwythonError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + except HTTPError, e: + raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) + + def destroyStatus(self, id): + """destroyStatus(id) + + Destroys the status specified by the required ID parameter. + The authenticating user must be the author of the specified status. + + Parameters: + id - Required. The ID of the status to destroy. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "DELETE")) + except HTTPError, e: + raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroyStatus() requires you to be authenticated.") + + def endSession(self): + """endSession() + + Ends the session of the authenticating user, returning a null cookie. + Use this method to sign users out of client-facing applications (widgets, etc). + """ + if self.authenticated is True: + try: + self.opener.open("http://twitter.com/account/end_session.json", "") + self.authenticated = False + except HTTPError, e: + raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent to the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages.json?page=%s" % `page` + if since_id is not None: + apiURL += "&since_id=%s" % `since_id` + if max_id is not None: + apiURL += "&max_id=%s" % `max_id` + if count is not None: + apiURL += "&count=%s" % `count` + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + """getSentMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + """ + if self.authenticated is True: + apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % `page` + if since_id is not None: + apiURL += "&since_id=%s" % `since_id` + if max_id is not None: + apiURL += "&max_id=%s" % `max_id` + if count is not None: + apiURL += "&count=%s" % `count` + + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text): + """sendDirectMessage(user, text) + + Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. + Returns the sent message in the requested format when successful. + + Parameters: + user - Required. The ID or screen name of the recipient user. + text - Required. The text of your direct message. Be sure to keep it under 140 characters. + """ + if self.authenticated is True: + if len(list(text)) < 140: + try: + return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + except HTTPError, e: + raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("Your message must not be longer than 140 characters") + else: + raise AuthError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id): + """destroyDirectMessage(id) + + Destroys the direct message specified in the required ID parameter. + The authenticating user must be the recipient of the specified direct message. + + Parameters: + id - Required. The ID of the direct message to destroy. + """ + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + except HTTPError, e: + raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") + + Allows the authenticating users to follow the user specified in the ID parameter. + Returns the befriended user in the requested format when successful. Returns a + string describing the failure condition when unsuccessful. If you are already + friends with the user an HTTP 403 will be returned. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to befriend. + user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. + follow - Optional. Enable notifications for the target user in addition to becoming friends. + """ + if self.authenticated is True: + apiURL = "" + if user_id is not None: + apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) + if screen_name is not None: + apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) + try: + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % `id`, "?folow=%s" % follow)) + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) + except HTTPError, e: + # Rate limiting is done differently here for API reasons... + if e.code == 403: + raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") + raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None): + """destroyFriendship(id = None, user_id = None, screen_name = None) + + Allows the authenticating users to unfollow the user specified in the ID parameter. + Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to unfollow. + user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + """ + if self.authenticated is True: + apiURL = "" + if user_id is not None: + apiURL = "?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "?screen_name=%s" % screen_name + try: + if id is not None: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % `id`, "lol=1")) # Random string appended for POST reasons, quick hack ;P + else: + return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) + except HTTPError, e: + raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b): + """checkIfFriendshipExists(user_a, user_b) + + Tests for the existence of friendship between two users. + Will return true if user_a follows user_b; otherwise, it'll return false. + + Parameters: + user_a - Required. The ID or screen_name of the subject user. + user_b - Required. The ID or screen_name of the user to test for following. + """ + if self.authenticated is True: + try: + friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.urlencode({"user_a": user_a, "user_b": user_b}) + return simplejson.load(self.opener.open(friendshipURL)) + except HTTPError, e: + raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): + """showFriendship(source_id, source_screen_name, target_id, target_screen_name) + + Returns detailed information about the relationship between two users. + + Parameters: + ** Note: One of the following is required if the request is unauthenticated + source_id - The user_id of the subject user. + source_screen_name - The screen_name of the subject user. + + ** Note: One of the following is required at all times + target_id - The user_id of the target user. + target_screen_name - The screen_name of the target user. + """ + apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D + if source_id is not None: + apiURL += "&source_id=%s" % `source_id` + if source_screen_name is not None: + apiURL += "&source_screen_name=%s" % source_screen_name + if target_id is not None: + apiURL += "&target_id=%s" % `target_id` + if target_screen_name is not None: + apiURL += "&target_screen_name=%s" % target_screen_name + try: + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + # Catch this for now + if e.code == 403: + raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") + raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) + + def updateDeliveryDevice(self, device_name = "none"): + """updateDeliveryDevice(device_name = "none") + + Sets which device Twitter delivers updates to for the authenticating user. + Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) + + Parameters: + device - Required. Must be one of: sms, im, none. + """ + if self.authenticated is True: + try: + return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": self.unicode2utf8(device_name)})) + except HTTPError, e: + raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, **kwargs): + """updateProfileColors(**kwargs) + + Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + + Parameters: + ** Note: One or more of the following parameters must be present. Each parameter's value must + be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). + + profile_background_color - Optional. + profile_text_color - Optional. + profile_link_color - Optional. + profile_sidebar_fill_color - Optional. + profile_sidebar_border_color - Optional. + """ + if self.authenticated is True: + try: + return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + except HTTPError, e: + raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + """updateProfile(name = None, email = None, url = None, location = None, description = None) + + Sets values that users are able to set under the "Account" tab of their settings page. + Only the parameters specified will be updated. + + Parameters: + One or more of the following parameters must be present. Each parameter's value + should be a string. See the individual parameter descriptions below for further constraints. + + name - Optional. Maximum of 20 characters. + email - Optional. Maximum of 40 characters. Must be a valid email address. + url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. + location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. + description - Optional. Maximum of 160 characters. + """ + if self.authenticated is True: + useAmpersands = False + updateProfileQueryString = "" + if name is not None: + if len(list(name)) < 20: + updateProfileQueryString += "name=" + name + useAmpersands = True + else: + raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") + if email is not None and "@" in email: + if len(list(email)) < 40: + if useAmpersands is True: + updateProfileQueryString += "&email=" + email + else: + updateProfileQueryString += "email=" + email + useAmpersands = True + else: + raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + if url is not None: + if len(list(url)) < 100: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) + else: + updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) + useAmpersands = True + else: + raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") + if location is not None: + if len(list(location)) < 30: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) + else: + updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) + useAmpersands = True + else: + raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") + if description is not None: + if len(list(description)) < 160: + if useAmpersands is True: + updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) + else: + updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) + else: + raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") + + if updateProfileQueryString != "": + try: + return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + except HTTPError, e: + raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1"): + """getFavorites(page = "1") + + Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. + + Parameters: + page - Optional. Specifies the page of favorites to retrieve. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id): + """createFavorite(id) + + Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. + + Parameters: + id - Required. The ID of the status to favorite. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % `id`, "")) + except HTTPError, e: + raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id): + """destroyFavorite(id) + + Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. + + Parameters: + id - Required. The ID of the status to un-favorite. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % `id`, "")) + except HTTPError, e: + raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None): + """notificationFollow(id = None, user_id = None, screen_name = None) + + Enables device notifications for updates from the specified user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/follow/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None): + """notificationLeave(id = None, user_id = None, screen_name = None) + + Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + """ + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/notifications/leave/%s.json" % id + if user_id is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name + try: + return simplejson.load(self.opener.open(apiURL, "")) + except HTTPError, e: + raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user the specified user is following. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, `page`) + if user_id is not None: + apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) + if screen_name is not None: + apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") + + Returns an array of numeric IDs for every user following the specified user. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + """ + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(`id`, `page`) + if user_id is not None: + apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) + if screen_name is not None: + apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id): + """createBlock(id) + + Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. + Returns the blocked user in the requested format when successful. + + Parameters: + id - The ID or screen name of a user to block. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % `id`, "")) + except HTTPError, e: + raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id): + """destroyBlock(id) + + Un-blocks the user specified in the ID parameter for the authenticating user. + Returns the un-blocked user in the requested format when successful. + + Parameters: + id - Required. The ID or screen_name of the user to un-block + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % `id`, "")) + except HTTPError, e: + raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + """checkIfBlockExists(id = None, user_id = None, screen_name = None) + + Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and + error with an HTTP 404 response code otherwise. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen_name of the potentially blocked user. + user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + """ + apiURL = "" + if id is not None: + apiURL = "http://twitter.com/blocks/exists/%s.json" % `id` + if user_id is not None: + apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % `user_id` + if screen_name is not None: + apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name + try: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1"): + """getBlocking(page = "1") + + Returns an array of user objects that the authenticating user is blocking. + + Parameters: + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % `page`)) + except HTTPError, e: + raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self): + """getBlockedIDs() + + Returns an array of numeric user ids the authenticating user is blocking. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + except HTTPError, e: + raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getBlockedIDs() requires you to be authenticated.") + + def searchTwitter(self, search_query, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. + lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. + locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. + rpp - Optional. The number of tweets to return per page, up to a max of 100. + page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) + since_id - Optional. Returns tweets with status ids greater than the given id. + geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. + show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. + + Usage Notes: + Queries are limited 140 URL encoded characters. + Some users may be absent from search results. + The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. + This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. + + Applications must have a meaningful and unique User Agent when using this method. + An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than + applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + return simplejson.load(urllib2.urlopen(searchURL)) + except HTTPError, e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def getCurrentTrends(self, excludeHashTags = False): + """getCurrentTrends(excludeHashTags = False) + + Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used + on Twitter Search results page for that topic. + + Parameters: + excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ + apiURL = "http://search.twitter.com/trends/current.json" + if excludeHashTags is True: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + + def getDailyTrends(self, date = None, exclude = False): + """getDailyTrends(date = None, exclude = False) + + Returns the top 20 trending topics for each hour in a given day. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=%s" % date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + + def getWeeklyTrends(self, date = None, exclude = False): + """getWeeklyTrends(date = None, exclude = False) + + Returns the top 30 trending topics for each day in a given week. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ + apiURL = "http://search.twitter.com/trends/daily.json" + questionMarkUsed = False + if date is not None: + apiURL += "?date=%s" % date + questionMarkUsed = True + if exclude is True: + if questionMarkUsed is True: + apiURL += "&exclude=hashtags" + else: + apiURL += "?exclude=hashtags" + try: + return simplejson.load(urllib.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self): + """getSavedSearches() + + Returns the authenticated user's saved search queries. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + except HTTPError, e: + raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id): + """showSavedSearch(id) + + Retrieve the data for a saved search owned by the authenticating user specified by the given id. + + Parameters: + id - Required. The id of the saved search to be retrieved. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % `id`)) + except HTTPError, e: + raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query): + """createSavedSearch(query) + + Creates a saved search for the authenticated user. + + Parameters: + query - Required. The query of the search the user would like to save. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) + except HTTPError, e: + raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id): + """destroySavedSearch(id) + + Destroys a saved search for the authenticated user. + The search specified by id must be owned by the authenticating user. + + Parameters: + id - Required. The id of the saved search to be deleted. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % `id`, "")) + except HTTPError, e: + raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroySavedSearch() requires you to be authenticated.") + + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true"): + """updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + """ + if self.authenticated is True: + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename): + """updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ + if self.authenticated is True: + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("You realize you need to be authenticated to change a profile image, right?") + + def encode_multipart_formdata(self, fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % self.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + def get_content_type(self, filename): + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + def unicode2utf8(self, text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text diff --git a/twython_oauth_example/twythonoauth/views.py b/twython_oauth_example/twythonoauth/views.py new file mode 100644 index 0000000..d3ac084 --- /dev/null +++ b/twython_oauth_example/twythonoauth/views.py @@ -0,0 +1,6 @@ +import twython + +def auth(request): + + + diff --git a/twython_oauth_example/urls.py b/twython_oauth_example/urls.py new file mode 100644 index 0000000..acbdf76 --- /dev/null +++ b/twython_oauth_example/urls.py @@ -0,0 +1,17 @@ +from django.conf.urls.defaults import * + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns('', + # Example: + # (r'^twython_oauth_example/', include('twython_oauth_example.foo.urls')), + + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # to INSTALLED_APPS to enable admin documentation: + # (r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # (r'^admin/', include(admin.site.urls)), +) From 44de246cdee9eb05dbca0a7b3ca412caa2ab6196 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 24 Sep 2009 04:05:38 -0400 Subject: [PATCH 120/687] Simple Django app skeleton for OAuth testing; currently doesn't do anything special, just wanted it in the repo for testing purposes. --- twython_oauth_example/manage.py | 11 --- twython_oauth_example/settings.py | 79 ------------------- .../twythonoauth/__init__.py | 0 twython_oauth_example/urls.py | 17 ---- .../__init__.py | 0 .../twythonoauth => twythonoauth}/models.py | 0 .../twythonoauth => twythonoauth}/tests.py | 0 .../twythonoauth => twythonoauth}/twython.py | 0 .../twythonoauth => twythonoauth}/views.py | 0 9 files changed, 107 deletions(-) delete mode 100755 twython_oauth_example/manage.py delete mode 100644 twython_oauth_example/settings.py delete mode 100644 twython_oauth_example/twythonoauth/__init__.py delete mode 100644 twython_oauth_example/urls.py rename {twython_oauth_example => twythonoauth}/__init__.py (100%) rename {twython_oauth_example/twythonoauth => twythonoauth}/models.py (100%) rename {twython_oauth_example/twythonoauth => twythonoauth}/tests.py (100%) rename {twython_oauth_example/twythonoauth => twythonoauth}/twython.py (100%) rename {twython_oauth_example/twythonoauth => twythonoauth}/views.py (100%) diff --git a/twython_oauth_example/manage.py b/twython_oauth_example/manage.py deleted file mode 100755 index 5e78ea9..0000000 --- a/twython_oauth_example/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/twython_oauth_example/settings.py b/twython_oauth_example/settings.py deleted file mode 100644 index 0558139..0000000 --- a/twython_oauth_example/settings.py +++ /dev/null @@ -1,79 +0,0 @@ -# Django settings for twython_oauth_example project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - # ('Your Name', 'your_email@domain.com'), -) - -MANAGERS = ADMINS - -DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. -DATABASE_NAME = '' # Or path to database file if using sqlite3. -DATABASE_USER = '' # Not used with sqlite3. -DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. -DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 't@!+0t5ke88p6n^4flyi!s*)^#q65e^36kto#p%vm^m2rj6k=f' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -) - -ROOT_URLCONF = 'twython_oauth_example.urls' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', -) diff --git a/twython_oauth_example/twythonoauth/__init__.py b/twython_oauth_example/twythonoauth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/twython_oauth_example/urls.py b/twython_oauth_example/urls.py deleted file mode 100644 index acbdf76..0000000 --- a/twython_oauth_example/urls.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.conf.urls.defaults import * - -# Uncomment the next two lines to enable the admin: -# from django.contrib import admin -# admin.autodiscover() - -urlpatterns = patterns('', - # Example: - # (r'^twython_oauth_example/', include('twython_oauth_example.foo.urls')), - - # Uncomment the admin/doc line below and add 'django.contrib.admindocs' - # to INSTALLED_APPS to enable admin documentation: - # (r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Uncomment the next line to enable the admin: - # (r'^admin/', include(admin.site.urls)), -) diff --git a/twython_oauth_example/__init__.py b/twythonoauth/__init__.py similarity index 100% rename from twython_oauth_example/__init__.py rename to twythonoauth/__init__.py diff --git a/twython_oauth_example/twythonoauth/models.py b/twythonoauth/models.py similarity index 100% rename from twython_oauth_example/twythonoauth/models.py rename to twythonoauth/models.py diff --git a/twython_oauth_example/twythonoauth/tests.py b/twythonoauth/tests.py similarity index 100% rename from twython_oauth_example/twythonoauth/tests.py rename to twythonoauth/tests.py diff --git a/twython_oauth_example/twythonoauth/twython.py b/twythonoauth/twython.py similarity index 100% rename from twython_oauth_example/twythonoauth/twython.py rename to twythonoauth/twython.py diff --git a/twython_oauth_example/twythonoauth/views.py b/twythonoauth/views.py similarity index 100% rename from twython_oauth_example/twythonoauth/views.py rename to twythonoauth/views.py From 23aeff128d5b2e9dc022ac28a7f52c5c42189693 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 29 Sep 2009 02:58:20 -0400 Subject: [PATCH 121/687] Support for the cursoring parameter (getFriendsIDs(), getFollowersIDs(), getFriendsStatus(), getFollowersStatus()) is in Twython now, which puts us more in line for Twitter's deprecation of the page method on the 26th of October. statuses/retweets is also now supported through getRetweets() --- twython.py | 87 +++++++++++++++++++++++++++++++++++++------------- twython3k.py | 89 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 131 insertions(+), 45 deletions(-) diff --git a/twython.py b/twython.py index fccc2ac..53b9139 100644 --- a/twython.py +++ b/twython.py @@ -307,6 +307,26 @@ class setup: else: raise AuthError("reTweet() requires you to be authenticated.") + def getRetweets(self, id, count = None): + """ getRetweets(self, id, count): + + Returns up to 100 of the first retweets of a given tweet. + + Parameters: + id - Required. The numerical ID of the tweet you want the retweets of. + Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + """ + if self.authenticated is True: + apiURL = "http://twitter.com/statuses/retweets/%s.json" % `id` + if count is not None: + apiURL += "?count=%s" % `count` + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("getRetweets failed with a %s eroror code." % `e.code`, e.code) + else: + raise AuthError("getRetweets() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): """retweetedOfMe(**kwargs) @@ -401,8 +421,8 @@ class setup: except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1"): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access @@ -410,12 +430,15 @@ class setup: It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, or screen_name) id - Optional. The ID or screen name of the user for whom to request a list of friends. user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page of friends to receive. + page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. """ if self.authenticated is True: apiURL = "" @@ -426,14 +449,17 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError, e: raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns the authenticating user's followers, each with current status inline. They are ordered by the order in which they joined Twitter, 100 at a time. @@ -441,12 +467,15 @@ class setup: Use the page option to access earlier followers. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Optional. The ID or screen name of the user for whom to request a list of followers. user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page to retrieve. + page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. """ if self.authenticated is True: apiURL = "" @@ -457,13 +486,15 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError, e: raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - - + def showStatus(self, id): """showStatus(id) @@ -951,49 +982,61 @@ class setup: else: raise AuthError("notificationLeave() requires you to be authenticated.") - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user the specified user is following. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. """ apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, `page`) + apiURL = "http://twitter.com/friends/ids/%s.json?%s" %(id, breakResults) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) + apiURL = "http://twitter.com/friends/ids.json?user_id=%s&%s" %(`user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) + apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&%s" %(screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user following the specified user. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. """ apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(`id`, `page`) + apiURL = "http://twitter.com/followers/ids/%s.json?%s" %(`id`, breakResults) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) + apiURL = "http://twitter.com/followers/ids.json?user_id=%s&%s" %(`user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) + apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&%s" %(screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: diff --git a/twython3k.py b/twython3k.py index 7761f61..a4bbbbe 100644 --- a/twython3k.py +++ b/twython3k.py @@ -307,6 +307,26 @@ class setup: else: raise AuthError("reTweet() requires you to be authenticated.") + def getRetweets(self, id, count = None): + """ getAllRetweets(self, id, count): + + Returns up to 100 of the first retweets of a given tweet. + + Parameters: + id - Required. The numerical ID of the tweet you want the retweets of. + Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + """ + if self.authenticated is True: + apiURL = "http://twitter.com/statuses/retweets/%s.json" % repr(id) + if count is not None: + apiURL += "?count=%s" % repr(count) + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TwythonError("getRetweets failed with a %s eroror code." % repr(e.code), e.code) + else: + raise AuthError("getRetweets() requires you to be authenticated.") + def retweetedOfMe(self, **kwargs): """retweetedOfMe(**kwargs) @@ -401,8 +421,8 @@ class setup: except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1"): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access @@ -410,12 +430,15 @@ class setup: It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, or screen_name) id - Optional. The ID or screen name of the user for whom to request a list of friends. user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page of friends to receive. + page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. """ if self.authenticated is True: apiURL = "" @@ -426,14 +449,17 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError as e: raise TwythonError("getFriendsStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns the authenticating user's followers, each with current status inline. They are ordered by the order in which they joined Twitter, 100 at a time. @@ -441,12 +467,15 @@ class setup: Use the page option to access earlier followers. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Optional. The ID or screen name of the user for whom to request a list of followers. user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page to retrieve. + page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. """ if self.authenticated is True: apiURL = "" @@ -457,13 +486,15 @@ class setup: if screen_name is not None: apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError as e: raise TwythonError("getFollowersStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - - + def showStatus(self, id): """showStatus(id) @@ -650,7 +681,7 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % repr(id), "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % id, "?folow=%s" % follow)) else: return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) except HTTPError as e: @@ -951,49 +982,61 @@ class setup: else: raise AuthError("notificationLeave() requires you to be authenticated.") - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user the specified user is following. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. """ apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, repr(page)) + apiURL = "http://twitter.com/friends/ids/%s.json?%s" %(id, breakResults) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(repr(user_id), repr(page)) + apiURL = "http://twitter.com/friends/ids.json?user_id=%s&%s" %(repr(user_id), breakResults) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, repr(page)) + apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&%s" %(screen_name, breakResults) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user following the specified user. + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + Parameters: ** Note: One of the following is required. (id, user_id, screen_name) id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. """ apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(repr(id), repr(page)) + apiURL = "http://twitter.com/followers/ids/%s.json?%s" %(repr(id), breakResults) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(repr(user_id), repr(page)) + apiURL = "http://twitter.com/followers/ids.json?user_id=%s&%s" %(repr(user_id), breakResults) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, repr(page)) + apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&%s" %(screen_name, breakResults) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: From 46c93f4adb5f76d60aed3a5ec727ae3f1c548b35 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 5 Oct 2009 02:07:03 -0400 Subject: [PATCH 122/687] A stub for further work on Streaming API integration - nothing to see here yet, move along... --- streaming.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 streaming.py diff --git a/streaming.py b/streaming.py new file mode 100644 index 0000000..b681145 --- /dev/null +++ b/streaming.py @@ -0,0 +1,50 @@ +#!/usr/bin/python + +""" + The beginnings of Twitter Streaming API support in Twython. Don't expect this to work at all, + consider it a stub for now. -- Ryan + + Questions, comments? ryan@venodesigns.net +""" + +import httplib, urllib, urllib2, mimetypes, mimetools, socket, time + +from urllib2 import HTTPError + +try: + import simplejson +except ImportError: + try: + import json as simplejson + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython (Streaming) requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +__author__ = "Ryan McGrath " +__version__ = "0.1" + +class TwythonStreamingError(Exception): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +feeds = { + "firehose": "http://stream.twitter.com/firehose.json", + "gardenhose": "http://stream.twitter.com/gardenhose.json", + "spritzer": "http://stream.twitter.com/spritzer.json", + "birddog": "http://stream.twitter.com/birddog.json", + "shadow": "http://stream.twitter.com/shadow.json", + "follow": "http://stream.twitter.com/follow.json", + "track": "http://stream.twitter.com/track.json", +} + +class stream: + def __init__(self, username = None, password = None, feed = "spritzer", user_agent = "Twython Streaming"): + self.username = username + self.password = password + self.feed = feeds[feed] + self.user_agent = user_agent + self.connection_open = False From 024742d51bc3af4a68fd4f572fb1d59e773233be Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 10 Oct 2009 17:22:29 -0400 Subject: [PATCH 123/687] All example code now properly references Twython instead of Tango - this should've been done months ago, can't believe it slipped my mind. Sorry for anyone who was totally confused by this. ;P --- examples/current_trends.py | 6 +++--- examples/daily_trends.py | 6 +++--- examples/get_friends_timeline.py | 4 ++-- examples/get_user_mention.py | 4 ++-- examples/get_user_timeline.py | 4 ++-- examples/public_timeline.py | 4 ++-- examples/rate_limit.py | 4 ++-- examples/search_results.py | 4 ++-- examples/twython_setup.py | 11 +++++++++++ examples/update_profile_image.py | 6 +++--- examples/update_status.py | 6 +++--- examples/weekly_trends.py | 6 +++--- 12 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 examples/twython_setup.py diff --git a/examples/current_trends.py b/examples/current_trends.py index dd2d50d..6fcfdd7 100644 --- a/examples/current_trends.py +++ b/examples/current_trends.py @@ -1,7 +1,7 @@ -import tango +import twitter -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() +""" Instantiate Twython with no Authentication """ +twitter = twitter.setup() trends = twitter.getCurrentTrends() print trends diff --git a/examples/daily_trends.py b/examples/daily_trends.py index 28bdde1..6e3a539 100644 --- a/examples/daily_trends.py +++ b/examples/daily_trends.py @@ -1,7 +1,7 @@ -import tango +import twitter -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() +""" Instantiate Twython with no Authentication """ +twitter = twitter.setup() trends = twitter.getDailyTrends() print trends diff --git a/examples/get_friends_timeline.py b/examples/get_friends_timeline.py index e3c3ddd..9f3fa06 100644 --- a/examples/get_friends_timeline.py +++ b/examples/get_friends_timeline.py @@ -1,7 +1,7 @@ -import tango, pprint +import twython, pprint # Authenticate using Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(authtype="Basic", username="example", password="example") friends_timeline = twitter.getFriendsTimeline(count="150", page="3") for tweet in friends_timeline: diff --git a/examples/get_user_mention.py b/examples/get_user_mention.py index 6b94371..d150aa1 100644 --- a/examples/get_user_mention.py +++ b/examples/get_user_mention.py @@ -1,6 +1,6 @@ -import tango +import twitter -twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter = twitter.setup(authtype="Basic", username="example", password="example") mentions = twitter.getUserMentions(count="150") print mentions diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py index aa7bf97..b4191b0 100644 --- a/examples/get_user_timeline.py +++ b/examples/get_user_timeline.py @@ -1,7 +1,7 @@ -import tango +import twython # We won't authenticate for this, but sometimes it's necessary -twitter = tango.setup() +twitter = twython.setup() user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") print user_timeline diff --git a/examples/public_timeline.py b/examples/public_timeline.py index 4a6c161..b6be0f7 100644 --- a/examples/public_timeline.py +++ b/examples/public_timeline.py @@ -1,7 +1,7 @@ -import tango +import twython # Getting the public timeline requires no authentication, huzzah -twitter = tango.setup() +twitter = twython.setup() public_timeline = twitter.getPublicTimeline() for tweet in public_timeline: diff --git a/examples/rate_limit.py b/examples/rate_limit.py index ae85973..aca5095 100644 --- a/examples/rate_limit.py +++ b/examples/rate_limit.py @@ -1,7 +1,7 @@ -import tango +import twython # Instantiate with Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(authtype="Basic", username="example", password="example") # This returns the rate limit for the requesting IP rateLimit = twitter.getRateLimitStatus() diff --git a/examples/search_results.py b/examples/search_results.py index 6cec48f..b046b22 100644 --- a/examples/search_results.py +++ b/examples/search_results.py @@ -1,7 +1,7 @@ -import tango +import twython """ Instantiate Tango with no Authentication """ -twitter = tango.setup() +twitter = twython.setup() search_results = twitter.searchTwitter("WebsDotCom", rpp="50") for tweet in search_results["results"]: diff --git a/examples/twython_setup.py b/examples/twython_setup.py new file mode 100644 index 0000000..362cb43 --- /dev/null +++ b/examples/twython_setup.py @@ -0,0 +1,11 @@ +import twython + +# Using no authentication and specifying Debug +twitter = twython.setup(debug=True) + +# Using Basic Authentication +twitter = twython.setup(authtype="Basic", username="example", password="example") + +# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) +auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} +twitter = twython.setup(username="example", password="example", oauth_keys=auth_keys) diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py index a6f52b2..ad6f9ad 100644 --- a/examples/update_profile_image.py +++ b/examples/update_profile_image.py @@ -1,5 +1,5 @@ -import tango +import twython -# Instantiate Tango with Basic (HTTP) Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") +# Instantiate Twython with Basic (HTTP) Authentication +twitter = twython.setup(authtype="Basic", username="example", password="example") twitter.updateProfileImage("myImage.png") diff --git a/examples/update_status.py b/examples/update_status.py index 1466752..52d2e87 100644 --- a/examples/update_status.py +++ b/examples/update_status.py @@ -1,5 +1,5 @@ -import tango +import twython -# Create a Tango instance using Basic (HTTP) Authentication and update our Status -twitter = tango.setup(authtype="Basic", username="example", password="example") +# Create a Twython instance using Basic (HTTP) Authentication and update our Status +twitter = twython.setup(authtype="Basic", username="example", password="example") twitter.updateStatus("See how easy this was?") diff --git a/examples/weekly_trends.py b/examples/weekly_trends.py index fd9b564..5871fbd 100644 --- a/examples/weekly_trends.py +++ b/examples/weekly_trends.py @@ -1,7 +1,7 @@ -import tango +import twython -""" Instantiate Tango with no Authentication """ -twitter = tango.setup() +""" Instantiate Twython with no Authentication """ +twitter = twython.setup() trends = twitter.getWeeklyTrends() print trends From 09cce11143e53325cbc509e6697b8f9fec305710 Mon Sep 17 00:00:00 2001 From: idris Date: Sun, 11 Oct 2009 17:18:44 -0400 Subject: [PATCH 124/687] oops my bad. fixed the examples properly this time --- examples/current_trends.py | 2 +- examples/daily_trends.py | 2 +- examples/get_user_mention.py | 2 +- examples/shorten_url.py | 4 ++-- examples/tango_setup.py | 11 ----------- 5 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 examples/tango_setup.py diff --git a/examples/current_trends.py b/examples/current_trends.py index 6fcfdd7..425cdee 100644 --- a/examples/current_trends.py +++ b/examples/current_trends.py @@ -1,7 +1,7 @@ import twitter """ Instantiate Twython with no Authentication """ -twitter = twitter.setup() +twitter = twython.setup() trends = twitter.getCurrentTrends() print trends diff --git a/examples/daily_trends.py b/examples/daily_trends.py index 6e3a539..987611e 100644 --- a/examples/daily_trends.py +++ b/examples/daily_trends.py @@ -1,7 +1,7 @@ import twitter """ Instantiate Twython with no Authentication """ -twitter = twitter.setup() +twitter = twython.setup() trends = twitter.getDailyTrends() print trends diff --git a/examples/get_user_mention.py b/examples/get_user_mention.py index d150aa1..08b3dff 100644 --- a/examples/get_user_mention.py +++ b/examples/get_user_mention.py @@ -1,6 +1,6 @@ import twitter -twitter = twitter.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(authtype="Basic", username="example", password="example") mentions = twitter.getUserMentions(count="150") print mentions diff --git a/examples/shorten_url.py b/examples/shorten_url.py index 12b7668..2a744d6 100644 --- a/examples/shorten_url.py +++ b/examples/shorten_url.py @@ -1,7 +1,7 @@ -import tango +import twython # Shortening URLs requires no authentication, huzzah -twitter = tango.setup() +twitter = twython.setup() shortURL = twitter.shortenURL("http://www.webs.com/") print shortURL diff --git a/examples/tango_setup.py b/examples/tango_setup.py deleted file mode 100644 index 56d2429..0000000 --- a/examples/tango_setup.py +++ /dev/null @@ -1,11 +0,0 @@ -import tango - -# Using no authentication and specifying Debug -twitter = tango.setup(debug=True) - -# Using Basic Authentication -twitter = tango.setup(authtype="Basic", username="example", password="example") - -# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) -auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} -twitter = tango.setup(username="example", password="example", oauth_keys=auth_keys) From 601bb0246a082ad28eac88629dc06084fae20e8a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 16 Oct 2009 03:01:28 -0400 Subject: [PATCH 125/687] twython.reportSpam() is now included, and it *should* work, but for some reason I'm getting constant 404's at the moment whenever I try to use it (even outside of Twython, the calls seem to fail...). This'll be migrated to Twython3k once I'm sure it's actually working - would love for people to test this out and make sure I'm not an idiot. ;) --- twython.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/twython.py b/twython.py index 53b9139..fc7bfdf 100644 --- a/twython.py +++ b/twython.py @@ -290,7 +290,37 @@ class setup: raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getUserMentions() requires you to be authenticated.") + + def reportSpam(self, id = None, user_id = None, screen_name = None): + """reportSpam(self, id), user_id, screen_name): + Report a user account to Twitter as a spam account. *One* of the following parameters is required, and + this requires that you be authenticated with a user account. + + Parameters: + id - Optional. The ID or screen_name of the user you want to report as a spammer. + user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. + """ + if self.authenticated is True: + # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. + if id is not None or user_id is not None or screen_name is not None: + try: + apiExtension = "" + if id is not None: + apiExtension = "?id=%s" % id + if user_id is not None: + apiExtension = "?user_id=%s" % `user_id` + if screen_name is not None: + apiExtension = "?screen_name=%s" % screen_name + return simplejson.load(self.opener.open("http://twitter.com/report_spam.json" + apiExtension)) + except HTTPError, e: + raise TwythonError("reportSpam() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") + else: + raise AuthError("reportSpam() requires you to be authenticated.") + def reTweet(self, id): """reTweet(id) From 2ee2c0a251f8461e538fc65c19671e76a997a6fb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 19 Oct 2009 06:32:51 -0400 Subject: [PATCH 126/687] A massive amount of code changes - Twython now supports Twitter's versioning API, and uses it by default. We default to API version 1, but this can be overridden on a class or function basis by specifying 'version=x' in the respective calls. The search.twitter methods remain largely untouched, as they still seem to be on a separate API - reportSpam() is also fixed now. Try this out, and feel free to open any tickets in the Issues tracker if you find anything. --- twython.py | 378 +++++++++++++++++++++++++++++++----------------- twython3k.py | 400 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 518 insertions(+), 260 deletions(-) diff --git a/twython.py b/twython.py index fc7bfdf..9dba974 100644 --- a/twython.py +++ b/twython.py @@ -57,7 +57,7 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): + def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -69,14 +69,17 @@ class setup: consumer_key - Consumer key, if you want OAuth. signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. + version - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + + ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ self.authenticated = False self.username = username # OAuth specific variables below - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' + self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version + self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version + self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version + self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.request_token = None @@ -84,23 +87,24 @@ class setup: self.consumer = None self.connection = None self.signature_method = None + self.apiVersion = version # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, password) + self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib2.build_opener(self.handler) if headers is not None: self.opener.addheaders = [('User-agent', headers)] try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) self.authenticated = True except HTTPError, e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = httplib.HTTPSConnection(SERVER) + self.connection = httplib.HTTPSConnection("http://api.twitter.com") pass else: pass @@ -161,7 +165,7 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - def getRateLimitStatus(self, rate_for = "requestingIP"): + def getRateLimitStatus(self, rate_for = "requestingIP", version = None): """getRateLimitStatus() Returns the remaining number of API requests available to the requesting user before the @@ -169,30 +173,39 @@ class setup: the rate limit. If authentication credentials are provided, the rate limit status for the authenticating user is returned. Otherwise, the rate limit status for the requesting IP address is returned. + + Params: + rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) + version - 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 = version or self.apiVersion try: if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - def getPublicTimeline(self): + def getPublicTimeline(self, version = None): """getPublicTimeline() Returns the 20 most recent statuses from non-protected users who have set a custom user icon. The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + + Params: + version - 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 = version or self.apiVersion try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) except HTTPError, e: raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) - def getHomeTimeline(self, **kwargs): + def getHomeTimeline(self, version = None, **kwargs): """getHomeTimeline(**kwargs) Returns the 20 most recent statuses, including retweets, posted by the authenticating user @@ -208,17 +221,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) return simplejson.load(self.opener.open(homeTimelineURL)) except HTTPError, e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) else: raise AuthError("getHomeTimeline() requires you to be authenticated.") - def getFriendsTimeline(self, **kwargs): + def getFriendsTimeline(self, version = None, **kwargs): """getFriendsTimeline(**kwargs) Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. @@ -229,17 +244,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: raise AuthError("getFriendsTimeline() requires you to be authenticated.") - def getUserTimeline(self, id = None, **kwargs): + def getUserTimeline(self, id = None, version = None, **kwargs): """getUserTimeline(id = None, **kwargs) Returns the 20 most recent statuses posted from the authenticating user. It's also @@ -254,13 +271,15 @@ class setup: max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % `id`, kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, `id`), kwargs) elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) try: # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user if self.authenticated is True: @@ -271,7 +290,7 @@ class setup: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % `e.code`, e.code) - def getUserMentions(self, **kwargs): + def getUserMentions(self, version = None, **kwargs): """getUserMentions(**kwargs) Returns the 20 most recent mentions (status containing @username) for the authenticating user. @@ -281,17 +300,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getUserMentions() requires you to be authenticated.") - def reportSpam(self, id = None, user_id = None, screen_name = None): + def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): """reportSpam(self, id), user_id, screen_name): Report a user account to Twitter as a spam account. *One* of the following parameters is required, and @@ -301,19 +322,21 @@ class setup: id - Optional. The ID or screen_name of the user you want to report as a spammer. user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. if id is not None or user_id is not None or screen_name is not None: try: apiExtension = "" if id is not None: - apiExtension = "?id=%s" % id + apiExtension = "id=%s" % id if user_id is not None: - apiExtension = "?user_id=%s" % `user_id` + apiExtension = "user_id=%s" % `user_id` if screen_name is not None: - apiExtension = "?screen_name=%s" % screen_name - return simplejson.load(self.opener.open("http://twitter.com/report_spam.json" + apiExtension)) + apiExtension = "screen_name=%s" % screen_name + return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) except HTTPError, e: raise TwythonError("reportSpam() failed with a %s error code." % `e.code`, e.code) else: @@ -321,33 +344,37 @@ class setup: else: raise AuthError("reportSpam() requires you to be authenticated.") - def reTweet(self, id): + def reTweet(self, id, version = None): """reTweet(id) Retweets a tweet. Requires the id parameter of the tweet you are retweeting. Parameters: id - Required. The numerical ID of the tweet you are retweeting. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % `id`, "POST")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, `id`), "POST")) except HTTPError, e: raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - def getRetweets(self, id, count = None): + def getRetweets(self, id, count = None, version = None): """ getRetweets(self, id, count): Returns up to 100 of the first retweets of a given tweet. Parameters: id - Required. The numerical ID of the tweet you want the retweets of. - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/statuses/retweets/%s.json" % `id` + apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, `id`) if count is not None: apiURL += "?count=%s" % `count` try: @@ -357,7 +384,7 @@ class setup: else: raise AuthError("getRetweets() requires you to be authenticated.") - def retweetedOfMe(self, **kwargs): + def retweetedOfMe(self, version = None, **kwargs): """retweetedOfMe(**kwargs) Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. @@ -367,17 +394,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError, e: raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - def retweetedByMe(self, **kwargs): + def retweetedByMe(self, version = None, **kwargs): """retweetedByMe(**kwargs) Returns the 20 most recent retweets posted by the authenticating user. @@ -387,17 +416,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError, e: raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - def retweetedToMe(self, **kwargs): + def retweetedToMe(self, version = None, **kwargs): """retweetedToMe(**kwargs) Returns the 20 most recent retweets posted by the authenticating user's friends. @@ -407,17 +438,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError, e: raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - def showUser(self, id = None, user_id = None, screen_name = None): + def showUser(self, id = None, user_id = None, screen_name = None, version = None): """showUser(id = None, user_id = None, screen_name = None) Returns extended information of a given user. The author's most recent status will be returned inline. @@ -427,6 +460,7 @@ class setup: id - The ID or screen name of a user. user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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. Usage Notes: Requests for protected users without credentials from @@ -435,13 +469,14 @@ class setup: ...will result in only publicly available data being returned. """ + version = version or self.apiVersion apiURL = "" if id is not None: - apiURL = "http://twitter.com/users/show/%s.json" % id + apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) if apiURL != "": try: if self.authenticated is True: @@ -451,7 +486,7 @@ class setup: except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1"): + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. @@ -469,15 +504,17 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/statuses/friends/%s.json" % id + apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) try: if page is not None: return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) @@ -488,7 +525,7 @@ class setup: else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns the authenticating user's followers, each with current status inline. @@ -506,18 +543,20 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/statuses/followers/%s.json" % id + apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) try: if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) else: return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError, e: @@ -525,7 +564,7 @@ class setup: else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - def showStatus(self, id): + def showStatus(self, id, version = None): """showStatus(id) Returns a single status, specified by the id parameter below. @@ -533,17 +572,19 @@ class setup: Parameters: id - Required. The numerical ID of the status to retrieve. + version - 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 = version or self.apiVersion try: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) except HTTPError, e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) - def updateStatus(self, status, in_reply_to_status_id = None): + def updateStatus(self, status, in_reply_to_status_id = None, version = None): """updateStatus(status, in_reply_to_status_id = None) Updates the authenticating user's status. Requires the status parameter specified below. @@ -552,19 +593,21 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + version - 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is the author of the referenced tweet, within the update. """ + version = version or self.apiVersion if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError, e: raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - def destroyStatus(self, id): + def destroyStatus(self, id, version = None): """destroyStatus(id) Destroys the status specified by the required ID parameter. @@ -572,31 +615,37 @@ class setup: Parameters: id - Required. The ID of the status to destroy. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, `id`), "DELETE")) except HTTPError, e: raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyStatus() requires you to be authenticated.") - def endSession(self): + def endSession(self, version = None): """endSession() Ends the session of the authenticating user, returning a null cookie. Use this method to sign users out of client-facing applications (widgets, etc). + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - self.opener.open("http://twitter.com/account/end_session.json", "") + self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") self.authenticated = False except HTTPError, e: raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You can't end a session when you're not authenticated to begin with.") - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") Returns a list of the 20 most recent direct messages sent to the authenticating user. @@ -606,9 +655,11 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=%s" % `page` + apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, `page`) if since_id is not None: apiURL += "&since_id=%s" % `since_id` if max_id is not None: @@ -623,7 +674,7 @@ class setup: else: raise AuthError("getDirectMessages() requires you to be authenticated.") - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): """getSentMessages(since_id = None, max_id = None, count = None, page = "1") Returns a list of the 20 most recent direct messages sent by the authenticating user. @@ -633,9 +684,11 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % `page` + apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, `page`) if since_id is not None: apiURL += "&since_id=%s" % `since_id` if max_id is not None: @@ -650,7 +703,7 @@ class setup: else: raise AuthError("getSentMessages() requires you to be authenticated.") - def sendDirectMessage(self, user, text): + def sendDirectMessage(self, user, text, version = None): """sendDirectMessage(user, text) Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. @@ -659,11 +712,13 @@ class setup: Parameters: user - Required. The ID or screen name of the recipient user. text - Required. The text of your direct message. Be sure to keep it under 140 characters. + version - 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 = version or self.apiVersion if self.authenticated is True: if len(list(text)) < 140: try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.urlencode({"user": user, "text": text})) except HTTPError, e: raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) else: @@ -671,7 +726,7 @@ class setup: else: raise AuthError("You must be authenticated to send a new direct message.") - def destroyDirectMessage(self, id): + def destroyDirectMessage(self, id, version = None): """destroyDirectMessage(id) Destroys the direct message specified in the required ID parameter. @@ -679,16 +734,18 @@ class setup: Parameters: id - Required. The ID of the direct message to destroy. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") except HTTPError, e: raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You must be authenticated to destroy a direct message.") - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") Allows the authenticating users to follow the user specified in the ID parameter. @@ -702,7 +759,9 @@ class setup: user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. follow - Optional. Enable notifications for the target user in addition to becoming friends. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if user_id is not None: @@ -711,9 +770,9 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % id, "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) except HTTPError, e: # Rate limiting is done differently here for API reasons... if e.code == 403: @@ -722,7 +781,7 @@ class setup: else: raise AuthError("createFriendship() requires you to be authenticated.") - def destroyFriendship(self, id = None, user_id = None, screen_name = None): + def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): """destroyFriendship(id = None, user_id = None, screen_name = None) Allows the authenticating users to unfollow the user specified in the ID parameter. @@ -733,7 +792,9 @@ class setup: id - Required. The ID or screen name of the user to unfollow. user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if user_id is not None: @@ -742,15 +803,15 @@ class setup: apiURL = "?screen_name=%s" % screen_name try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % `id`, "lol=1")) # Random string appended for POST reasons, quick hack ;P + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, `id`), "lol=1")) # Random string hack for POST reasons ;P else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) except HTTPError, e: raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyFriendship() requires you to be authenticated.") - def checkIfFriendshipExists(self, user_a, user_b): + def checkIfFriendshipExists(self, user_a, user_b, version = None): """checkIfFriendshipExists(user_a, user_b) Tests for the existence of friendship between two users. @@ -759,17 +820,19 @@ class setup: Parameters: user_a - Required. The ID or screen_name of the subject user. user_b - Required. The ID or screen_name of the user to test for following. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.urlencode({"user_a": user_a, "user_b": user_b}) + friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.urlencode({"user_a": user_a, "user_b": user_b})) return simplejson.load(self.opener.open(friendshipURL)) except HTTPError, e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): """showFriendship(source_id, source_screen_name, target_id, target_screen_name) Returns detailed information about the relationship between two users. @@ -782,8 +845,11 @@ class setup: ** Note: One of the following is required at all times target_id - The user_id of the target user. target_screen_name - The screen_name of the target user. + + version - 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. """ - apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D if source_id is not None: apiURL += "&source_id=%s" % `source_id` if source_screen_name is not None: @@ -803,7 +869,7 @@ class setup: raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) - def updateDeliveryDevice(self, device_name = "none"): + def updateDeliveryDevice(self, device_name = "none", version = None): """updateDeliveryDevice(device_name = "none") Sets which device Twitter delivers updates to for the authenticating user. @@ -811,19 +877,21 @@ class setup: Parameters: device - Required. Must be one of: sms, im, none. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": self.unicode2utf8(device_name)})) + return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.urlencode({"device": self.unicode2utf8(device_name)})) except HTTPError, e: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - def updateProfileColors(self, **kwargs): + def updateProfileColors(self, version = None, **kwargs): """updateProfileColors(**kwargs) - Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. Parameters: ** Note: One or more of the following parameters must be present. Each parameter's value must @@ -834,16 +902,19 @@ class setup: profile_link_color - Optional. profile_sidebar_fill_color - Optional. profile_sidebar_border_color - Optional. + + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) except HTTPError, e: raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): """updateProfile(name = None, email = None, url = None, location = None, description = None) Sets values that users are able to set under the "Account" tab of their settings page. @@ -858,7 +929,10 @@ class setup: url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. description - Optional. Maximum of 160 characters. + + version - 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 = version or self.apiVersion if self.authenticated is True: useAmpersands = False updateProfileQueryString = "" @@ -906,61 +980,67 @@ class setup: if updateProfileQueryString != "": try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) except HTTPError, e: raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateProfile() requires you to be authenticated.") - def getFavorites(self, page = "1"): + def getFavorites(self, page = "1", version = None): """getFavorites(page = "1") Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. Parameters: page - Optional. Specifies the page of favorites to retrieve. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % `page`)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, `page`))) except HTTPError, e: raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getFavorites() requires you to be authenticated.") - def createFavorite(self, id): + def createFavorite(self, id, version = None): """createFavorite(id) Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. Parameters: id - Required. The ID of the status to favorite. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % `id`, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, `id`), "")) except HTTPError, e: raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createFavorite() requires you to be authenticated.") - def destroyFavorite(self, id): + def destroyFavorite(self, id, version = None): """destroyFavorite(id) Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. Parameters: id - Required. The ID of the status to un-favorite. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % `id`, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyFavorite() requires you to be authenticated.") - def notificationFollow(self, id = None, user_id = None, screen_name = None): + def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): """notificationFollow(id = None, user_id = None, screen_name = None) Enables device notifications for updates from the specified user. Returns the specified user when successful. @@ -970,15 +1050,17 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/follow/%s.json" % id + apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: @@ -986,7 +1068,7 @@ class setup: else: raise AuthError("notificationFollow() requires you to be authenticated.") - def notificationLeave(self, id = None, user_id = None, screen_name = None): + def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): """notificationLeave(id = None, user_id = None, screen_name = None) Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. @@ -996,15 +1078,17 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/leave/%s.json" % id + apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: @@ -1012,7 +1096,7 @@ class setup: else: raise AuthError("notificationLeave() requires you to be authenticated.") - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user the specified user is following. @@ -1026,23 +1110,25 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion apiURL = "" breakResults = "cursor=%s" % cursor if page is not None: breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/friends/ids/%s.json?%s" %(id, breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=%s&%s" %(`user_id`, breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&%s" %(screen_name, breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user following the specified user. @@ -1056,23 +1142,25 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion apiURL = "" breakResults = "cursor=%s" % cursor if page is not None: breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/followers/ids/%s.json?%s" %(`id`, breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, `id`, breakResults) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=%s&%s" %(`user_id`, breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&%s" %(screen_name, breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - def createBlock(self, id): + def createBlock(self, id, version = None): """createBlock(id) Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. @@ -1080,16 +1168,18 @@ class setup: Parameters: id - The ID or screen name of a user to block. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % `id`, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, `id`), "")) except HTTPError, e: raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createBlock() requires you to be authenticated.") - def destroyBlock(self, id): + def destroyBlock(self, id, version = None): """destroyBlock(id) Un-blocks the user specified in the ID parameter for the authenticating user. @@ -1097,16 +1187,18 @@ class setup: Parameters: id - Required. The ID or screen_name of the user to un-block + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % `id`, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroyBlock() requires you to be authenticated.") - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): """checkIfBlockExists(id = None, user_id = None, screen_name = None) Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and @@ -1117,43 +1209,51 @@ class setup: id - Optional. The ID or screen_name of the potentially blocked user. user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion apiURL = "" if id is not None: - apiURL = "http://twitter.com/blocks/exists/%s.json" % `id` + apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, `id`) if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % `user_id` + apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - def getBlocking(self, page = "1"): + def getBlocking(self, page = "1", version = None): """getBlocking(page = "1") Returns an array of user objects that the authenticating user is blocking. Parameters: page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % `page`)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, `page`))) except HTTPError, e: raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getBlocking() requires you to be authenticated") - def getBlockedIDs(self): + def getBlockedIDs(self, version = None): """getBlockedIDs() Returns an array of numeric user ids the authenticating user is blocking. + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) except HTTPError, e: raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: @@ -1255,52 +1355,60 @@ class setup: except HTTPError, e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - def getSavedSearches(self): + def getSavedSearches(self, version = None): """getSavedSearches() Returns the authenticated user's saved search queries. + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) except HTTPError, e: raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("getSavedSearches() requires you to be authenticated.") - def showSavedSearch(self, id): + def showSavedSearch(self, id, version = None): """showSavedSearch(id) Retrieve the data for a saved search owned by the authenticating user specified by the given id. Parameters: id - Required. The id of the saved search to be retrieved. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % `id`)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, `id`))) except HTTPError, e: raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("showSavedSearch() requires you to be authenticated.") - def createSavedSearch(self, query): + def createSavedSearch(self, query, version = None): """createSavedSearch(query) Creates a saved search for the authenticated user. Parameters: query - Required. The query of the search the user would like to save. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) except HTTPError, e: raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("createSavedSearch() requires you to be authenticated.") - def destroySavedSearch(self, id): + def destroySavedSearch(self, id, version = None): """destroySavedSearch(id) Destroys a saved search for the authenticated user. @@ -1308,17 +1416,19 @@ class setup: Parameters: id - Required. The id of the saved search to be deleted. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % `id`, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): + def updateProfileBackgroundImage(self, filename, tile="true", version = None): """updateProfileBackgroundImage(filename, tile="true") Updates the authenticating user's profile background image. @@ -1327,35 +1437,39 @@ class setup: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + version - 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 = version or self.apiVersion if self.authenticated is True: try: files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return self.opener.open(r).read() except HTTPError, e: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") - def updateProfileImage(self, filename): + def updateProfileImage(self, filename, version = None): """updateProfileImage(filename) Updates the authenticating user's profile image (avatar). Parameters: 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 - 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 = version or self.apiVersion if self.authenticated is True: try: files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return self.opener.open(r).read() except HTTPError, e: raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) diff --git a/twython3k.py b/twython3k.py index a4bbbbe..96fa44e 100644 --- a/twython3k.py +++ b/twython3k.py @@ -57,7 +57,7 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): + def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -69,14 +69,17 @@ class setup: consumer_key - Consumer key, if you want OAuth. signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. + version - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + + ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ self.authenticated = False self.username = username # OAuth specific variables below - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' + self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version + self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version + self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version + self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.request_token = None @@ -84,23 +87,24 @@ class setup: self.consumer = None self.connection = None self.signature_method = None + self.apiVersion = version # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, password) + self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) self.opener = urllib.request.build_opener(self.handler) if headers is not None: self.opener.addheaders = [('User-agent', headers)] try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) + simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) self.authenticated = True except HTTPError as e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) elif consumer_secret is not None and consumer_key is not None: self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = http.client.HTTPSConnection(SERVER) + self.connection = http.client.HTTPSConnection("http://api.twitter.com") pass else: pass @@ -161,7 +165,7 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) - def getRateLimitStatus(self, rate_for = "requestingIP"): + def getRateLimitStatus(self, rate_for = "requestingIP", version = None): """getRateLimitStatus() Returns the remaining number of API requests available to the requesting user before the @@ -169,30 +173,39 @@ class setup: the rate limit. If authentication credentials are provided, the rate limit status for the authenticating user is returned. Otherwise, the rate limit status for the requesting IP address is returned. + + Params: + rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) + version - 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 = version or self.apiVersion try: if rate_for == "requestingIP": - return simplejson.load(urllib.request.urlopen("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) - def getPublicTimeline(self): + def getPublicTimeline(self, version = None): """getPublicTimeline() Returns the 20 most recent statuses from non-protected users who have set a custom user icon. The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + + Params: + version - 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 = version or self.apiVersion try: - return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/public_timeline.json")) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) except HTTPError as e: raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) - def getHomeTimeline(self, **kwargs): + def getHomeTimeline(self, version = None, **kwargs): """getHomeTimeline(**kwargs) Returns the 20 most recent statuses, including retweets, posted by the authenticating user @@ -208,17 +221,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) + homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) return simplejson.load(self.opener.open(homeTimelineURL)) except HTTPError as e: raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) else: raise AuthError("getHomeTimeline() requires you to be authenticated.") - def getFriendsTimeline(self, **kwargs): + def getFriendsTimeline(self, version = None, **kwargs): """getFriendsTimeline(**kwargs) Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. @@ -229,17 +244,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError as e: raise TwythonError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) else: raise AuthError("getFriendsTimeline() requires you to be authenticated.") - def getUserTimeline(self, id = None, **kwargs): + def getUserTimeline(self, id = None, version = None, **kwargs): """getUserTimeline(id = None, **kwargs) Returns the 20 most recent statuses posted from the authenticating user. It's also @@ -254,13 +271,15 @@ class setup: max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % repr(id), kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, repr(id)), kwargs) elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) try: # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user if self.authenticated is True: @@ -271,7 +290,7 @@ class setup: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % repr(e.code), e.code) - def getUserMentions(self, **kwargs): + def getUserMentions(self, version = None, **kwargs): """getUserMentions(**kwargs) Returns the 20 most recent mentions (status containing @username) for the authenticating user. @@ -281,43 +300,81 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError as e: raise TwythonError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getUserMentions() requires you to be authenticated.") + + def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): + """reportSpam(self, id), user_id, screen_name): - def reTweet(self, id): + Report a user account to Twitter as a spam account. *One* of the following parameters is required, and + this requires that you be authenticated with a user account. + + Parameters: + id - Optional. The ID or screen_name of the user you want to report as a spammer. + user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion + if self.authenticated is True: + # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. + if id is not None or user_id is not None or screen_name is not None: + try: + apiExtension = "" + if id is not None: + apiExtension = "id=%s" % id + if user_id is not None: + apiExtension = "user_id=%s" % repr(user_id) + if screen_name is not None: + apiExtension = "screen_name=%s" % screen_name + return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) + except HTTPError as e: + raise TwythonError("reportSpam() failed with a %s error code." % repr(e.code), e.code) + else: + raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") + else: + raise AuthError("reportSpam() requires you to be authenticated.") + + def reTweet(self, id, version = None): """reTweet(id) Retweets a tweet. Requires the id parameter of the tweet you are retweeting. Parameters: id - Required. The numerical ID of the tweet you are retweeting. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % repr(id), "POST")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, repr(id)), "POST")) except HTTPError as e: raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("reTweet() requires you to be authenticated.") - def getRetweets(self, id, count = None): - """ getAllRetweets(self, id, count): + def getRetweets(self, id, count = None, version = None): + """ getRetweets(self, id, count): Returns up to 100 of the first retweets of a given tweet. Parameters: id - Required. The numerical ID of the tweet you want the retweets of. - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/statuses/retweets/%s.json" % repr(id) + apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, repr(id)) if count is not None: apiURL += "?count=%s" % repr(count) try: @@ -327,7 +384,7 @@ class setup: else: raise AuthError("getRetweets() requires you to be authenticated.") - def retweetedOfMe(self, **kwargs): + def retweetedOfMe(self, version = None, **kwargs): """retweetedOfMe(**kwargs) Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. @@ -337,17 +394,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError as e: raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedOfMe() requires you to be authenticated.") - def retweetedByMe(self, **kwargs): + def retweetedByMe(self, version = None, **kwargs): """retweetedByMe(**kwargs) Returns the 20 most recent retweets posted by the authenticating user. @@ -357,17 +416,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError as e: raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedByMe() requires you to be authenticated.") - def retweetedToMe(self, **kwargs): + def retweetedToMe(self, version = None, **kwargs): """retweetedToMe(**kwargs) Returns the 20 most recent retweets posted by the authenticating user's friends. @@ -377,17 +438,19 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) return simplejson.load(self.opener.open(retweetURL)) except HTTPError as e: raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("retweetedToMe() requires you to be authenticated.") - def showUser(self, id = None, user_id = None, screen_name = None): + def showUser(self, id = None, user_id = None, screen_name = None, version = None): """showUser(id = None, user_id = None, screen_name = None) Returns extended information of a given user. The author's most recent status will be returned inline. @@ -397,6 +460,7 @@ class setup: id - The ID or screen name of a user. user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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. Usage Notes: Requests for protected users without credentials from @@ -405,13 +469,14 @@ class setup: ...will result in only publicly available data being returned. """ + version = version or self.apiVersion apiURL = "" if id is not None: - apiURL = "http://twitter.com/users/show/%s.json" % id + apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/users/show.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) if apiURL != "": try: if self.authenticated is True: @@ -421,7 +486,7 @@ class setup: except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1"): + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. @@ -439,15 +504,17 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/statuses/friends/%s.json" % id + apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) try: if page is not None: return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) @@ -458,7 +525,7 @@ class setup: else: raise AuthError("getFriendsStatus() requires you to be authenticated.") - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns the authenticating user's followers, each with current status inline. @@ -476,18 +543,20 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/statuses/followers/%s.json" % id + apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) try: if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) + return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) else: return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) except HTTPError as e: @@ -495,7 +564,7 @@ class setup: else: raise AuthError("getFollowersStatus() requires you to be authenticated.") - def showStatus(self, id): + def showStatus(self, id, version = None): """showStatus(id) Returns a single status, specified by the id parameter below. @@ -503,17 +572,19 @@ class setup: Parameters: id - Required. The numerical ID of the status to retrieve. + version - 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 = version or self.apiVersion try: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) else: - return simplejson.load(urllib.request.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) except HTTPError as e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) - def updateStatus(self, status, in_reply_to_status_id = None): + def updateStatus(self, status, in_reply_to_status_id = None, version = None): """updateStatus(status, in_reply_to_status_id = None) Updates the authenticating user's status. Requires the status parameter specified below. @@ -522,19 +593,21 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + version - 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is the author of the referenced tweet, within the update. """ + version = version or self.apiVersion if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.parse.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.parse.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) except HTTPError as e: raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) - def destroyStatus(self, id): + def destroyStatus(self, id, version = None): """destroyStatus(id) Destroys the status specified by the required ID parameter. @@ -542,31 +615,37 @@ class setup: Parameters: id - Required. The ID of the status to destroy. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % repr(id), "DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, repr(id)), "DELETE")) except HTTPError as e: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyStatus() requires you to be authenticated.") - def endSession(self): + def endSession(self, version = None): """endSession() Ends the session of the authenticating user, returning a null cookie. Use this method to sign users out of client-facing applications (widgets, etc). + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - self.opener.open("http://twitter.com/account/end_session.json", "") + self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") self.authenticated = False except HTTPError as e: raise TwythonError("endSession failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You can't end a session when you're not authenticated to begin with.") - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") Returns a list of the 20 most recent direct messages sent to the authenticating user. @@ -576,9 +655,11 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=%s" % repr(page) + apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, repr(page)) if since_id is not None: apiURL += "&since_id=%s" % repr(since_id) if max_id is not None: @@ -593,7 +674,7 @@ class setup: else: raise AuthError("getDirectMessages() requires you to be authenticated.") - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): """getSentMessages(since_id = None, max_id = None, count = None, page = "1") Returns a list of the 20 most recent direct messages sent by the authenticating user. @@ -603,9 +684,11 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + version - 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % repr(page) + apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, repr(page)) if since_id is not None: apiURL += "&since_id=%s" % repr(since_id) if max_id is not None: @@ -620,7 +703,7 @@ class setup: else: raise AuthError("getSentMessages() requires you to be authenticated.") - def sendDirectMessage(self, user, text): + def sendDirectMessage(self, user, text, version = None): """sendDirectMessage(user, text) Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. @@ -629,11 +712,13 @@ class setup: Parameters: user - Required. The ID or screen name of the recipient user. text - Required. The text of your direct message. Be sure to keep it under 140 characters. + version - 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 = version or self.apiVersion if self.authenticated is True: if len(list(text)) < 140: try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.parse.urlencode({"user": user, "text": text})) + return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.parse.urlencode({"user": user, "text": text})) except HTTPError as e: raise TwythonError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: @@ -641,7 +726,7 @@ class setup: else: raise AuthError("You must be authenticated to send a new direct message.") - def destroyDirectMessage(self, id): + def destroyDirectMessage(self, id, version = None): """destroyDirectMessage(id) Destroys the direct message specified in the required ID parameter. @@ -649,16 +734,18 @@ class setup: Parameters: id - Required. The ID of the direct message to destroy. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") except HTTPError as e: raise TwythonError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You must be authenticated to destroy a direct message.") - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") Allows the authenticating users to follow the user specified in the ID parameter. @@ -672,7 +759,9 @@ class setup: user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. follow - Optional. Enable notifications for the target user in addition to becoming friends. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if user_id is not None: @@ -681,9 +770,9 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % id, "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) except HTTPError as e: # Rate limiting is done differently here for API reasons... if e.code == 403: @@ -692,7 +781,7 @@ class setup: else: raise AuthError("createFriendship() requires you to be authenticated.") - def destroyFriendship(self, id = None, user_id = None, screen_name = None): + def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): """destroyFriendship(id = None, user_id = None, screen_name = None) Allows the authenticating users to unfollow the user specified in the ID parameter. @@ -703,7 +792,9 @@ class setup: id - Required. The ID or screen name of the user to unfollow. user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if user_id is not None: @@ -712,15 +803,15 @@ class setup: apiURL = "?screen_name=%s" % screen_name try: if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % repr(id), "lol=1")) # Random string appended for POST reasons, quick hack ;P + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, repr(id)), "lol=1")) # Random string hack for POST reasons ;P else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) except HTTPError as e: raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyFriendship() requires you to be authenticated.") - def checkIfFriendshipExists(self, user_a, user_b): + def checkIfFriendshipExists(self, user_a, user_b, version = None): """checkIfFriendshipExists(user_a, user_b) Tests for the existence of friendship between two users. @@ -729,17 +820,19 @@ class setup: Parameters: user_a - Required. The ID or screen_name of the subject user. user_b - Required. The ID or screen_name of the user to test for following. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.parse.urlencode({"user_a": user_a, "user_b": user_b}) + friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.parse.urlencode({"user_a": user_a, "user_b": user_b})) return simplejson.load(self.opener.open(friendshipURL)) except HTTPError as e: raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): """showFriendship(source_id, source_screen_name, target_id, target_screen_name) Returns detailed information about the relationship between two users. @@ -752,8 +845,11 @@ class setup: ** Note: One of the following is required at all times target_id - The user_id of the target user. target_screen_name - The screen_name of the target user. + + version - 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. """ - apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D if source_id is not None: apiURL += "&source_id=%s" % repr(source_id) if source_screen_name is not None: @@ -773,7 +869,7 @@ class setup: raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") raise TwythonError("showFriendship() failed with a %s error code." % repr(e.code), e.code) - def updateDeliveryDevice(self, device_name = "none"): + def updateDeliveryDevice(self, device_name = "none", version = None): """updateDeliveryDevice(device_name = "none") Sets which device Twitter delivers updates to for the authenticating user. @@ -781,19 +877,21 @@ class setup: Parameters: device - Required. Must be one of: sms, im, none. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.parse.urlencode({"device": self.unicode2utf8(device_name)})) + return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.parse.urlencode({"device": self.unicode2utf8(device_name)})) except HTTPError as e: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - def updateProfileColors(self, **kwargs): + def updateProfileColors(self, version = None, **kwargs): """updateProfileColors(**kwargs) - Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. + Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. Parameters: ** Note: One or more of the following parameters must be present. Each parameter's value must @@ -804,16 +902,19 @@ class setup: profile_link_color - Optional. profile_sidebar_fill_color - Optional. profile_sidebar_border_color - Optional. + + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) + return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) except HTTPError as e: raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): """updateProfile(name = None, email = None, url = None, location = None, description = None) Sets values that users are able to set under the "Account" tab of their settings page. @@ -828,7 +929,10 @@ class setup: url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. description - Optional. Maximum of 160 characters. + + version - 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 = version or self.apiVersion if self.authenticated is True: useAmpersands = False updateProfileQueryString = "" @@ -876,61 +980,67 @@ class setup: if updateProfileQueryString != "": try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) except HTTPError as e: raise TwythonError("updateProfile() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateProfile() requires you to be authenticated.") - def getFavorites(self, page = "1"): + def getFavorites(self, page = "1", version = None): """getFavorites(page = "1") Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. Parameters: page - Optional. Specifies the page of favorites to retrieve. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % repr(page))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, repr(page)))) except HTTPError as e: raise TwythonError("getFavorites() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getFavorites() requires you to be authenticated.") - def createFavorite(self, id): + def createFavorite(self, id, version = None): """createFavorite(id) Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. Parameters: id - Required. The ID of the status to favorite. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % repr(id), "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, repr(id)), "")) except HTTPError as e: raise TwythonError("createFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createFavorite() requires you to be authenticated.") - def destroyFavorite(self, id): + def destroyFavorite(self, id, version = None): """destroyFavorite(id) Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. Parameters: id - Required. The ID of the status to un-favorite. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % repr(id), "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, repr(id)), "")) except HTTPError as e: raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyFavorite() requires you to be authenticated.") - def notificationFollow(self, id = None, user_id = None, screen_name = None): + def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): """notificationFollow(id = None, user_id = None, screen_name = None) Enables device notifications for updates from the specified user. Returns the specified user when successful. @@ -940,15 +1050,17 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/follow/%s.json" % id + apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: @@ -956,7 +1068,7 @@ class setup: else: raise AuthError("notificationFollow() requires you to be authenticated.") - def notificationLeave(self, id = None, user_id = None, screen_name = None): + def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): """notificationLeave(id = None, user_id = None, screen_name = None) Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. @@ -966,15 +1078,17 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/leave/%s.json" % id + apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError as e: @@ -982,7 +1096,7 @@ class setup: else: raise AuthError("notificationLeave() requires you to be authenticated.") - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user the specified user is following. @@ -996,23 +1110,25 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion apiURL = "" breakResults = "cursor=%s" % cursor if page is not None: breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/friends/ids/%s.json?%s" %(id, breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=%s&%s" %(repr(user_id), breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, repr(user_id), breakResults) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&%s" %(screen_name, breakResults) + apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1"): + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") Returns an array of numeric IDs for every user following the specified user. @@ -1026,23 +1142,25 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion apiURL = "" breakResults = "cursor=%s" % cursor if page is not None: breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/followers/ids/%s.json?%s" %(repr(id), breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, repr(id), breakResults) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=%s&%s" %(repr(user_id), breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, repr(user_id), breakResults) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&%s" %(screen_name, breakResults) + apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) - def createBlock(self, id): + def createBlock(self, id, version = None): """createBlock(id) Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. @@ -1050,16 +1168,18 @@ class setup: Parameters: id - The ID or screen name of a user to block. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % repr(id), "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, repr(id)), "")) except HTTPError as e: raise TwythonError("createBlock() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createBlock() requires you to be authenticated.") - def destroyBlock(self, id): + def destroyBlock(self, id, version = None): """destroyBlock(id) Un-blocks the user specified in the ID parameter for the authenticating user. @@ -1067,16 +1187,18 @@ class setup: Parameters: id - Required. The ID or screen_name of the user to un-block + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % repr(id), "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, repr(id)), "")) except HTTPError as e: raise TwythonError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroyBlock() requires you to be authenticated.") - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): """checkIfBlockExists(id = None, user_id = None, screen_name = None) Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and @@ -1087,43 +1209,51 @@ class setup: id - Optional. The ID or screen_name of the potentially blocked user. user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + version - 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 = version or self.apiVersion apiURL = "" if id is not None: - apiURL = "http://twitter.com/blocks/exists/%s.json" % repr(id) + apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, repr(id)) if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % repr(user_id) + apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, repr(user_id)) if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name + apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(urllib.request.urlopen(apiURL)) except HTTPError as e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) - def getBlocking(self, page = "1"): + def getBlocking(self, page = "1", version = None): """getBlocking(page = "1") Returns an array of user objects that the authenticating user is blocking. Parameters: page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % repr(page))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, repr(page)))) except HTTPError as e: raise TwythonError("getBlocking() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getBlocking() requires you to be authenticated") - def getBlockedIDs(self): + def getBlockedIDs(self, version = None): """getBlockedIDs() Returns an array of numeric user ids the authenticating user is blocking. + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) except HTTPError as e: raise TwythonError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) else: @@ -1225,52 +1355,60 @@ class setup: except HTTPError as e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) - def getSavedSearches(self): + def getSavedSearches(self, version = None): """getSavedSearches() Returns the authenticated user's saved search queries. + + Parameters: + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) except HTTPError as e: raise TwythonError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("getSavedSearches() requires you to be authenticated.") - def showSavedSearch(self, id): + def showSavedSearch(self, id, version = None): """showSavedSearch(id) Retrieve the data for a saved search owned by the authenticating user specified by the given id. Parameters: id - Required. The id of the saved search to be retrieved. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % repr(id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, repr(id)))) except HTTPError as e: raise TwythonError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("showSavedSearch() requires you to be authenticated.") - def createSavedSearch(self, query): + def createSavedSearch(self, query, version = None): """createSavedSearch(query) Creates a saved search for the authenticated user. Parameters: query - Required. The query of the search the user would like to save. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) except HTTPError as e: raise TwythonError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("createSavedSearch() requires you to be authenticated.") - def destroySavedSearch(self, id): + def destroySavedSearch(self, id, version = None): """destroySavedSearch(id) Destroys a saved search for the authenticated user. @@ -1278,17 +1416,19 @@ class setup: Parameters: id - Required. The id of the saved search to be deleted. + version - 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % repr(id), "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, repr(id)), "")) except HTTPError as e: raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): + def updateProfileBackgroundImage(self, filename, tile="true", version = None): """updateProfileBackgroundImage(filename, tile="true") Updates the authenticating user's profile background image. @@ -1297,35 +1437,39 @@ class setup: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + version - 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 = version or self.apiVersion if self.authenticated is True: try: files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return self.opener.open(r).read() except HTTPError as e: raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") - def updateProfileImage(self, filename): + def updateProfileImage(self, filename, version = None): """updateProfileImage(filename) Updates the authenticating user's profile image (avatar). Parameters: 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 - 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 = version or self.apiVersion if self.authenticated is True: try: files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib.request.Request("http://twitter.com/account/update_profile_image.json", body, headers) + r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return self.opener.open(r).read() except HTTPError as e: raise TwythonError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) From 01a84f4ce00f1cddc241a5120a3bc8e6238a0f4b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 4 Nov 2009 04:12:51 -0500 Subject: [PATCH 127/687] Basic Lists API support is here; everything should work, sans DELETE calls. Twitter has apparently decided to only allow HTTP DELETE calls for certain List API methods, in contrast to the old ways where a POST would work fine as well. Not sure why they did this; yes, it's ideal, but nowhere near enough crap supports DELETE/PUT calls. At any rate, this stuff may also depend on Twitter propogating some API changes - test it out, review it, let me know what you think. This piece is going to be somewhat annoying to implement... --- twython.py | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++- twython3k.py | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 584 insertions(+), 10 deletions(-) diff --git a/twython.py b/twython.py index 9dba974..704ebed 100644 --- a/twython.py +++ b/twython.py @@ -56,6 +56,14 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) +class RequestWithMethod(urllib2.Request): + def __init__(self, method, *args, **kwargs): + self._method = method + urllib2.Request.__init__(*args, **kwargs) + + def get_method(self): + return self._method + class setup: def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) @@ -1409,7 +1417,7 @@ class setup: raise AuthError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id, version = None): - """destroySavedSearch(id) + """ destroySavedSearch(id) Destroys a saved search for the authenticated user. The search specified by id must be owned by the authenticating user. @@ -1426,10 +1434,285 @@ class setup: raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") + + def createList(self, name, mode = "public", description = "", version = None): + """ createList(self, name, mode, description, version) + + Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + Parameters: + name - Required. The name for the new list. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), + urllib.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError, e: + raise TwythonError("createList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("createList() requires you to be authenticated.") + + def updateList(self, list_id, name, mode = "public", description = "", version = None): + """ updateList(self, list_id, name, mode, description, version) + + Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look + at this... + + Parameters: + list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). + name - Required. The name of the list, possibly for renaming or such. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), + urllib.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError, e: + raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("updateList() requires you to be authenticated.") + + def showLists(self, version = None): + """ showLists(self, version) + + Show all the lists for the currently authenticated user (i.e, they own these lists). + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) + except HTTPError, e: + raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("showLists() requires you to be authenticated.") + + def getListMemberships(self, version = None): + """ getListMemberships(self, version) + + Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) + except HTTPError, e: + raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("getLists() requires you to be authenticated.") + + def deleteList(self, list_id, version = None): + """ deleteList(self, list_id, version) + + Deletes a list for the authenticating user. + + Parameters: + list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + #deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) + print "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) + except HTTPError, e: + raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("deleteList() requires you to be authenticated.") + + def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): + """ getListTimeline(self, list_id, cursor, version, **kwargs) + + Retrieves a timeline representing everyone in the list specified. + + Parameters: + list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. + Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion + try: + baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) + return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) + except HTTPError, e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) + + def getSpecificList(self, list_id, version = None): + """ getSpecificList(self, list_id, version) + + Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). + + Parameters: + list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + version - 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 = version or self.apiVersion + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + except HTTPError, e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) + + def addListMember(self, list_id, version = None): + """ addListMember(self, list_id, id, version) + + Adds a new Member (the passed in id) to the specified list. + + Parameters: + list_id - Required. The slug of the list to add the new member to. + id - Required. The ID of the user that's being added to the list. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % `id`)) + except HTTPError, e: + raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("addListMember requires you to be authenticated.") + + def getListMembers(self, list_id, version = None): + """ getListMembers(self, list_id, version = None) + + Show all members of a specified list. This method requires authentication if the list is private/protected. + + Parameters: + list_id - Required. The slug of the list to retrieve members for. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + except HTTPError, e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + + def removeListMember(self, list_id, id, version = None): + """ removeListMember(self, list_id, id, version) + + Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. + + Parameters: + list_id - Required. The slug of the list to remove the specified user from. + id - Required. The ID of the user that's being added to the list. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id)) + return simplejson.load(self.opener.open(deleteURL)) + except HTTPError, e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("removeListMember() requires you to be authenticated.") + + def isListMember(self, list_id, id, version = None): + """ isListMember(self, list_id, id, version) + + 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def subscribeToList(self, list_id, version): + """ subscribeToList(self, list_id, version) + + Subscribe the authenticated user to the list provided (must be public). + + Parameters: + list_id - Required. The list to subscribe to. + version - 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. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) + except HTTPError, e: + raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("subscribeToList() requires you to be authenticated.") + + def unsubscribeFromList(self, list_id, version): + """ unsubscribeFromList(self, list_id, version) + + Unsubscribe the authenticated user from the list in question (must be public). + + Parameters: + list_id - Required. The list to unsubscribe from. + version - 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. + """ + if self.authenticated is True: + try: + deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id)) + return simplejson.load(self.opener.open(deleteURL)) + except HTTPError, e: + raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("unsubscribeFromList() requires you to be authenticated.") + + def isListSubscriber(self, list_id, id, version = None): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + except HTTPError, e: + 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. def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """updateProfileBackgroundImage(filename, tile="true") + """ updateProfileBackgroundImage(filename, tile="true") Updates the authenticating user's profile background image. @@ -1449,12 +1732,12 @@ class setup: r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename, version = None): - """updateProfileImage(filename) + """ updateProfileImage(filename) Updates the authenticating user's profile image (avatar). @@ -1472,7 +1755,7 @@ class setup: r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) else: raise AuthError("You realize you need to be authenticated to change a profile image, right?") @@ -1498,6 +1781,10 @@ class setup: return content_type, body def get_content_type(self, filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def unicode2utf8(self, text): diff --git a/twython3k.py b/twython3k.py index 96fa44e..d5c64f2 100644 --- a/twython3k.py +++ b/twython3k.py @@ -56,6 +56,14 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) +class RequestWithMethod(urllib.request.Request): + def __init__(self, method, *args, **kwargs): + self._method = method + urllib.request.Request.__init__(*args, **kwargs) + + def get_method(self): + return self._method + class setup: def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) @@ -1409,7 +1417,7 @@ class setup: raise AuthError("createSavedSearch() requires you to be authenticated.") def destroySavedSearch(self, id, version = None): - """destroySavedSearch(id) + """ destroySavedSearch(id) Destroys a saved search for the authenticated user. The search specified by id must be owned by the authenticating user. @@ -1426,10 +1434,285 @@ class setup: raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("destroySavedSearch() requires you to be authenticated.") + + def createList(self, name, mode = "public", description = "", version = None): + """ createList(self, name, mode, description, version) + + Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + Parameters: + name - Required. The name for the new list. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), + urllib.parse.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError as e: + raise TwythonError("createList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("createList() requires you to be authenticated.") + + def updateList(self, list_id, name, mode = "public", description = "", version = None): + """ updateList(self, list_id, name, mode, description, version) + + Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look + at this... + + Parameters: + list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). + name - Required. The name of the list, possibly for renaming or such. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), + urllib.parse.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError as e: + raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("updateList() requires you to be authenticated.") + + def showLists(self, version = None): + """ showLists(self, version) + + Show all the lists for the currently authenticated user (i.e, they own these lists). + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) + except HTTPError as e: + raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("showLists() requires you to be authenticated.") + + def getListMemberships(self, version = None): + """ getListMemberships(self, version) + + Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) + except HTTPError as e: + raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("getLists() requires you to be authenticated.") + + def deleteList(self, list_id, version = None): + """ deleteList(self, list_id, version) + + Deletes a list for the authenticating user. + + Parameters: + list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + #deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) + print("http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) + except HTTPError as e: + raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("deleteList() requires you to be authenticated.") + + def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): + """ getListTimeline(self, list_id, cursor, version, **kwargs) + + Retrieves a timeline representing everyone in the list specified. + + Parameters: + list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. + Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + version - 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 = version or self.apiVersion + try: + baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) + return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) + except HTTPError as e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) + + def getSpecificList(self, list_id, version = None): + """ getSpecificList(self, list_id, version) + + Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). + + Parameters: + list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + version - 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 = version or self.apiVersion + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + except HTTPError as e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) + + def addListMember(self, list_id, version = None): + """ addListMember(self, list_id, id, version) + + Adds a new Member (the passed in id) to the specified list. + + Parameters: + list_id - Required. The slug of the list to add the new member to. + id - Required. The ID of the user that's being added to the list. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % repr(id))) + except HTTPError as e: + raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("addListMember requires you to be authenticated.") + + def getListMembers(self, list_id, version = None): + """ getListMembers(self, list_id, version = None) + + Show all members of a specified list. This method requires authentication if the list is private/protected. + + Parameters: + list_id - Required. The slug of the list to retrieve members for. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + except HTTPError as e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + + def removeListMember(self, list_id, id, version = None): + """ removeListMember(self, list_id, id, version) + + Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. + + Parameters: + list_id - Required. The slug of the list to remove the specified user from. + id - Required. The ID of the user that's being added to the list. + version - 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 = version or self.apiVersion + if self.authenticated is True: + try: + deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id)) + return simplejson.load(self.opener.open(deleteURL)) + except HTTPError as e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("removeListMember() requires you to be authenticated.") + + def isListMember(self, list_id, id, version = None): + """ isListMember(self, list_id, id, version) + + 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) + except HTTPError as e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def subscribeToList(self, list_id, version): + """ subscribeToList(self, list_id, version) + + Subscribe the authenticated user to the list provided (must be public). + + Parameters: + list_id - Required. The list to subscribe to. + version - 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. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) + except HTTPError as e: + raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("subscribeToList() requires you to be authenticated.") + + def unsubscribeFromList(self, list_id, version): + """ unsubscribeFromList(self, list_id, version) + + Unsubscribe the authenticated user from the list in question (must be public). + + Parameters: + list_id - Required. The list to unsubscribe from. + version - 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. + """ + if self.authenticated is True: + try: + deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id)) + return simplejson.load(self.opener.open(deleteURL)) + except HTTPError as e: + raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("unsubscribeFromList() requires you to be authenticated.") + + def isListSubscriber(self, list_id, id, version = None): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + version - 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) + except HTTPError as e: + 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. def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """updateProfileBackgroundImage(filename, tile="true") + """ updateProfileBackgroundImage(filename, tile="true") Updates the authenticating user's profile background image. @@ -1449,12 +1732,12 @@ class setup: r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) else: raise AuthError("You realize you need to be authenticated to change a background image, right?") def updateProfileImage(self, filename, version = None): - """updateProfileImage(filename) + """ updateProfileImage(filename) Updates the authenticating user's profile image (avatar). @@ -1472,7 +1755,7 @@ class setup: r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return self.opener.open(r).read() except HTTPError as e: - raise TwythonError("updateProfileImage() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) else: raise AuthError("You realize you need to be authenticated to change a profile image, right?") @@ -1498,6 +1781,10 @@ class setup: return content_type, body def get_content_type(self, filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def unicode2utf8(self, text): From 60aaba6ad76378cc52e4835b08885ed2ea09180b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 18 Nov 2009 04:55:54 -0500 Subject: [PATCH 128/687] Fixed delete methods for List API (I believe, user testing is the best method to check here). Some of the list methods were throwing incorrect openers if you didn't authenticate; those have been fixed now as well. This commit also lands support for the new Trends API from Twitter (Available/WoeID), as well as the new 'Search Users' API. These changes were also migrated to the 3k build, so test away. ;) --- twython.py | 200 ++++++++++++++++++++++++++++++++------------------- twython3k.py | 200 ++++++++++++++++++++++++++++++++------------------- 2 files changed, 256 insertions(+), 144 deletions(-) diff --git a/twython.py b/twython.py index 704ebed..438649e 100644 --- a/twython.py +++ b/twython.py @@ -77,7 +77,7 @@ class setup: consumer_key - Consumer key, if you want OAuth. signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. - version - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ @@ -184,7 +184,7 @@ class setup: Params: rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) - version - 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. """ version = version or self.apiVersion try: @@ -205,7 +205,7 @@ class setup: The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. Params: - version - 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. """ version = version or self.apiVersion try: @@ -229,7 +229,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -252,7 +252,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -279,7 +279,7 @@ class setup: max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: @@ -308,7 +308,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -330,7 +330,7 @@ class setup: id - Optional. The ID or screen_name of the user you want to report as a spammer. user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -359,7 +359,7 @@ class setup: Parameters: id - Required. The numerical ID of the tweet you are retweeting. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -378,7 +378,7 @@ class setup: Parameters: id - Required. The numerical ID of the tweet you want the retweets of. count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -402,7 +402,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -424,7 +424,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -446,7 +446,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -458,6 +458,26 @@ class setup: else: raise AuthError("retweetedToMe() requires you to be authenticated.") + def searchUsers(self, q, per_page = 20, page = 1, version = None): + """ searchUsers(q, per_page = None, page = None): + + Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) + + Parameters: + q (string) - Required. The query you wanna search against; self explanatory. ;) + per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) + page (number) - Optional, defaults to 1. The page of users you want to pull 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. + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) + except HTTPError, e: + raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("searchUsers(), oddly, requires you to be authenticated.") + def showUser(self, id = None, user_id = None, screen_name = None, version = None): """showUser(id = None, user_id = None, screen_name = None) @@ -468,7 +488,7 @@ class setup: id - The ID or screen name of a user. user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. Usage Notes: Requests for protected users without credentials from @@ -512,7 +532,7 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -551,7 +571,7 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -580,7 +600,7 @@ class setup: Parameters: id - Required. The numerical ID of the status to retrieve. - version - 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. """ version = version or self.apiVersion try: @@ -601,7 +621,7 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - version - 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is @@ -623,7 +643,7 @@ class setup: Parameters: id - Required. The ID of the status to destroy. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -641,7 +661,7 @@ class setup: Use this method to sign users out of client-facing applications (widgets, etc). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -663,7 +683,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -692,7 +712,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -720,7 +740,7 @@ class setup: Parameters: user - Required. The ID or screen name of the recipient user. text - Required. The text of your direct message. Be sure to keep it under 140 characters. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -742,7 +762,7 @@ class setup: Parameters: id - Required. The ID of the direct message to destroy. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -767,7 +787,7 @@ class setup: user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. follow - Optional. Enable notifications for the target user in addition to becoming friends. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -800,7 +820,7 @@ class setup: id - Required. The ID or screen name of the user to unfollow. user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -828,7 +848,7 @@ class setup: Parameters: user_a - Required. The ID or screen_name of the subject user. user_b - Required. The ID or screen_name of the user to test for following. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -854,7 +874,7 @@ class setup: target_id - The user_id of the target user. target_screen_name - The screen_name of the target user. - version - 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. """ version = version or self.apiVersion apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D @@ -885,7 +905,7 @@ class setup: Parameters: device - Required. Must be one of: sms, im, none. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -911,7 +931,7 @@ class setup: profile_sidebar_fill_color - Optional. profile_sidebar_border_color - Optional. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -938,7 +958,7 @@ class setup: location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. description - Optional. Maximum of 160 characters. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1001,7 +1021,7 @@ class setup: Parameters: page - Optional. Specifies the page of favorites to retrieve. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1019,7 +1039,7 @@ class setup: Parameters: id - Required. The ID of the status to favorite. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1037,7 +1057,7 @@ class setup: Parameters: id - Required. The ID of the status to un-favorite. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1058,7 +1078,7 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1086,7 +1106,7 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1118,7 +1138,7 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1150,7 +1170,7 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1176,7 +1196,7 @@ class setup: Parameters: id - The ID or screen name of a user to block. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1195,7 +1215,7 @@ class setup: Parameters: id - Required. The ID or screen_name of the user to un-block - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1217,7 +1237,7 @@ class setup: id - Optional. The ID or screen_name of the potentially blocked user. user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1239,7 +1259,7 @@ class setup: Parameters: page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1256,7 +1276,7 @@ class setup: Returns an array of numeric user ids the authenticating user is blocking. Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1369,7 +1389,7 @@ class setup: Returns the authenticated user's saved search queries. Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1387,7 +1407,7 @@ class setup: Parameters: id - Required. The id of the saved search to be retrieved. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1405,7 +1425,7 @@ class setup: Parameters: query - Required. The query of the search the user would like to save. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1424,7 +1444,7 @@ class setup: Parameters: id - Required. The id of the saved search to be deleted. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1444,7 +1464,7 @@ class setup: name - Required. The name for the new list. description - Optional, in the sense that you can leave it blank if you don't want one. ;) mode - Optional. This is a string indicating "public" or "private", defaults to "public". - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1468,7 +1488,7 @@ class setup: name - Required. The name of the list, possibly for renaming or such. description - Optional, in the sense that you can leave it blank if you don't want one. ;) mode - Optional. This is a string indicating "public" or "private", defaults to "public". - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1487,7 +1507,7 @@ class setup: (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1505,7 +1525,7 @@ class setup: (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1523,13 +1543,11 @@ class setup: Parameters: list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: try: - #deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) - print "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id) return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError, e: raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) @@ -1548,7 +1566,7 @@ class setup: count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion try: @@ -1566,11 +1584,14 @@ class setup: Parameters: list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - version - 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. """ version = version or self.apiVersion try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) except HTTPError, e: if e.code == 404: raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") @@ -1584,7 +1605,7 @@ class setup: Parameters: list_id - Required. The slug of the list to add the new member to. id - Required. The ID of the user that's being added to the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1602,7 +1623,7 @@ class setup: Parameters: list_id - Required. The slug of the list to retrieve members for. - version - 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. """ version = version or self.apiVersion try: @@ -1621,13 +1642,12 @@ class setup: Parameters: list_id - Required. The slug of the list to remove the specified user from. id - Required. The ID of the user that's being added to the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: try: - deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id)) - return simplejson.load(self.opener.open(deleteURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError, e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) else: @@ -1643,14 +1663,14 @@ class setup: Parameters: list_id - Required. The slug of the list to check against. id - Required. The ID of the user being checked in the list. - version - 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. """ version = version or self.apiVersion try: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1661,7 +1681,7 @@ class setup: Parameters: list_id - Required. The list to subscribe to. - version - 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. """ if self.authenticated is True: try: @@ -1678,12 +1698,11 @@ class setup: Parameters: list_id - Required. The list to unsubscribe from. - version - 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. """ if self.authenticated is True: try: - deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id)) - return simplejson.load(self.opener.open(deleteURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError, e: raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) else: @@ -1699,17 +1718,54 @@ class setup: Parameters: list_id - Required. The slug of the list to check against. id - Required. The ID of the user being checked in the list. - version - 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. """ version = version or self.apiVersion try: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + def availableTrends(self, latitude = None, longitude = None, version = None): + """ availableTrends(latitude, longitude, version): + + Gets all available trends, optionally filtering by geolocation based stuff. + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + latitude (string) - Optional. A latitude to sort by. + longitude (string) - Optional. A longitude to sort by. + 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 = version or self.apiVersion + try: + if latitude is not None and longitude is not None: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) + except HTTPError, e: + raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) + + def trendsByLocation(self, woeid, version = None): + """ trendsByLocation(woeid, version): + + Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + woeid (string) - Required. WoeID of the area you're searching in. + 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 = version or self.apiVersion + try: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) + except HTTPError, e: + raise TwythonError("trendsByLocation() 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. def updateProfileBackgroundImage(self, filename, tile="true", version = None): """ updateProfileBackgroundImage(filename, tile="true") @@ -1720,7 +1776,7 @@ class setup: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1743,7 +1799,7 @@ class setup: Parameters: 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 - 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. """ version = version or self.apiVersion if self.authenticated is True: diff --git a/twython3k.py b/twython3k.py index d5c64f2..aedffc1 100644 --- a/twython3k.py +++ b/twython3k.py @@ -77,7 +77,7 @@ class setup: consumer_key - Consumer key, if you want OAuth. signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. - version - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ @@ -184,7 +184,7 @@ class setup: Params: rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) - version - 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. """ version = version or self.apiVersion try: @@ -205,7 +205,7 @@ class setup: The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. Params: - version - 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. """ version = version or self.apiVersion try: @@ -229,7 +229,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -252,7 +252,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -279,7 +279,7 @@ class setup: max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: @@ -308,7 +308,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -330,7 +330,7 @@ class setup: id - Optional. The ID or screen_name of the user you want to report as a spammer. user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -359,7 +359,7 @@ class setup: Parameters: id - Required. The numerical ID of the tweet you are retweeting. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -378,7 +378,7 @@ class setup: Parameters: id - Required. The numerical ID of the tweet you want the retweets of. count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -402,7 +402,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -424,7 +424,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -446,7 +446,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -458,6 +458,26 @@ class setup: else: raise AuthError("retweetedToMe() requires you to be authenticated.") + def searchUsers(self, q, per_page = 20, page = 1, version = None): + """ searchUsers(q, per_page = None, page = None): + + Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) + + Parameters: + q (string) - Required. The query you wanna search against; self explanatory. ;) + per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) + page (number) - Optional, defaults to 1. The page of users you want to pull 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. + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) + except HTTPError as e: + raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("searchUsers(), oddly, requires you to be authenticated.") + def showUser(self, id = None, user_id = None, screen_name = None, version = None): """showUser(id = None, user_id = None, screen_name = None) @@ -468,7 +488,7 @@ class setup: id - The ID or screen name of a user. user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. Usage Notes: Requests for protected users without credentials from @@ -512,7 +532,7 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -551,7 +571,7 @@ class setup: screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -580,7 +600,7 @@ class setup: Parameters: id - Required. The numerical ID of the status to retrieve. - version - 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. """ version = version or self.apiVersion try: @@ -601,7 +621,7 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - version - 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is @@ -623,7 +643,7 @@ class setup: Parameters: id - Required. The ID of the status to destroy. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -641,7 +661,7 @@ class setup: Use this method to sign users out of client-facing applications (widgets, etc). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -663,7 +683,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -692,7 +712,7 @@ class setup: max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -720,7 +740,7 @@ class setup: Parameters: user - Required. The ID or screen name of the recipient user. text - Required. The text of your direct message. Be sure to keep it under 140 characters. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -742,7 +762,7 @@ class setup: Parameters: id - Required. The ID of the direct message to destroy. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -767,7 +787,7 @@ class setup: user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. follow - Optional. Enable notifications for the target user in addition to becoming friends. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -800,7 +820,7 @@ class setup: id - Required. The ID or screen name of the user to unfollow. user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -828,7 +848,7 @@ class setup: Parameters: user_a - Required. The ID or screen_name of the subject user. user_b - Required. The ID or screen_name of the user to test for following. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -854,7 +874,7 @@ class setup: target_id - The user_id of the target user. target_screen_name - The screen_name of the target user. - version - 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. """ version = version or self.apiVersion apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D @@ -885,7 +905,7 @@ class setup: Parameters: device - Required. Must be one of: sms, im, none. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -911,7 +931,7 @@ class setup: profile_sidebar_fill_color - Optional. profile_sidebar_border_color - Optional. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -938,7 +958,7 @@ class setup: location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. description - Optional. Maximum of 160 characters. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1001,7 +1021,7 @@ class setup: Parameters: page - Optional. Specifies the page of favorites to retrieve. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1019,7 +1039,7 @@ class setup: Parameters: id - Required. The ID of the status to favorite. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1037,7 +1057,7 @@ class setup: Parameters: id - Required. The ID of the status to un-favorite. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1058,7 +1078,7 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1086,7 +1106,7 @@ class setup: id - Required. The ID or screen name of the user to follow with device updates. user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1118,7 +1138,7 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1150,7 +1170,7 @@ class setup: screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1176,7 +1196,7 @@ class setup: Parameters: id - The ID or screen name of a user to block. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1195,7 +1215,7 @@ class setup: Parameters: id - Required. The ID or screen_name of the user to un-block - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1217,7 +1237,7 @@ class setup: id - Optional. The ID or screen_name of the potentially blocked user. user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - version - 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. """ version = version or self.apiVersion apiURL = "" @@ -1239,7 +1259,7 @@ class setup: Parameters: page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1256,7 +1276,7 @@ class setup: Returns an array of numeric user ids the authenticating user is blocking. Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1369,7 +1389,7 @@ class setup: Returns the authenticated user's saved search queries. Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1387,7 +1407,7 @@ class setup: Parameters: id - Required. The id of the saved search to be retrieved. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1405,7 +1425,7 @@ class setup: Parameters: query - Required. The query of the search the user would like to save. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1424,7 +1444,7 @@ class setup: Parameters: id - Required. The id of the saved search to be deleted. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1444,7 +1464,7 @@ class setup: name - Required. The name for the new list. description - Optional, in the sense that you can leave it blank if you don't want one. ;) mode - Optional. This is a string indicating "public" or "private", defaults to "public". - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1468,7 +1488,7 @@ class setup: name - Required. The name of the list, possibly for renaming or such. description - Optional, in the sense that you can leave it blank if you don't want one. ;) mode - Optional. This is a string indicating "public" or "private", defaults to "public". - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1487,7 +1507,7 @@ class setup: (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1505,7 +1525,7 @@ class setup: (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). Parameters: - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1523,13 +1543,11 @@ class setup: Parameters: list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: try: - #deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) - print("http://api.twitter.com/%d/%s/lists/%s.json?_method=DELETE" % (version, self.username, list_id)) return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError as e: raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) @@ -1548,7 +1566,7 @@ class setup: count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - version - 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. """ version = version or self.apiVersion try: @@ -1566,11 +1584,14 @@ class setup: Parameters: list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - version - 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. """ version = version or self.apiVersion try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) except HTTPError as e: if e.code == 404: raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") @@ -1584,7 +1605,7 @@ class setup: Parameters: list_id - Required. The slug of the list to add the new member to. id - Required. The ID of the user that's being added to the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1602,7 +1623,7 @@ class setup: Parameters: list_id - Required. The slug of the list to retrieve members for. - version - 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. """ version = version or self.apiVersion try: @@ -1621,13 +1642,12 @@ class setup: Parameters: list_id - Required. The slug of the list to remove the specified user from. id - Required. The ID of the user that's being added to the list. - version - 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. """ version = version or self.apiVersion if self.authenticated is True: try: - deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id)) - return simplejson.load(self.opener.open(deleteURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError as e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) else: @@ -1643,14 +1663,14 @@ class setup: Parameters: list_id - Required. The slug of the list to check against. id - Required. The ID of the user being checked in the list. - version - 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. """ version = version or self.apiVersion try: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1661,7 +1681,7 @@ class setup: Parameters: list_id - Required. The list to subscribe to. - version - 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. """ if self.authenticated is True: try: @@ -1678,12 +1698,11 @@ class setup: Parameters: list_id - Required. The list to unsubscribe from. - version - 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. """ if self.authenticated is True: try: - deleteURL = RequestWithMethod("DELETE", "http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id)) - return simplejson.load(self.opener.open(deleteURL)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) except HTTPError as e: raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) else: @@ -1699,17 +1718,54 @@ class setup: Parameters: list_id - Required. The slug of the list to check against. id - Required. The ID of the user being checked in the list. - version - 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. """ version = version or self.apiVersion try: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + def availableTrends(self, latitude = None, longitude = None, version = None): + """ availableTrends(latitude, longitude, version): + + Gets all available trends, optionally filtering by geolocation based stuff. + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + latitude (string) - Optional. A latitude to sort by. + longitude (string) - Optional. A longitude to sort by. + 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 = version or self.apiVersion + try: + if latitude is not None and longitude is not None: + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) + except HTTPError as e: + raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) + + def trendsByLocation(self, woeid, version = None): + """ trendsByLocation(woeid, version): + + Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + woeid (string) - Required. WoeID of the area you're searching in. + 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 = version or self.apiVersion + try: + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) + except HTTPError as e: + raise TwythonError("trendsByLocation() 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. def updateProfileBackgroundImage(self, filename, tile="true", version = None): """ updateProfileBackgroundImage(filename, tile="true") @@ -1720,7 +1776,7 @@ class setup: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - version - 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. """ version = version or self.apiVersion if self.authenticated is True: @@ -1743,7 +1799,7 @@ class setup: Parameters: 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 - 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. """ version = version or self.apiVersion if self.authenticated is True: From 61f77252bfe772996f7b8f89234ac5136ee5598e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 20 Nov 2009 04:16:42 -0500 Subject: [PATCH 129/687] updateStatus() now supports latitude/longitude parameters for Twitter's API; OAuth functionality moved out of Twython core and into it's own module. This should solve the annoying problems people were running into with OAuth-include related problems when they never wanted/needed OAuth in the first place. --- twython.py | 91 ++++------------------------ twython3k.py | 91 ++++------------------------ twython_oauth.py | 85 ++++++++++++++++++++++++++ streaming.py => twython_streaming.py | 0 4 files changed, 111 insertions(+), 156 deletions(-) create mode 100644 twython_oauth.py rename streaming.py => twython_streaming.py (100%) diff --git a/twython.py b/twython.py index 438649e..d135f6e 100644 --- a/twython.py +++ b/twython.py @@ -10,11 +10,6 @@ Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes, mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - __author__ = "Ryan McGrath " __version__ = "0.8" @@ -31,11 +26,6 @@ except ImportError: except: raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -try: - import oauth -except ImportError: - pass - class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg @@ -56,16 +46,8 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) -class RequestWithMethod(urllib2.Request): - def __init__(self, method, *args, **kwargs): - self._method = method - urllib2.Request.__init__(*args, **kwargs) - - def get_method(self): - return self._method - class setup: - def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): + def __init__(self, username = None, password = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -73,9 +55,6 @@ class setup: Parameters: username - Your Twitter username, if you want Basic (HTTP) Authentication. password - Password for your twitter account, if you want Basic (HTTP) Authentication. - consumer_secret - Consumer secret, if you want OAuth. - consumer_key - Consumer key, if you want OAuth. - signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. @@ -83,18 +62,6 @@ class setup: """ self.authenticated = False self.username = username - # OAuth specific variables below - self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version - self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version - self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version - self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None - self.consumer = None - self.connection = None - self.signature_method = None self.apiVersion = version # Check and set up authentication if self.username is not None and password is not None: @@ -110,51 +77,9 @@ class setup: self.authenticated = True except HTTPError, e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) - elif consumer_secret is not None and consumer_key is not None: - self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = httplib.HTTPSConnection("http://api.twitter.com") - pass else: pass - def getOAuthResource(self, url, access_token, params, http_method="GET"): - """getOAuthResource(self, url, access_token, params, http_method="GET") - - Returns a signed OAuth object for use in requests. - """ - newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) - oauth_request.sign_request(self.signature_method, consumer, access_token) - return oauth_request - - def getResponse(self, oauth_request, connection): - """getResponse(self, oauth_request, connection) - - Returns a JSON-ified list of results. - """ - url = oauth_request.to_url() - connection.request(oauth_request.http_method, url) - response = connection.getresponse() - return simplejson.load(response.read()) - - def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) - oauth_request.sign_request(signature_method, consumer, None) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - - def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) - oauth_request.sign_request(signature_method, consumer, token) - return oauth_request.to_url() - - def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - # May not be needed... - self.request_token = request_token - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) - oauth_request.sign_request(signature_method, consumer, request_token) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): """shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl") @@ -612,7 +537,7 @@ class setup: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) - def updateStatus(self, status, in_reply_to_status_id = None, version = None): + def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): """updateStatus(status, in_reply_to_status_id = None) Updates the authenticating user's status. Requires the status parameter specified below. @@ -621,17 +546,27 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + latitude (string) - Optional. The location's latitude that this tweet refers to. + longitude (string) - Optional. The location's longitude that this tweet refers 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is the author of the referenced tweet, within the update. + + ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. + This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. """ version = version or self.apiVersion if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({ + "status": self.unicode2utf8(status), + "in_reply_to_status_id": in_reply_to_status_id, + "lat": latitude, + "long": longitude + }))) except HTTPError, e: raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) diff --git a/twython3k.py b/twython3k.py index aedffc1..c55b1b7 100644 --- a/twython3k.py +++ b/twython3k.py @@ -10,11 +10,6 @@ Questions, comments? ryan@venodesigns.net """ -import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools - -from urllib.parse import urlparse -from urllib.error import HTTPError - __author__ = "Ryan McGrath " __version__ = "0.8" @@ -31,11 +26,6 @@ except ImportError: except: raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -try: - from . import oauth -except ImportError: - pass - class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg @@ -56,16 +46,8 @@ class AuthError(TwythonError): def __str__(self): return repr(self.msg) -class RequestWithMethod(urllib.request.Request): - def __init__(self, method, *args, **kwargs): - self._method = method - urllib.request.Request.__init__(*args, **kwargs) - - def get_method(self): - return self._method - class setup: - def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None, version = 1): + def __init__(self, username = None, password = None, headers = None, version = 1): """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -73,9 +55,6 @@ class setup: Parameters: username - Your Twitter username, if you want Basic (HTTP) Authentication. password - Password for your twitter account, if you want Basic (HTTP) Authentication. - consumer_secret - Consumer secret, if you want OAuth. - consumer_key - Consumer key, if you want OAuth. - signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() headers - User agent header. version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. @@ -83,18 +62,6 @@ class setup: """ self.authenticated = False self.username = username - # OAuth specific variables below - self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version - self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version - self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version - self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None - self.consumer = None - self.connection = None - self.signature_method = None self.apiVersion = version # Check and set up authentication if self.username is not None and password is not None: @@ -110,51 +77,9 @@ class setup: self.authenticated = True except HTTPError as e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) - elif consumer_secret is not None and consumer_key is not None: - self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = http.client.HTTPSConnection("http://api.twitter.com") - pass else: pass - def getOAuthResource(self, url, access_token, params, http_method="GET"): - """getOAuthResource(self, url, access_token, params, http_method="GET") - - Returns a signed OAuth object for use in requests. - """ - newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) - oauth_request.sign_request(self.signature_method, consumer, access_token) - return oauth_request - - def getResponse(self, oauth_request, connection): - """getResponse(self, oauth_request, connection) - - Returns a JSON-ified list of results. - """ - url = oauth_request.to_url() - connection.request(oauth_request.http_method, url) - response = connection.getresponse() - return simplejson.load(response.read()) - - def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) - oauth_request.sign_request(signature_method, consumer, None) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - - def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) - oauth_request.sign_request(signature_method, consumer, token) - return oauth_request.to_url() - - def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - # May not be needed... - self.request_token = request_token - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) - oauth_request.sign_request(signature_method, consumer, request_token) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): """shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl") @@ -612,7 +537,7 @@ class setup: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) - def updateStatus(self, status, in_reply_to_status_id = None, version = None): + def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): """updateStatus(status, in_reply_to_status_id = None) Updates the authenticating user's status. Requires the status parameter specified below. @@ -621,17 +546,27 @@ class setup: Parameters: status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + latitude (string) - Optional. The location's latitude that this tweet refers to. + longitude (string) - Optional. The location's longitude that this tweet refers 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. ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references is mentioned within the status text. Therefore, you must include @username, where username is the author of the referenced tweet, within the update. + + ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. + This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. """ version = version or self.apiVersion if len(list(status)) > 140: raise TwythonError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.parse.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.parse.urlencode({ + "status": self.unicode2utf8(status), + "in_reply_to_status_id": in_reply_to_status_id, + "lat": latitude, + "long": longitude + }))) except HTTPError as e: raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) diff --git a/twython_oauth.py b/twython_oauth.py new file mode 100644 index 0000000..db9ed1d --- /dev/null +++ b/twython_oauth.py @@ -0,0 +1,85 @@ +#!/usr/bin/python + +""" + Twython-oauth (twyauth) is a separate library to handle OAuth routines with Twython. This currently doesn't work, as I never get the time to finish it. + Feel free to help out. + + Questions, comments? ryan@venodesigns.net +""" + +import twython, httplib, urllib, urllib2, mimetypes, mimetools + +from urlparse import urlparse +from urllib2 import HTTPError + +try: + import oauth +except ImportError: + pass + +class twyauth: + def __init__(self, username, consumer_key, consumer_secret, signature_method = None, headers = None, version = 1): + """oauth(username = None, consumer_secret = None, consumer_key = None, headers = None) + + Instantiates an instance of Twython with OAuth. Takes optional parameters for authentication and such (see below). + + Parameters: + username - Your Twitter username, if you want Basic (HTTP) Authentication. + consumer_secret - Consumer secret, given to you when you register your App with Twitter. + consumer_key - Consumer key (see situation with consumer_secret). + signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() + headers - User agent header. + version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + """ + # OAuth specific variables below + self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version + self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version + self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version + self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + self.request_token = None + self.access_token = None + self.consumer = None + self.connection = None + self.signature_method = None + self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) + self.connection = httplib.HTTPSConnection("http://api.twitter.com") + + def getOAuthResource(self, url, access_token, params, http_method="GET"): + """getOAuthResource(self, url, access_token, params, http_method="GET") + + Returns a signed OAuth object for use in requests. + """ + newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) + oauth_request.sign_request(self.signature_method, consumer, access_token) + return oauth_request + + def getResponse(self, oauth_request, connection): + """getResponse(self, oauth_request, connection) + + Returns a JSON-ified list of results. + """ + url = oauth_request.to_url() + connection.request(oauth_request.http_method, url) + response = connection.getresponse() + return simplejson.load(response.read()) + + def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) + oauth_request.sign_request(signature_method, consumer, None) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) + + def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) + oauth_request.sign_request(signature_method, consumer, token) + return oauth_request.to_url() + + def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): + # May not be needed... + self.request_token = request_token + oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) + oauth_request.sign_request(signature_method, consumer, request_token) + resp = fetch_response(oauth_request, connection) + return oauth.OAuthToken.from_string(resp) diff --git a/streaming.py b/twython_streaming.py similarity index 100% rename from streaming.py rename to twython_streaming.py From d37f91ce8eda1a32a6d741ba5e045ee8e8d319ff Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 20 Nov 2009 04:19:43 -0500 Subject: [PATCH 130/687] Accidentally commited a changeset in the last push that stripped out all the import statements. Needless to say, this was bad - fixed now. --- twython.py | 5 +++++ twython3k.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/twython.py b/twython.py index d135f6e..cf05f71 100644 --- a/twython.py +++ b/twython.py @@ -10,6 +10,11 @@ Questions, comments? ryan@venodesigns.net """ +import httplib, urllib, urllib2, mimetypes, mimetools + +from urlparse import urlparse +from urllib2 import HTTPError + __author__ = "Ryan McGrath " __version__ = "0.8" diff --git a/twython3k.py b/twython3k.py index c55b1b7..dc7026c 100644 --- a/twython3k.py +++ b/twython3k.py @@ -10,6 +10,11 @@ Questions, comments? ryan@venodesigns.net """ +import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools + +from urllib.parse import urlparse +from urllib.error import HTTPError + __author__ = "Ryan McGrath " __version__ = "0.8" From 9ca737b986dd8056ce1c159b9eadafc9597e4c19 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 23 Nov 2009 22:03:21 -0500 Subject: [PATCH 131/687] Twython 0.9 - enough has changed with the Twitter API as of late that this merits a new release. 0.8 was beginning to show age as the API moved forward, and is now deprecated as a result - 0.9 is the way to go (or trunk, if you're adventurous. ;D) --- README.markdown | 8 +- build/lib/twython.py | 1764 ++++++++++++++++++---- dist/twython-0.9.macosx-10.5-i386.tar.gz | Bin 0 -> 58118 bytes dist/twython-0.9.tar.gz | Bin 0 -> 16158 bytes dist/twython-0.9.win32.exe | Bin 0 -> 90830 bytes setup.py | 16 +- twython.egg-info/PKG-INFO | 27 +- twython.egg-info/SOURCES.txt | 1 - twython.py | 2 +- twython3k.py | 2 +- 10 files changed, 1478 insertions(+), 342 deletions(-) create mode 100644 dist/twython-0.9.macosx-10.5-i386.tar.gz create mode 100644 dist/twython-0.9.tar.gz create mode 100644 dist/twython-0.9.win32.exe diff --git a/README.markdown b/README.markdown index 336b5d1..8501a38 100644 --- a/README.markdown +++ b/README.markdown @@ -8,9 +8,9 @@ This is my first library I've ever written in Python, so there could be some stu make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, and I'm open to anything that'll improve the library as a whole. -OAuth support is in the works, but every other part of the Twitter API should be covered. Twython -handles both Basic (HTTP) Authentication and OAuth. Older versions of Twython need Basic Auth specified - -to override this, specify 'authtype="Basic"' in your twython.setup() call. +OAuth and Streaming API support is in the works, but every other part of the Twitter API should be covered. Twython +handles both Basic (HTTP) Authentication and OAuth (Older versions (pre 0.9) of Twython need Basic Auth specified - +to override this, specify 'authtype="Basic"' in your twython.setup() call). Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or Twitter's API Wiki (Twython calls mirror most of the methods listed there). @@ -27,7 +27,7 @@ Example Use ----------------------------------------------------------------------------------------------------- > import twython > -> twitter = twython.setup(authtype="Basic", username="example", password="example") +> twitter = twython.setup(username="example", password="example") > twitter.updateStatus("See how easy this was?") diff --git a/build/lib/twython.py b/build/lib/twython.py index 456b6c8..ffe31e5 100644 --- a/build/lib/twython.py +++ b/build/lib/twython.py @@ -1,8 +1,6 @@ #!/usr/bin/python """ - NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. - Twython is an up-to-date library for Python that wraps the Twitter API. Other Python Twitter libraries seem to have fallen a bit behind, and Twitter's API has evolved a bit. Here's hoping this helps. @@ -18,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.6" +__version__ = "0.9" """Twython - Easy Twitter utilities in Python""" @@ -27,15 +25,13 @@ try: except ImportError: try: import json as simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): +class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg if error_code == 400: @@ -43,114 +39,185 @@ class TangoError(Exception): def __str__(self): return repr(self.msg) -class APILimit(TangoError): +class APILimit(TwythonError): + def __init__(self, msg): + self.msg = msg + def __str__(self): + return repr(self.msg) + +class AuthError(TwythonError): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): - self.authtype = authtype + def __init__(self, username = None, password = None, headers = None, version = 1): + """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + username - Your Twitter username, if you want Basic (HTTP) Authentication. + password - Password for your twitter account, if you want Basic (HTTP) Authentication. + headers - User agent header. + version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. + + ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. + """ self.authenticated = False self.username = username - self.password = password - # OAuth specific variables below - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None + self.apiVersion = version # Check and set up authentication - if self.username is not None and self.password is not None: - if self.authtype == "Basic": - # Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - else: - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - if consumer_secret is not None and consumer_key is not None: - #req = oauth.OAuthRequest.from_consumer_and_token - #req.sign_request(self.signature_method, self.consumer_key, self.token) - #self.opener = urllib2.build_opener() - pass - else: - raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - - def getRequestToken(self): - response = self.oauth_request(self.request_token_url) - token = self.parseOAuthResponse(response) - self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) - return self.request_token - - def parseOAuthResponse(self, response_string): - # Partial credit goes to Harper Reed for this gem. - lol = {} - for param in response_string.split("&"): - pair = param.split("=") - if(len(pair) != 2): - break - lol[pair[0]] = pair[1] - return lol + if self.username is not None and password is not None: + # Assume Basic authentication ritual + self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) + self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) + self.opener = urllib2.build_opener(self.handler) + if headers is not None: + self.opener.addheaders = [('User-agent', headers)] + try: + simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) + self.authenticated = True + except HTTPError, e: + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) + else: + pass # URL Shortening function huzzah def shortenURL(self, 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 url shorterning service other that is.gd. + """ try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() + return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) - + raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) + def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): + + def getRateLimitStatus(self, rate_for = "requestingIP", version = None): + """getRateLimitStatus() + + Returns the remaining number of API requests available to the requesting user before the + API limit is reached for the current hour. Calls to rate_limit_status do not count against + the rate limit. If authentication credentials are provided, the rate limit status for the + authenticating user is returned. Otherwise, the rate limit status for the requesting + IP address is returned. + + Params: + rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) + 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 = version or self.apiVersion try: if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") + raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): + raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) + + def getPublicTimeline(self, version = None): + """getPublicTimeline() + + Returns the 20 most recent statuses from non-protected users who have set a custom user icon. + The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. + + Params: + 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 = version or self.apiVersion try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getFriendsTimeline(self, **kwargs): + raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) + + def getHomeTimeline(self, version = None, **kwargs): + """getHomeTimeline(**kwargs) + + Returns the 20 most recent statuses, including retweets, posted by the authenticating user + and that user's friends. This is the equivalent of /timeline/home on the Web. + + Usage note: This home_timeline is identical to statuses/friends_timeline, except it also + contains retweets, which statuses/friends_timeline does not (for backwards compatibility + reasons). In a future version of the API, statuses/friends_timeline will go away and + be replaced by home_timeline. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion if self.authenticated is True: try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) + homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) + return simplejson.load(self.opener.open(homeTimelineURL)) + except HTTPError, e: + raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) + else: + raise AuthError("getHomeTimeline() requires you to be authenticated.") + + def getFriendsTimeline(self, version = None, **kwargs): + """getFriendsTimeline(**kwargs) + + Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. + This is the equivalent of /timeline/home on the Web. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) return simplejson.load(self.opener.open(friendsTimelineURL)) except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) + raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): + raise AuthError("getFriendsTimeline() requires you to be authenticated.") + + def getUserTimeline(self, id = None, version = None, **kwargs): + """getUserTimeline(id = None, **kwargs) + + Returns the 20 most recent statuses posted from the authenticating user. It's also + possible to request another user's timeline via the id parameter. This is the + equivalent of the Web / page for your own user, or the profile page for a third party. + + Parameters: + id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. + user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. + screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, `id`), kwargs) elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) + userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) try: # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user if self.authenticated is True: @@ -158,177 +225,682 @@ class setup: else: return simplejson.load(urllib2.urlopen(userTimelineURL)) except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % `e.code`, e.code) - - def getUserMentions(self, **kwargs): + + def getUserMentions(self, version = None, **kwargs): + """getUserMentions(**kwargs) + + Returns the 20 most recent mentions (status containing @username) for the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion if self.authenticated is True: try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) + mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) return simplejson.load(self.opener.open(mentionsFeedURL)) except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getUserMentions() requires you to be authenticated.") + raise AuthError("getUserMentions() requires you to be authenticated.") - def showStatus(self, id): + def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): + """reportSpam(self, id), user_id, screen_name): + + Report a user account to Twitter as a spam account. *One* of the following parameters is required, and + this requires that you be authenticated with a user account. + + Parameters: + id - Optional. The ID or screen_name of the user you want to report as a spammer. + user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. + 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 = version or self.apiVersion + if self.authenticated is True: + # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. + if id is not None or user_id is not None or screen_name is not None: + try: + apiExtension = "" + if id is not None: + apiExtension = "id=%s" % id + if user_id is not None: + apiExtension = "user_id=%s" % `user_id` + if screen_name is not None: + apiExtension = "screen_name=%s" % screen_name + return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) + except HTTPError, e: + raise TwythonError("reportSpam() failed with a %s error code." % `e.code`, e.code) + else: + raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") + else: + raise AuthError("reportSpam() requires you to be authenticated.") + + def reTweet(self, id, version = None): + """reTweet(id) + + Retweets a tweet. Requires the id parameter of the tweet you are retweeting. + + Parameters: + id - Required. The numerical ID of the tweet you are retweeting. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, `id`), "POST")) + except HTTPError, e: + raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("reTweet() requires you to be authenticated.") + + def getRetweets(self, id, count = None, version = None): + """ getRetweets(self, id, count): + + Returns up to 100 of the first retweets of a given tweet. + + Parameters: + id - Required. The numerical ID of the tweet you want the retweets of. + count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. + 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 = version or self.apiVersion + if self.authenticated is True: + apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, `id`) + if count is not None: + apiURL += "?count=%s" % `count` + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("getRetweets failed with a %s eroror code." % `e.code`, e.code) + else: + raise AuthError("getRetweets() requires you to be authenticated.") + + def retweetedOfMe(self, version = None, **kwargs): + """retweetedOfMe(**kwargs) + + Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedOfMe() requires you to be authenticated.") + + def retweetedByMe(self, version = None, **kwargs): + """retweetedByMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedByMe() requires you to be authenticated.") + + def retweetedToMe(self, version = None, **kwargs): + """retweetedToMe(**kwargs) + + Returns the 20 most recent retweets posted by the authenticating user's friends. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) + return simplejson.load(self.opener.open(retweetURL)) + except HTTPError, e: + raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("retweetedToMe() requires you to be authenticated.") + + def searchUsers(self, q, per_page = 20, page = 1, version = None): + """ searchUsers(q, per_page = None, page = None): + + Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) + + Parameters: + q (string) - Required. The query you wanna search against; self explanatory. ;) + per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) + page (number) - Optional, defaults to 1. The page of users you want to pull 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. + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) + except HTTPError, e: + raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("searchUsers(), oddly, requires you to be authenticated.") + + def showUser(self, id = None, user_id = None, screen_name = None, version = None): + """showUser(id = None, user_id = None, screen_name = None) + + Returns extended information of a given user. The author's most recent status will be returned inline. + + Parameters: + ** Note: One of the following must always be specified. + id - The ID or screen name of a user. + user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. + 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. + + Usage Notes: + Requests for protected users without credentials from + 1) the user requested or + 2) a user that is following the protected user will omit the nested status element. + + ...will result in only publicly available data being returned. + """ + version = version or self.apiVersion + apiURL = "" + if id is not None: + apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) + if user_id is not None: + apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, `user_id`) + if screen_name is not None: + apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) + if apiURL != "": + try: + if self.authenticated is True: + return simplejson.load(self.opener.open(apiURL)) + else: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) + + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): + """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") + + Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. + (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access + older friends. With no user specified, the request defaults to the authenticated users friends. + + It's also possible to request another user's friends list via the id, screen_name or user_id parameter. + + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + + Parameters: + ** Note: One of the following is required. (id, user_id, or screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of friends. + user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. + page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + 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 = version or self.apiVersion + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) + if user_id is not None: + apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, `user_id`) + if screen_name is not None: + apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) + try: + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) + except HTTPError, e: + raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFriendsStatus() requires you to be authenticated.") + + def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): + """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") + + Returns the authenticating user's followers, each with current status inline. + They are ordered by the order in which they joined Twitter, 100 at a time. + (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) + + Use the page option to access earlier followers. + + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen name of the user for whom to request a list of followers. + user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. + page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. + cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. + 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 = version or self.apiVersion + if self.authenticated is True: + apiURL = "" + if id is not None: + apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) + if user_id is not None: + apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, `user_id`) + if screen_name is not None: + apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) + try: + if page is not None: + return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) + else: + return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) + except HTTPError, e: + raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("getFollowersStatus() requires you to be authenticated.") + + def showStatus(self, id, version = None): + """showStatus(id) + + Returns a single status, specified by the id parameter below. + The status's author will be returned inline. + + Parameters: + id - Required. The numerical ID of the status to retrieve. + 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 = version or self.apiVersion try: if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." + raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): + + def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): + """updateStatus(status, in_reply_to_status_id = None) + + Updates the authenticating user's status. Requires the status parameter specified below. + A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. + + Parameters: + status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. + in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. + latitude (string) - Optional. The location's latitude that this tweet refers to. + longitude (string) - Optional. The location's longitude that this tweet refers 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. + + ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references + is mentioned within the status text. Therefore, you must include @username, where username is + the author of the referenced tweet, within the update. + + ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. + This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. + """ + version = version or self.apiVersion + if len(list(status)) > 140: + raise TwythonError("This status message is over 140 characters. Trim it down!") + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({ + "status": self.unicode2utf8(status), + "in_reply_to_status_id": in_reply_to_status_id, + "lat": latitude, + "long": longitude + }))) + except HTTPError, e: + raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) + + def destroyStatus(self, id, version = None): + """destroyStatus(id) + + Destroys the status specified by the required ID parameter. + The authenticating user must be the author of the specified status. + + Parameters: + id - Required. The ID of the status to destroy. + 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 = version or self.apiVersion if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, `id`), "DELETE")) except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): + raise AuthError("destroyStatus() requires you to be authenticated.") + + def endSession(self, version = None): + """endSession() + + Ends the session of the authenticating user, returning a null cookie. + Use this method to sign users out of client-facing applications (widgets, etc). + + Parameters: + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") + self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") self.authenticated = False except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) + raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): + raise AuthError("You can't end a session when you're not authenticated to begin with.") + + def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): + """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent to the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page + apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, `page`) if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % `since_id` if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % `max_id` if count is not None: - apiURL += "&count=" + count - + apiURL += "&count=%s" % `count` + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): + raise AuthError("getDirectMessages() requires you to be authenticated.") + + def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): + """getSentMessages(since_id = None, max_id = None, count = None, page = "1") + + Returns a list of the 20 most recent direct messages sent by the authenticating user. + + Parameters: + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. + 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 = version or self.apiVersion if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page + apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, `page`) if since_id is not None: - apiURL += "&since_id=" + since_id + apiURL += "&since_id=%s" % `since_id` if max_id is not None: - apiURL += "&max_id=" + max_id + apiURL += "&max_id=%s" % `max_id` if count is not None: - apiURL += "&count=" + count - + apiURL += "&count=%s" % `count` + try: return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): + raise AuthError("getSentMessages() requires you to be authenticated.") + + def sendDirectMessage(self, user, text, version = None): + """sendDirectMessage(user, text) + + Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. + Returns the sent message in the requested format when successful. + + Parameters: + user - Required. The ID or screen name of the recipient user. + text - Required. The text of your direct message. Be sure to keep it under 140 characters. + 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 = version or self.apiVersion if self.authenticated is True: if len(list(text)) < 140: try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) + return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.urlencode({"user": user, "text": text})) except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("Your message must not be longer than 140 characters") + raise TwythonError("Your message must not be longer than 140 characters") else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): + raise AuthError("You must be authenticated to send a new direct message.") + + def destroyDirectMessage(self, id, version = None): + """destroyDirectMessage(id) + + Destroys the direct message specified in the required ID parameter. + The authenticating user must be the recipient of the specified direct message. + + Parameters: + id - Required. The ID of the direct message to destroy. + 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") + return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): + raise AuthError("You must be authenticated to destroy a direct message.") + + def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): + """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") + + Allows the authenticating users to follow the user specified in the ID parameter. + Returns the befriended user in the requested format when successful. Returns a + string describing the failure condition when unsuccessful. If you are already + friends with the user an HTTP 403 will be returned. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to befriend. + user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. + follow - Optional. Enable notifications for the target user in addition to becoming friends. + 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow + apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow + apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: - return simplejson.load(self.opener.open(apiURL)) + if id is not None: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) except HTTPError, e: # Rate limiting is done differently here for API reasons... if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) + raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") + raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): + raise AuthError("createFriendship() requires you to be authenticated.") + + def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): + """destroyFriendship(id = None, user_id = None, screen_name = None) + + Allows the authenticating users to unfollow the user specified in the ID parameter. + Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to unfollow. + user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. + 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id + apiURL = "?user_id=%s" % `user_id` if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name + apiURL = "?screen_name=%s" % screen_name try: + if id is not None: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, `id`), "lol=1")) # Random string hack for POST reasons ;P + else: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) + except HTTPError, e: + raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("destroyFriendship() requires you to be authenticated.") + + def checkIfFriendshipExists(self, user_a, user_b, version = None): + """checkIfFriendshipExists(user_a, user_b) + + Tests for the existence of friendship between two users. + Will return true if user_a follows user_b; otherwise, it'll return false. + + Parameters: + user_a - Required. The ID or screen_name of the subject user. + user_b - Required. The ID or screen_name of the user to test for following. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.urlencode({"user_a": user_a, "user_b": user_b})) + return simplejson.load(self.opener.open(friendshipURL)) + except HTTPError, e: + raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") + + def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): + """showFriendship(source_id, source_screen_name, target_id, target_screen_name) + + Returns detailed information about the relationship between two users. + + Parameters: + ** Note: One of the following is required if the request is unauthenticated + source_id - The user_id of the subject user. + source_screen_name - The screen_name of the subject user. + + ** Note: One of the following is required at all times + target_id - The user_id of the target user. + target_screen_name - The screen_name of the target user. + + 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 = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D + if source_id is not None: + apiURL += "&source_id=%s" % `source_id` + if source_screen_name is not None: + apiURL += "&source_screen_name=%s" % source_screen_name + if target_id is not None: + apiURL += "&target_id=%s" % `target_id` + if target_screen_name is not None: + apiURL += "&target_screen_name=%s" % target_screen_name + try: + if self.authenticated is True: return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") + else: + return simplejson.load(urllib2.urlopen(apiURL)) + except HTTPError, e: + # Catch this for now + if e.code == 403: + raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") + raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) - def checkIfFriendshipExists(self, user_a, user_b): + def updateDeliveryDevice(self, device_name = "none", version = None): + """updateDeliveryDevice(device_name = "none") + + Sets which device Twitter delivers updates to for the authenticating user. + Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) + + Parameters: + device - Required. Must be one of: sms, im, none. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) + return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.urlencode({"device": self.unicode2utf8(device_name)})) except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): + raise AuthError("updateDeliveryDevice() requires you to be authenticated.") + + def updateProfileColors(self, version = None, **kwargs): + """updateProfileColors(**kwargs) + + Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. + + Parameters: + ** Note: One or more of the following parameters must be present. Each parameter's value must + be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). + + profile_background_color - Optional. + profile_text_color - Optional. + profile_link_color - Optional. + profile_sidebar_fill_color - Optional. + profile_sidebar_border_color - Optional. + + 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 = version or self.apiVersion if self.authenticated is True: try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) + return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): + raise AuthError("updateProfileColors() requires you to be authenticated.") + + def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): + """updateProfile(name = None, email = None, url = None, location = None, description = None) + + Sets values that users are able to set under the "Account" tab of their settings page. + Only the parameters specified will be updated. + + Parameters: + One or more of the following parameters must be present. Each parameter's value + should be a string. See the individual parameter descriptions below for further constraints. + + name - Optional. Maximum of 20 characters. + email - Optional. Maximum of 40 characters. Must be a valid email address. + url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. + location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. + description - Optional. Maximum of 160 characters. + + 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 = version or self.apiVersion if self.authenticated is True: useAmpersands = False updateProfileQueryString = "" @@ -337,7 +909,7 @@ class setup: updateProfileQueryString += "name=" + name useAmpersands = True else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") + raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") if email is not None and "@" in email: if len(list(email)) < 40: if useAmpersands is True: @@ -346,197 +918,376 @@ class setup: updateProfileQueryString += "email=" + email useAmpersands = True else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") + raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") if url is not None: if len(list(url)) < 100: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) + updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) else: - updateProfileQueryString += urllib.urlencode({"url": url}) + updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") + raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") if location is not None: if len(list(location)) < 30: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) + updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) else: - updateProfileQueryString += urllib.urlencode({"location": location}) + updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) useAmpersands = True else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") + raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") if description is not None: if len(list(description)) < 160: if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) + updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) else: - updateProfileQueryString += urllib.urlencode({"description": description}) + updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - + raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") + if updateProfileQueryString != "": try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) + return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): + raise AuthError("updateProfile() requires you to be authenticated.") + + def getFavorites(self, page = "1", version = None): + """getFavorites(page = "1") + + Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. + + Parameters: + page - Optional. Specifies the page of favorites to retrieve. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, `page`))) except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): + raise AuthError("getFavorites() requires you to be authenticated.") + + def createFavorite(self, id, version = None): + """createFavorite(id) + + Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. + + Parameters: + id - Required. The ID of the status to favorite. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, `id`), "")) except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): + raise AuthError("createFavorite() requires you to be authenticated.") + + def destroyFavorite(self, id, version = None): + """destroyFavorite(id) + + Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. + + Parameters: + id - Required. The ID of the status to un-favorite. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): + raise AuthError("destroyFavorite() requires you to be authenticated.") + + def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): + """notificationFollow(id = None, user_id = None, screen_name = None) + + Enables device notifications for updates from the specified user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" + apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name + apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): + raise AuthError("notificationFollow() requires you to be authenticated.") + + def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): + """notificationLeave(id = None, user_id = None, screen_name = None) + + Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + 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 = version or self.apiVersion if self.authenticated is True: apiURL = "" if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" + apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name + apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(self.opener.open(apiURL, "")) except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + raise AuthError("notificationLeave() requires you to be authenticated.") + + def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): + """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") + + Returns an array of numeric IDs for every user the specified user is following. + + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + 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 = version or self.apiVersion apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page + apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): + raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) + + def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): + """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") + + Returns an array of numeric IDs for every user following the specified user. + + Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Required. The ID or screen name of the user to follow with device updates. + user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. + page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) + cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + 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 = version or self.apiVersion apiURL = "" + breakResults = "cursor=%s" % cursor + if page is not None: + breakResults = "page=%s" % page if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page + apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, `id`, breakResults) if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page + apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page + apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): + raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) + + def createBlock(self, id, version = None): + """createBlock(id) + + Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. + Returns the blocked user in the requested format when successful. + + Parameters: + id - The ID or screen name of a user to block. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, `id`), "")) except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): + raise AuthError("createBlock() requires you to be authenticated.") + + def destroyBlock(self, id, version = None): + """destroyBlock(id) + + Un-blocks the user specified in the ID parameter for the authenticating user. + Returns the un-blocked user in the requested format when successful. + + Parameters: + id - Required. The ID or screen_name of the user to un-block + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): + raise AuthError("destroyBlock() requires you to be authenticated.") + + def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): + """checkIfBlockExists(id = None, user_id = None, screen_name = None) + + Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and + error with an HTTP 404 response code otherwise. + + Parameters: + ** Note: One of the following is required. (id, user_id, screen_name) + id - Optional. The ID or screen_name of the potentially blocked user. + user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. + screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. + 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 = version or self.apiVersion apiURL = "" if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" + apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, `id`) if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id + apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, `user_id`) if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name + apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) try: return simplejson.load(urllib2.urlopen(apiURL)) except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): + raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) + + def getBlocking(self, page = "1", version = None): + """getBlocking(page = "1") + + Returns an array of user objects that the authenticating user is blocking. + + Parameters: + page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, `page`))) except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): + raise AuthError("getBlocking() requires you to be authenticated") + + def getBlockedIDs(self, version = None): + """getBlockedIDs() + + Returns an array of numeric user ids the authenticating user is blocking. + + Parameters: + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - + raise AuthError("getBlockedIDs() requires you to be authenticated.") + def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. + lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. + locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. + rpp - Optional. The number of tweets to return per page, up to a max of 100. + page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) + since_id - Optional. Returns tweets with status ids greater than the given id. + geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. + show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. + + Usage Notes: + Queries are limited 140 URL encoded characters. + Some users may be absent from search results. + The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. + This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. + + Applications must have a meaningful and unique User Agent when using this method. + An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than + applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) try: return simplejson.load(urllib2.urlopen(searchURL)) except HTTPError, e: - raise TangoError("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 getCurrentTrends(self, excludeHashTags = False): + """getCurrentTrends(excludeHashTags = False) + + Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used + on Twitter Search results page for that topic. + + Parameters: + excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/current.json" if excludeHashTags is True: apiURL += "?exclude=hashtags" try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - + raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) + def getDailyTrends(self, date = None, exclude = False): + """getDailyTrends(date = None, exclude = False) + + Returns the top 20 trending topics for each hour in a given day. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -546,13 +1297,21 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - + raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) + def getWeeklyTrends(self, date = None, exclude = False): + """getWeeklyTrends(date = None, exclude = False) + + Returns the top 30 trending topics for each day in a given week. + + Parameters: + date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. + exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + """ apiURL = "http://search.twitter.com/trends/daily.json" questionMarkUsed = False if date is not None: - apiURL += "?date=" + date + apiURL += "?date=%s" % date questionMarkUsed = True if exclude is True: if questionMarkUsed is True: @@ -562,73 +1321,440 @@ class setup: try: return simplejson.load(urllib.urlopen(apiURL)) except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): + raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) + + def getSavedSearches(self, version = None): + """getSavedSearches() + + Returns the authenticated user's saved search queries. + + Parameters: + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): + raise AuthError("getSavedSearches() requires you to be authenticated.") + + def showSavedSearch(self, id, version = None): + """showSavedSearch(id) + + Retrieve the data for a saved search owned by the authenticating user specified by the given id. + + Parameters: + id - Required. The id of the saved search to be retrieved. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, `id`))) except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): + raise AuthError("showSavedSearch() requires you to be authenticated.") + + def createSavedSearch(self, query, version = None): + """createSavedSearch(query) + + Creates a saved search for the authenticated user. + + Parameters: + query - Required. The query of the search the user would like to save. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): + raise AuthError("createSavedSearch() requires you to be authenticated.") + + def destroySavedSearch(self, id, version = None): + """ destroySavedSearch(id) + + Destroys a saved search for the authenticated user. + The search specified by id must be owned by the authenticating user. + + Parameters: + id - Required. The id of the saved search to be deleted. + 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, `id`), "")) except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") + raise AuthError("destroySavedSearch() requires you to be authenticated.") + + def createList(self, name, mode = "public", description = "", version = None): + """ createList(self, name, mode, description, version) + + Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + name - Required. The name for the new list. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), + urllib.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError, e: + raise TwythonError("createList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("createList() requires you to be authenticated.") + + def updateList(self, list_id, name, mode = "public", description = "", version = None): + """ updateList(self, list_id, name, mode, description, version) + + Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look + at this... + + Parameters: + list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). + name - Required. The name of the list, possibly for renaming or such. + description - Optional, in the sense that you can leave it blank if you don't want one. ;) + mode - Optional. This is a string indicating "public" or "private", defaults to "public". + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), + urllib.urlencode({"name": name, "mode": mode, "description": description}))) + except HTTPError, e: + raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("updateList() requires you to be authenticated.") + + def showLists(self, version = None): + """ showLists(self, version) + + Show all the lists for the currently authenticated user (i.e, they own these lists). + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) + except HTTPError, e: + raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("showLists() requires you to be authenticated.") + + def getListMemberships(self, version = None): + """ getListMemberships(self, version) + + Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) + (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). + + Parameters: + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) + except HTTPError, e: + raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("getLists() requires you to be authenticated.") + + def deleteList(self, list_id, version = None): + """ deleteList(self, list_id, version) + + Deletes a list for the authenticating user. + + Parameters: + list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) + except HTTPError, e: + raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("deleteList() requires you to be authenticated.") + + def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): + """ getListTimeline(self, list_id, cursor, version, **kwargs) + + Retrieves a timeline representing everyone in the list specified. + + Parameters: + list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. + max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. + count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. + cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. + Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. + 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 = version or self.apiVersion + try: + baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) + return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) + except HTTPError, e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) + + def getSpecificList(self, list_id, version = None): + """ getSpecificList(self, list_id, version) + + Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). + + Parameters: + list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. + 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + except HTTPError, e: + if e.code == 404: + raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") + raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) + + def addListMember(self, list_id, version = None): + """ addListMember(self, list_id, id, version) + + Adds a new Member (the passed in id) to the specified list. + + Parameters: + list_id - Required. The slug of the list to add the new member to. + id - Required. The ID of the user that's being added to the list. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % `id`)) + except HTTPError, e: + raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("addListMember requires you to be authenticated.") + + def getListMembers(self, list_id, version = None): + """ getListMembers(self, list_id, version = None) + + Show all members of a specified list. This method requires authentication if the list is private/protected. + + Parameters: + list_id - Required. The slug of the list to retrieve members for. + 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + except HTTPError, e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + + def removeListMember(self, list_id, id, version = None): + """ removeListMember(self, list_id, id, version) + + Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. + + Parameters: + list_id - Required. The slug of the list to remove the specified user from. + id - Required. The ID of the user that's being added to the list. + 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 = version or self.apiVersion + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) + except HTTPError, e: + raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("removeListMember() requires you to be authenticated.") + + def isListMember(self, list_id, id, version = None): + """ isListMember(self, list_id, id, version) + + 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def subscribeToList(self, list_id, version): + """ subscribeToList(self, list_id, version) + + Subscribe the authenticated user to the list provided (must be public). + + Parameters: + list_id - Required. The list to subscribe 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. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) + except HTTPError, e: + raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("subscribeToList() requires you to be authenticated.") + + def unsubscribeFromList(self, list_id, version): + """ unsubscribeFromList(self, list_id, version) + + Unsubscribe the authenticated user from the list in question (must be public). + + Parameters: + list_id - Required. The list to unsubscribe from. + 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. + """ + if self.authenticated is True: + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) + except HTTPError, e: + raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("unsubscribeFromList() requires you to be authenticated.") + + def isListSubscriber(self, list_id, id, version = None): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + Parameters: + list_id - Required. The slug of the list to check against. + id - Required. The ID of the user being checked in the list. + 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 = version or self.apiVersion + try: + if self.authenticated is True: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + else: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def availableTrends(self, latitude = None, longitude = None, version = None): + """ availableTrends(latitude, longitude, version): + + Gets all available trends, optionally filtering by geolocation based stuff. + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + latitude (string) - Optional. A latitude to sort by. + longitude (string) - Optional. A longitude to sort by. + 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 = version or self.apiVersion + try: + if latitude is not None and longitude is not None: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) + except HTTPError, e: + raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) + + def trendsByLocation(self, woeid, version = None): + """ trendsByLocation(woeid, version): + + Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). + + Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P + + Parameters: + woeid (string) - Required. WoeID of the area you're searching in. + 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 = version or self.apiVersion + try: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) + except HTTPError, e: + raise TwythonError("trendsByLocation() 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. - def updateProfileBackgroundImage(self, filename, tile="true"): + def updateProfileBackgroundImage(self, filename, tile="true", version = None): + """ updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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 = version or self.apiVersion if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): + raise AuthError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename, version = None): + """ updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ + version = version or self.apiVersion if self.authenticated is True: try: - files = [("image", filename, open(filename).read())] + files = [("image", filename, open(filename, 'rb').read())] fields = [] content_type, body = self.encode_multipart_formdata(fields, files) headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) return self.opener.open(r).read() except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - + raise AuthError("You realize you need to be authenticated to change a profile image, right?") + def encode_multipart_formdata(self, fields, files): BOUNDARY = mimetools.choose_boundary() CRLF = '\r\n' @@ -649,6 +1775,18 @@ class setup: body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - + def get_content_type(self, filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + def unicode2utf8(self, text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text diff --git a/dist/twython-0.9.macosx-10.5-i386.tar.gz b/dist/twython-0.9.macosx-10.5-i386.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6e3e62b54f47a51b73cf3c728d7ffbb45b12c1c2 GIT binary patch literal 58118 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 PeDl9w3Q+;<0|NXH=9Phb literal 0 HcmV?d00001 diff --git a/dist/twython-0.9.win32.exe b/dist/twython-0.9.win32.exe new file mode 100644 index 0000000000000000000000000000000000000000..c1f416ea0d91f4d504b81d4322276e55593a7341 GIT binary patch literal 90830 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 CfJ>nO literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index fb1900c..f5c956f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '0.8' +__version__ = '0.9' # For the love of god, use Pip to install this. @@ -12,13 +12,13 @@ METADATA = dict( name = "twython", version = __version__, py_modules = ['twython'], - author='Ryan McGrath', - author_email='ryan@venodesigns.net', - description='A new and easy way to access Twitter data with Python.', - long_description= open("README.markdown").read(), - license='MIT License', - url='http://github.com/ryanmcgrath/twython/tree/master', - keywords='twitter search api tweet twython', + author = 'Ryan McGrath', + author_email = 'ryan@venodesigns.net', + description = 'An easy (and up to date) way to access Twitter data with Python.', + long_description = open("README.markdown").read(), + license = 'MIT License', + url = 'http://github.com/ryanmcgrath/twython/tree/master', + keywords = 'twitter search api tweet twython', ) # Setuptools version diff --git a/twython.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO index ade4232..8359b77 100644 --- a/twython.egg-info/PKG-INFO +++ b/twython.egg-info/PKG-INFO @@ -1,13 +1,13 @@ Metadata-Version: 1.0 Name: twython -Version: 0.8 -Summary: A new and easy way to access Twitter data with Python. +Version: 0.9 +Summary: An easy (and up to date) way to access Twitter data with Python. Home-page: http://github.com/ryanmcgrath/twython/tree/master Author: Ryan McGrath Author-email: ryan@venodesigns.net License: MIT License Description: Twython - Easy Twitter utilities in Python - ----------------------------------------------------------------------------------------------------- + ========================================================================================= I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at a library that offers more coverage. @@ -16,28 +16,27 @@ Description: Twython - Easy Twitter utilities in Python make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, and I'm open to anything that'll improve the library as a whole. - OAuth support is in the works, but every other part of the Twitter API should be covered. Twython - handles both Basic (HTTP) Authentication and OAuth, and OAuth is the default method for - Authentication. To override this, specify 'authtype="Basic"' in your twython.setup() call. - - Documentation is forthcoming, but Twython attempts to mirror the Twitter API in a large way. All - parameters for API calls should translate over as function arguments. + OAuth and Streaming API support is in the works, but every other part of the Twitter API should be covered. Twython + handles both Basic (HTTP) Authentication and OAuth (Older versions (pre 0.9) of Twython need Basic Auth specified - + to override this, specify 'authtype="Basic"' in your twython.setup() call). + Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or + Twitter's API Wiki (Twython calls mirror most of the methods listed there). Requirements ----------------------------------------------------------------------------------------------------- Twython requires (much like Python-Twitter, because they had the right idea :D) a library called "simplejson". You can grab it at the following link: - http://pypi.python.org/pypi/simplejson + > http://pypi.python.org/pypi/simplejson Example Use ----------------------------------------------------------------------------------------------------- - import twython - - twitter = twython.setup(authtype="Basic", username="example", password="example") - twitter.updateStatus("See how easy this was?") + > import twython + > + > twitter = twython.setup(username="example", password="example") + > twitter.updateStatus("See how easy this was?") Twython 3k diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt index 3a6ce67..26a7786 100644 --- a/twython.egg-info/SOURCES.txt +++ b/twython.egg-info/SOURCES.txt @@ -1,4 +1,3 @@ -README setup.py twython.py twython.egg-info/PKG-INFO diff --git a/twython.py b/twython.py index cf05f71..ffe31e5 100644 --- a/twython.py +++ b/twython.py @@ -16,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.8" +__version__ = "0.9" """Twython - Easy Twitter utilities in Python""" diff --git a/twython3k.py b/twython3k.py index dc7026c..b05b1f2 100644 --- a/twython3k.py +++ b/twython3k.py @@ -16,7 +16,7 @@ from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.8" +__version__ = "0.9" """Twython - Easy Twitter utilities in Python""" From 68c483d4315ff0f976f2b531c1f87a9184d0ba0d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 23 Nov 2009 22:09:30 -0500 Subject: [PATCH 132/687] Twython 0.9 - enough has changed with the Twitter API as of late that this merits a new release. 0.8 was beginning to show age as the API moved forward, and is now deprecated as a result - 0.9 is the way to go (or trunk, if you're adventurous. ;D) --- dist/twython-0.9-py2.5.egg | Bin 0 -> 63786 bytes dist/twython-0.9.macosx-10.5-i386.tar.gz | Bin 58118 -> 58121 bytes dist/twython-0.9.tar.gz | Bin 16158 -> 16158 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 dist/twython-0.9-py2.5.egg diff --git a/dist/twython-0.9-py2.5.egg b/dist/twython-0.9-py2.5.egg new file mode 100644 index 0000000000000000000000000000000000000000..871b3c708f17f2967ce9ee9066b972995db4f838 GIT binary patch literal 63786 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 z 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 bF!$f$KMH_={PQ7%kB7&H{7$+3^X~rvOyG4| literal 0 HcmV?d00001 diff --git a/dist/twython-0.9.macosx-10.5-i386.tar.gz b/dist/twython-0.9.macosx-10.5-i386.tar.gz index 6e3e62b54f47a51b73cf3c728d7ffbb45b12c1c2..693c10027016d5a00278fa591ef2ef5689ade589 100644 GIT binary patch delta 44368 zcmV(!K;^%N#si7Q0|p<92nbV7kp?|~-7L*G<8hog-gu91BPl0|GdY|V8x%WgcGHcf z6lLsv*yIEDZQu8CU-xhJGxi5|s|rBh*rZ5`lr?)yTO=9{R28ZURfS8VD^)h}&k}q# zZro76>*{xDWljC64_jVdU0PXR+gM&%WlPITOKaB!IuIA_73<@GCn2gMo< z_W$lvsXlOa0xLMIo+kM(uiaRACI6F^|C7V8>3h{vA^(+))zw$>e<|``sobc(bosBY ztZuxL|4Wmv!#{g^_3;YmtM(#;-9M@Ry#7N?l@kxP2!!M zEf$Nj96pa8Z+~%rJQ+<#&YZA1v^lPRPTi2*kXE!VXkC@Pj?>0}h1lcyJeR*`jTK1z}5gVP$r9 zMk7KZERT2Ei=n@0TcO~tQeEEg1FqB;s#_sH2&}e5JV2uph9cm1p4_j@&OAocrlh&0 zfH(pO5~76-BkCK_DxlxuJ5C7HHyzJj0Q5GJLlvO~02K;LF7|zQ9~w!8EBs5NWcGL!)nyjF8O zC#=;%Q^L)5J`&_KYCftX^|m5( z3n>D$UXX`;THwfZ5TzYukbr7?;!s9eO<~!f+xj{tJb}O@leeJjI(4v72>YJp%IIc- z2$M9o66Bbqxs@U;zEE;a!v$;O`5{q@_KK%LC+?E;P4&1r5yR-dLXTYpEK@D?_k>pi z(*ab>qxwsK(Em{N=n7b@Dv?%A6`zm6SoOM)a!@GA(n%m>oSdI5>=sEV`9O4Ok$`}f z%NeYy5u>VomEZN1?dHTQ8Fm^Cj_wSsThl+_vgvnRTbcYF0lvYrU18tmTi{tF7?eE_ z32y`-CUSevw7IWTDhOY=xzhLyU29SrB>tA=L39>>pc*MlUO^N7M9cZ%Zt%4eHt&jt z)p5g3VY#hRIUhpBvhAADkCIaQVmDKu|D^P6A+1;m;HuSG%BU5{x-Zr3QWqf!5Pn;D zDF{0q$F*xJyOhMDDmjg)pJo+21rggCcj)g*^It)076G{z&PQFn{5{pvz6Ak9KB40J zmR(AJ0>qzE4!cl${f-w_!7)0G!&*HMHYyXCf(k+^mdgq1vKC`I=m@&4eMRJ*!Psk} zl-W+WisGFZ24OvNA0M#M4b}h%d%}duL)#?PIT;$jI z-$VtS#oquaC4k~t=mV+)G&)|LLX}TY>w~a=-U%fIVyeL^@)0Rq%1}2%SZ~(A-?pK$ zs8L_jMq3#+Z9T};l3Scn6?a8g>v&OzwreRf6T6IToi2l+u~D_I8o+T1Srt^N%cfAJ z>RjbtZ9d>z$O7SEs|{m#v-8tWR#QQh>OK_J0D%kuR@3O7(LaV()1YkCEbhxf6R-wi}0;VU;*KwSWAB=z&A5Ay(pK# z?t{9cqW03mZY@l-w4BmGi-USm2*MF-C8=R$!~HtwL>P4H;hnYX{U>R$fN6>gW=pERtXq;bGg(RYvoeIjMG_%^ zLK>4sMnL#+)7Z%zdjlpD4URD^`P2+$^!k#MpeILvzV$oY6KaGBE;LOkp z6sQ3ppx_Dx$KW6SD-tTj;neniNW(v=29VJZ)rUb-6MOYR0DZ0!jb3(fhzM3StJN?V z4%7{s2DC8%%v0fkH;)6#+dVC;R=Ovh9oMOE!?4YTfkkZIGW~rNtx*gUvf3sbwNN8T zNNI4+b(1Ki%jMj9I0=v%6C*2MH>kOq_C z&;r*)%?{IHx=GUAvzL$0<#m=#LoqwS|=IX#lPStLS?rZVz#;=xrcO0Hz- z*jh^G*Ofq+DXtfsumD`6-EWB=wyI& z@HE}pKq0(p*JRoh59xY$g2^;FJ<9CS`bjap8<*+nSNYchj4j6qx89MV-{N}o0FpQm z^Fc;q7jT9MoCeLvackcPY1JWykoa`};*SG7k~9A4TV*I*FDBa}THWvClO16p*AY%`P=WX|p29%Uni$Z|`yD5}_5o*bGWLQ!l%4Pm%zPz^7s~>D5 zWAtdCC8V@=X*$&32UbBfT#O1VFzNx#dpIrh1GazQeMp0a*e;Ak-4t_34{jG1az@=n zRkV0BVp=hmYfvD6Hy;M*!y1Z|z{YMRGSG)G5jKp71OG73N(^Dnk1w{WCM4>>Ad@GL zx3*K}VsLh+n>BWRXTcA_aOn11&_Qq=EE-r2!RetU0{~Y|Dr6B;i6)n65xb5@WTXdwNq0d+_3)uGPm$Slyt=3* z21_mL$K)k(GO=#8OAA}e#Q_CpQ6Q6;T1?8xCW30(U@FDGjVNwm*pCV0r45l?bL<-a z$AI9vDMhu-g@&M8)wjMXL$y^>*HtX&Ta}7XO-kYYkqm@_IZ7Co5$+@cu`U$DjnTIt z|MV(fRpOq1k5?Kvx(24?I|=>EAQ)yK02VZ*y=E;0LfHi#iXX!fB15WH=6{Og!g%4% zaC;DMl~qJf92jqv6S^VE5SJ;+i%0`p;m->*$34WtT{mAwlUs5mljp~RC@5}qWJp>J z(!#|gh>ydj@tOp{)06M@KtO3d+uL;7-E*~ijxxGVOh!g|4c~SB1GMW6jM#>E2SweN``F`r zr_qQgmsQo4u1Q&14pbD9IaW!H(6sKjSe2vF&0F7`D)rJ{d55@hH&)xY==LOGcT z2Wyu8-8sogprm94k#s}|ShI8xi1ezQ9h0*&!u(t>#*E+o^g^`2ecN^q7m~Z8t&n8e zN1)33-W9I1FM`9nV&AEoK+dKMV|R)k^s(Y3Lpw9;q)4jSnx>kfqu+F_Dgok4ko;nQ z#3|McD<(A6+J$9^KByo(3H$~S_POu+!Esm9dP!;EHMNZB+KRSXorsGhr->U)F6s78 zNGw?-qe?}~BNcRu7El~J`5;cazTWt0GQddJ1E-zH8&INPzV5WzFaWf0aVRk%o1*S7 zxM^^U9{5QH2l);)Ibz2WG-6-T1NPp3aB)s0a;L_Rb`uzHm+iN+%g0Z^Y}*mg@wN z7(*#lC&7UJ)pKi4im_`kV-@Qx+lKxi@wp`A90u^I8`0=?-*O#0R|cs1ebo$q3aELZ zrSb4Pf9q@!_u6MYW7yfmJXpP?dW?P;W4x>2hp zu>_NZKFqLNO*-kyFJ+rW#OGe~wlwKJ^sqVIuu09<#qfvU+Hy*`-fQ-4B(`Upb|l|t zMx16EiSghporZoG?jFs0pYBQ0B)zh#=IO^fOPY&8{Y-AQ^hl(WnKd1MNuhI8X6&{k z7srTd{Xkgz;vY$?T_kvtO05vk_erUA7s^wr&`Mb*s?P?+kP)DoguZ`Qjc5hlgRr0W4u|xp#p5jUA)h}?EMg7yOkqMRM$_3? zzsSnM@b~m+n4#zdbS#N~>IAi{8|`V;v!r&6q-n{))n~3>-+L)B{tjZE#IQ;=X+mQv z1AO=a)l28}B!*HG;&lf4@GJFHGqp%Re7JdRtEu^}7UuJU$Wd)%3UI%$@i-PS2TA`x z@g(eDj(Cq@0>PRRB*)u*sgrq{)jOJ&F-_@3O&j|(M;iE1y|dVV#cyGo$mS(-Idb&H zk(sO7F^LJ&6!@WbtXo07sTUQ@(-M?dqcd|dn#^;>?_l9l<81idA12t~P7GGMjxoz!6`w%j| zq&GuD0#>an&`BqMk%gQy<$;S|w{^K&O(+;b!Q!9{K6X%==Ew1Pdl%{+rZ`JGWupxx zs*$6z*GA|ObIpNv53OMDE17zl4_7l0Zx5$Q495awqcMRubKvPE*jS9&CqugGq!v7r z59{Q~H?h2UI#eMkzT+vv*Fx;Q(u99pO-RY_c&e~v?F(BSS#WXkk%L6+HL1~$9yFv| zF_CB8G8l$E^&+nRa+AIg9DkR{`{d&~Ls@u^V~<qjiY0^y1kX~xp zTf{~?lY&OVK70Jtqq}!Dzs1)un0JI!>P_F5U@@=5V({v+UL981Mh8kMsRs8yk?Be*fFb z^2XYYSMUEiiO+xi&)n?#zx?|@MK7(c4bL{CkRDCob?B6?pcam@O_c`yBhZ!2Y$sAmn@Mbf49?h3}o0+sAJ@I#jh3J~%ATjEeQ}Fj0YTj?*OEBt+ zQ`kR$Dd_G8Y6^XCbE|X)O4RY|jIQ@N*82#)o_`nEbH-|rpw9cl6!R~$LRi2Xsy}%J z=%Ng?9P2%D{tZ}1$q-?+T#O7TTvL@C|aoHq0wlgRT#B)z)~ z8GkLOh{i{yvxrx<08K)zMhvReTE5+J@e&EZ6_8DA)#im1Ime@E6B~F9As~)(g^Sah zq!~t|3Pz*ek2Q*s7EKv-I)mzjO{q_5CZ`x3sS|08Vl;7;_&a6zt1idTAY@S`=I(IP z-#j0YGET{p$d+QFO4MRBHk&Bv35XSixPKUnb!I9&OFb~`v?JM`GqMe)60)^#CjNtj z&ywYs(v}0?@RbGePICnEe*}1)_#4>CCq&6B6%Gm4COG z{VpZmP855O+3!=acM_SuX1_qh@-DM~L*=2&Z<+l&qU3#Me?XbPPV)->b7I$T2=#|l z7-|)%@b8#?l@cE$%Kx6Q@I=lv%K0$eit9vPF`YS2nOB)zBCx+_c9{~_62(3uXxFLy z$3)LOvlpmXiP?*kC^PX9vnzD>V}GoLmsJwtg1KCx%SC1{Q7^VbUnyq-4K*VQ#MKCJ{kOo2L&T8)VdsDIU%m_e<^ z#4LLzqk8A4-hYEHe2qC5G2FuXiIj^ZuxPP8jr7!71KdV+;W|;W~WnMO! zZ?flS4YBV5aToBqF~xT2w(};dT_MkVc%6m6V$NGS*LfSe+RtRl3gJ7NOJ6_p3)A7d z6xv>e(DnsH)!}>C7oVSF&VT!C?rrt^^ArWhexdLUwlwnWuc1lYL{ll};cLLRILhmN z%O>B|D2uvi{}(dKYey9`6JVrC^!`BkW^NscrZ#?+pIpg@e8emHY#`zMx(yxvC zH!pOgrqD)+a874O(|lCW96Ul7=H-eMG{kTH+(Z$EZ%{Ooc`s*p8GoT>Ub9I>yWWCF zK;u_oQV63bXu}V5u-xZ$lHo-S)V#h?WhCAu39781WMBPAsPy|AsaEpsjv*Qeot#w= z^+c~G*u>ftI^jAXC9no1aAJKEeIUg@ltq!~`6wK@gkPvgn^*7)sZ(t!T<$uDZ!Uu)7;NM)~y}}vzn}fSi!1aIa{b`gOS9%zTMrPK6EL345 z0dQ%ug9(C_MOI~10U$sE1koS?lIX_P2~gnDR5B_fsw#-A%tA&M3e^qBZm}g>o8?wp zvMtGDSzgAABulm>*&5IGY#%?#mgi~ajLx%vJdI{7`}rKdGk-eP%(2HF+wWWMy>V|u zWM*Vm78bJ5&8*CbyWelS-~E=1B$+sLP0q3m5sQGy$euU|p%zyrDO=-}1R1R3I0q_f zKMNH*r-CeNm&elgM*>b=|l04JhYKZt=MMnv*nz84NLcswMhMoLP; z8j*{#ilf8T8-J7Yz~l19Jj&xBjPrr>U@MCZblI|ofcs~G&A$e$?I8Sz3>D^lqdsK~ z!6qPYdj@$MGT9N7Q9%;$M}fDAV=5_gaFI|NzDy{4CYZu*)25#+B7rSGDUn|oXA$*b zDo7dwWpFDFkB%GCmNK$LI^prw?BeoT3QcN4CEz6BB7cjIa&bVoFlNm7Yxrvi?O84} zk5nWlgsvTOQ{LiZ8}0=xDu_Nyuow4oxIcJgvaE9-J!5?>Yr(?b8%8+{<)sjl2awv`*yhaC|%jJ2C1USojA;9TT#?A1wjdusM2w`T;5w%`8D4D&J)}6C(QpL)L z^Ba&0f`B2Qu^7|p$t1%Sw=r46VRcM zfa1wvmyI5arku2xsuv*MVEX>XW29wJ_3$A=W+;nK4Q z453Rraq6J4F*3__Y;zX@#U22QeVJ!6h<}g73x;*jxXOAAakT$3kbB~zjE{hu2rb9d z)2#ONS!NB)V38XSSqBkd0FZK4{T{sKW})auiOs&9*zA0S<2q(_^Z@u%(`E zylut!)~Mz0pr?7{I(Ku5Zyl5`S$7h?VGcGCaxfI;cA>Hbg}L3x!RT+4gN>3%H-9P) z!xi6cAvxEaBk3+9eezNgOc)hPy;S5XKTZeshcH)W5!nT+0{A>ld1K3a6v~AdxUU@s zwxzzIREse0yXCj>`);u(TY-tY858vs!r#OOpCB}RHS=udLgu;bzD$l-BU0*-1X}%l zQCjWwPOHDEX!W5)TFrljdhMH?DS!4<7l1RehZ1=9mdi6ZnpkD)X>k>sUY0T6O# zKbAz74`r2%J}PA-1cQw$CL{nUWNC>>n?Eez;eaG$Ps;)=7R`;fOw4we;&`6GqN6y@ z;#IK=80bkugC7_m#o2TcP!^>_Kxqs}lOdE27 zHtdu@@oQ1q@RPLRt(>_2{*+I2LwqSdBp9o>6yG?7%n&Jsg}~txy0?fIN_7S>*4Ksr z3bMdTQLpi7RuGh50m#{D5|klggE?fFOi;!pLHQL)P=@fvK_e(T=tlmBaVD>#p0nRv zr(J|41vpFmjs?jBFo3j zLtaS7uHnb^L4xOaWOjckJlpNbJV})6lu5Ugdgkj<5d8ok?|CauxA;JJK!Mn80I?C1 zIPu+A7D1JX(+&w@w>=SPoqtPuPGMTicc(}9 zhYC$ZhyP31+_>US&+{I~&hNLQd;DFn$F>krd}&GdX_iO;-;mz8)l_QHL-JHVB90EE z7UjQUf17fDM-unc5Y0rT5Rde3UoyJa<&0hEqt(o$9&=#-qE zlXCiyl+yve{(p>lyvy4ziS1dtRAGu$H6M~8Nk>E9YX2k36@^^I<%9AtadqdRX=Y)Qp*`>)74?r`oTI@<2 z|GY3I6o2ZBR$dwm?hw3Foer}Z0f%`s?$zLMoNks0pk7D@#)Q1Qn(`fKp-jn3AlXNb zU1qwnkrTZ&qQ_hGzXKZ^yvYE**+jCzEJ}I3C5Zh4}UxrYt%v1x>(7mPLm(S@btFSu`#iFg{Io2V_au zHH12rx>f{Yu(Zt&kA=>Vu|6<~; zQh!c=I||>oSmzwm{6%EhtdwPQrq20C62;k}WO4RKtdQgn>mWKpqQ{Hf!9@#e%O-S` z6F2<2K(a)C>1~7aovQYdur|$QMhd~Sh94_~grkG)XZUQ!kXTrAOlJa2rK@CAbxumO zR+%zn)E2>#4npb_^o?Hh$lvI!M?TS6&wtn{K`#LgKOjk=kEFUpz?1}c#3&@kYwSp$P$_CtEnm0Xw*JqAJTgqbdOp=~Q{--?qj-y7cue^pYk<*vgrMHlg{kQc*bmUrQ*@0e-8=bt0 z9ILF3L?h(uwktCR$Ni_s<85D-e@)P@=d)kv@XOBb$xZ;@I|yw}CK!LMM+F@qxEs$| ziN;?C)tNp>lzg+vl5e0%z6p{}yW@O9l3BVI=pkXOycq2%1?;AbCVxI2v~7$&`i3dW zJlDpa?|Y(K`rWXl#Wq{&W=R30WZ17K?(9(V&VF6n*@UC;^Idi}uJ&XF&rGNbOP4K` zMBd)>&OSWmF}q^pzhmxo@nPTV!`=2eu9D-;E@@h!l)HU2vC1Q~-Mixx_-5jMOUr2k@q)yQSSa`3gvDad&|VcxSDqNwH~S^CGicu)Mo>Rws)Qs zO+BW?rK5g-iEbZmeeb@+dHrA~g|Gd21^dY~;K#{xHK;$CzE(n(-{AGWs>kj(dd-_l z_Zxj>!hQJfH)7NJ$rRuoyGQ?f7KP`z@Au|UA$s}B-)Go#PJbIV4a!DH;WQ#tnUPZPOC9!-QkRvVJ5ze-+#o;vxScP+wL&? zQh2aSwZn7q@{iCKk4HxTn;p-vimUuJ!Bw_Oc#m+8p|;#(n7Idw?W00$A5C_#@5nvO zWXs)M=sV<4ZpSCOWJIQ;XU_?Wdt(*%@6;Hd$ZwmBLVp*clNrT>*K5yo+{lD{%fuei z=T4TQK@ZuGA0E~APIK)4TcTz=<_#D`H}x7DAI3ISZD_;X(2Uy9jM&hOwxNyA8~ReJ z4OMcSJV>O@)+}Ss^p=0Qk3Ai{^WQ{t)*MDB7aUSO)W{@J!Y0>UK&BcqwO{{4BsTe zN$NSa2X9#(oy-57QSP}TIvdSJWwl=wo0-F%G$ghsSO}9ojX9*kq1+}SEKQM(kNB~R zYve2}eQSw-4bd+N66tVzSh+JMs9Qsj7>QEtH0hZ6z)u;K2{vNcj@v-YjBqMVoq(E2 zL4Ro0kQQ=A42uRFC`B|910v9&Esy4N9_U@AIn&{+(s7OmctS@-_?z8^tW86f5c7IS zXuWCBD*q^c{Bb4Qe|rnmK>B?4rvRXbOc-qA)RmB}TQZYg5^&S%_LEZkp^ygmBndijB*2as}iL@5Oe z_(GKoZ_%JWXWdh=TC-?DuQ~>Z`#pfTu@K_&l*R_-!@MxG&ufPEzb7GXXHp;)GCZb4 z#I#U)23tCcnTesv$a^5!%7ar;1ApV*vWQcrmx1O5kJ@$<^densJxM{N+9YKrbA13j zwg6Yj2tT@}nRTf+rrs@2O6||!BAkDr z%jSGJs}$deSkHoZ`~(o!CrKz41U9c@*6&3z>mLAS{b)zb$`WQt@_RQYG=Fg_vw?D> z(J*V+z^oAsvna|CV%4aIRig%0{jP~sI}}zWxv%_gN31IUX&I!V8$uoH386wPiu*65 z!kRDli#3Tk%iE$%88a-De@Zz0x4~EmstuIie6LXIuC_&K!_r|QdW5|^jv(m7Tl5|Xr(SM(r846UHIv+a= zIj{6mC6dCOf&@jgv2-4da58c>8(%3I!O)B?^!vfO9{!0ALu5g88iu*(Cdkq^C$ zY>$1|hZLCqS;-1yrgft2n7N(E`o%Ju0hiAz1o>Hs8-vJjg_ydS6=on-W}I{#qt76Yk1TUQezL(G9wYq3m0-T)V99oOVX7 zGSW;6;izSZ&l<#H5G(LkpyD^OTE^GoX8zBeDhVhqQ5h9%WmK$|QH4@|Ro$JSY*fbV zJmm}cfho4iU&RlkcjX)S@jiZhh##1+ri{rx%b1b8yo4X0;D5(W{J=EkWlX$MMtxEl zFQm%%@B|2L{cxfl=<|Dr}H`b-_Y)nd~SR^Kaw9C z-vet&HL-u-*?My9XlYZXz znixv7QlZlhJ-o28+Gw^Y*)yk1!Vnq=Kj-gMJ@RiPk$;X7uQA)BQ_%PT7nXu5y~Zht zU#A-~lCe#{c{A?~>PY z@Vq+nT`ZcOwC786W0&E~d(svgKc-#*1dLrQxjGYUEiJXK1Q*ZW+eIBPL$G_I{Yktf30*w60Qy8Z}DGJYOVbG{wAYU~Ww@HZ|@Vp*N0u-D`T7kb%mE8&>+;y?SBVzJhB!ZF{ZiwCy&qIn3=TG=G4LYqi>jUBw(U9tNnv$f+1Sb;EXGEGw%m z_^Wzx#Lzax;1?GG%Cl2?V!PV zN1^4l8f87g?4fqhT3cMia`X;|FAz1j!@e*qT&1JpqCem!vNu)Y^slYx!dg`wBAX4RX9t{Q7g%hTanz!qMZT;W29 zJM|4tZ7RlO`zx4up0`8Jn8M_&FE?sQ>qPrR?Or402sQ>AXa{Snc;3V%gn46g>y75k zVA@_-Ytiy;2rxo*76#qHA`+IUfq!MWgyBA~o9Mu)-2;08TCyqmyIsUQ@Kg5LP(|!h zrdU@Dw0NcFLI>h}9N5KG*bpE$Q#b?(^rUO%_NY>@>Q((kA0Ezxu-l*%NKN1MD01(# zD79fv;t3kHK&K1TMqv_HU;{&=%cF4Mwbn2yDQ@6Y67Y$EV1%M`jcPzqbAMnV7fBVq z?$ld=0`)4+ZDwI(Mt-+fYtBY+iWhd>4*&o-fiABtw|T?A=>rsLebJf&OXH z!L*`wFrN)re2$Gpw+J@uvwvfsJU6A06I>n7RUH)qK%Sb1dX+-S7I|rG;4BN65{oku z+w8N5NP_2>lkpXMUGk{a4NQK{@X}~5(eK%?i+D}-<@_B7OWE%N$9XVo{yNeT#Qs(U zV_(;bh?^w)bObGAFtpPJk0(-?1|kszKtJ5@*;Krxl9B4GaLx1V<$uOH3l9`rd)*1n zXcgPz-O-y53Rp$bY5*MX&Z^h+5p$fHq*pqTB8m;j>9`&RE4wO(9`ZwBtqq(4(=aTp zIZdD(o(8f&#E>d8QTDMpr2YWvE^{);>jM*%ftoIZ(2hB{{ZH6o14IN*%W0tHS;ki0Ye-6(Sxa37mOAyK)JL0dP!QQd$$a1(+DG zVKYvuXuy*$@DCIZUR-u=doL1~4Q?W*wQq&dE+dR~oaguLp+D;bA!9fa}W zi|~cE1Pxyta+Tr5hyB6G@+%lvK4kZ;MB%gp$AK=IUJY7?A%B3p2h%{YHxyw(S_L_b zhI#SAo98cIJ5P64G(Zxm@VM4oA-1b^^A&Fc(Kk4$u%69?Y-iPnN1hjwimNrJg-x8a z-@W{m%U5n(9(%I}*pK`d#PD;lw6(@6!i3F;Zl649AA@1N0S64D=moscLQp=t=K5Z} z>S0MyA3)kDD_my>C9Yh%V4qzjDagg&y8NzJx!za>`2Y%BYb>@1vh}f@#5Scs3_k>R zULxIhklYZ$`dVE^51}UjQ^{p{~tSXY<~Ove?OCQ zGhijhxAp(`<)0Mqw=rQG|2yM9aE3nn=!pMEP8>Tve^lcC(IY1iv-dMzHh(dHaCq$QCqtU{`(Tn_jJax zT+8|ZJ~Nh^wQgpt4>Iz5*7_hTzvrwEa`O9-^}&$*K5Tt3Oy6_X%@M0MVtp`TW$^b9 z)~StJAB@6pD7C|K;bq?1*ncCdj#(d!SvTrcYslMaeKKY>Zde)k7Zw<&0=NFTDlpFl z9;X7M*3Aj4MvY`{-1;4Sw7a6Jb4eQX5Oo3;ls^a@NyBVjalEatWPcL z{SS6q?oP|wO;5+wQ|Ns{KHa6B?zLPf4xf9}qkTBWYsG!Ij0a#D$A9ut0`@5`;o9m< zt1;tp>N&zbx_)L1WJTLTVmq@G$s*g9~$@{n9L5{)aw zzHsRD5lq8JFMr&Eda6|Xy5Fi)a07!8Y$ub?Ok}FKK^c8^6WF(fv-4>x*Juwf^Vo4< z7)*mkNH5AGa$J25Zj=TGGC-v=i4A0FY?q6}5*#ZPlpiV;T98U*rQxpC@Fxxd^#o-s zR30;i(cf6gxC;fWiEWQ$#)rx{3I#bC2{})NkYf?FWPc^tN%^3P371efHca5a0|<#JvPVGuh^TQd_5rid+L zUh25`i0R<1kUi*4nDt!%?}1SV-*Q4f%f(mA*AZz?>p%G7=6#n#&=ScG5S7EuXbu(-$2;}yB??S zyQsi~e6^eE?xOE|=+$oezL&o5q3`?XJIFDQ(L~_MAOvJzd+xgqwmjXpZLP{rpgcanYkL!{cU>1O}#_k&(ik`^nd*g`u=75{w96@3VlCkxff~H=PmaVeR+%C zUZ%HiYYTXVhW8GAze;Z}SngNpXPJiaW%_=NUVX)KuTyImE%#mea>;VPi@sb&=J1|H zG#5Ao5~mSCl87ijo{>bkl98+;Yuz29FF7LCS)@dt#6|O>#HZZLn1v5_(!(+NaNN3^lRZ2MLTMI%FJ`SJZg1SG zOpvs?F=MrMS^jSE)Zc@Y6Tj!I1D_Ii$+Y&0S>3p`Co|OAM@PPA;9Pfa7+yYxboMR` z;Bjl>fH>mq$-wb0L+8tDMO>xee}ByiT3$pItx$y(=onL&4kL+5qe4{BfXMHv2N=C+ zZN+O=01w0uRAy4tu!_aQ_XX{ z)`;^w+L&QbbGwKpL>O;%z0Kv4n%)fNByXydV`u^@BCb*_^EaHpui8aCet%7o+B@u% zFN4(L%-C1uR~#SZI2ZQ=wzwGRv1z$-Gn`M|p?vE6uZiehcwA8!-BRj`?UG|*X3LVz5i^_^qhec`f1J|uc0B=1mS|PZ_`Mv|2UxSj^ z)w1}GwHDU=np@%fs}>R*@pX(_6kuIe$5}Mrdq2)Sk~ilii*9LS{ewZzA(#CYQ0{@1D#v+2HXI zlmH8sTmq=o5fN@Dqz&gGhaDM?(bL3%Vf@MUPI)>nb zaaCH4ig+?@i!WYNOAKtxs36k6;d?Q$f{I^{RRg>`HItT^owOm)Mj51j<%?uZEh|xk zO*Oh1`$8RPRN#e_72Ut!H}y{CnnZX2U2gkTX<{d$UBZO0MFNvE!IWjJN#_(mUZrDpo$6|A=&cAePX2(XHZUmTETAs_f5RfZQ5K@dBzaPR~0^xPy0RR0o8`2FWJ^o)X z%O$T>rt}yqe(M@Vl}kt%-Nf+pgn3~ASQJ3S(geDNctg}sF&KsK)*{Bfg;x{?4eHjg zM-(BNn7iF`qp|z#W#G0Y`%R}-;~=wWh(VPq9Dn`Jb{i~ykbSN2B_qog?4R!;R#5Mc zMwk&HQ{&xax+|GFhx!jIi!irxY?MVPA(^lrhMI-$u1M3&F0E;cM56Rn4A{~X;qz2` zA!QAuaR@8~M?yZQ=hM;Qv|tE)Y^~vjj&~S55{M>7Z61!9l;4@B+g>DZE-kHlq(lSqK{rg)ktRK%JQm15Iutg#Gyp4fQIy zx`$zRW3=?h9GrWCmJ9=~;%JlKKph}Cbks%K5};=QUwsA?486J*w4fx@ovJutqJO10 zJK2Z9(h;FU%6fbb>O*lfw+b3YjNBH-C2uUEkVdwXLZU*q1Se?GVIJrb(BrBX#0k

5g2poU^EZ}+5l5PtiPKy;!m+DGR?^& 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 delta 44370 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 diff --git a/dist/twython-0.9.tar.gz b/dist/twython-0.9.tar.gz index c902579b07f6493e6942b9ab597753dd475453e3..3647d59dfb058c48b1b0d7f6264a45587bf92382 100644 GIT binary patch delta 14192 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 G-~j;1DW8G> delta 14194 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 Date: Thu, 10 Dec 2009 03:12:24 -0500 Subject: [PATCH 133/687] Proxy authentication support for Twython. Experimental, needs testing - pass a proxy object (username/password/host/port) to the setup method, Twython will try and route everything through the proxy and properly handle things. Headers can now be added to un-authed requests; proxies also work with both authed/un-authed requests. --- twython.py | 60 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/twython.py b/twython.py index ffe31e5..5c2de4b 100644 --- a/twython.py +++ b/twython.py @@ -52,8 +52,8 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, username = None, password = None, headers = None, version = 1): - """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): + """setup(authtype = "OAuth", username = None, password = None, proxy = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -68,13 +68,19 @@ class setup: self.authenticated = False self.username = username self.apiVersion = version + self.proxy = proxy + if self.proxy is not None: + self.proxyobj = urllib2.ProxyHandler({'http': 'http://%s:%s@%s:%d' % (self.proxy["username"], self.proxy["password"], self.proxy["host"], self.proxy["port"])}) # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) + if self.proxy is not None: + self.opener = urllib2.build_opener(self.proxyobj, self.handler) + else: + self.opener = urllib2.build_opener(self.handler) if headers is not None: self.opener.addheaders = [('User-agent', headers)] try: @@ -83,7 +89,13 @@ class setup: except HTTPError, e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) else: - pass + # Build a non-auth opener so we can allow proxy-auth and/or header swapping + if self.proxy is not None: + self.opener = urllib2.build_opener(self.proxyobj) + else: + self.opener = urllib2.build_opener() + if self.headers is not None: + self.opener.addheaders = [('User-agent', headers)] # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -119,7 +131,7 @@ class setup: version = version or self.apiVersion try: if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) @@ -139,7 +151,7 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) except HTTPError, e: raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) @@ -223,7 +235,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(userTimelineURL)) else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) + return simplejson.load(self.opener.open(userTimelineURL)) except HTTPError, e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % `e.code`, e.code) @@ -440,7 +452,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(apiURL)) else: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) @@ -537,7 +549,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) except HTTPError, e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % `e.code`, e.code) @@ -830,7 +842,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(apiURL)) else: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: # Catch this for now if e.code == 403: @@ -1092,7 +1104,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) @@ -1124,7 +1136,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) @@ -1188,7 +1200,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) try: - return simplejson.load(urllib2.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) @@ -1254,7 +1266,7 @@ class setup: """ searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) try: - return simplejson.load(urllib2.urlopen(searchURL)) + return simplejson.load(self.opener.open(searchURL)) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) @@ -1271,7 +1283,7 @@ class setup: if excludeHashTags is True: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) @@ -1295,7 +1307,7 @@ class setup: else: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) @@ -1319,7 +1331,7 @@ class setup: else: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) @@ -1531,7 +1543,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) except HTTPError, e: if e.code == 404: raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") @@ -1570,7 +1582,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) except HTTPError, e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) @@ -1610,7 +1622,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1665,7 +1677,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1684,8 +1696,8 @@ class setup: version = version or self.apiVersion try: if latitude is not None and longitude is not None: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json" % version)) except HTTPError, e: raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) @@ -1702,7 +1714,7 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) except HTTPError, e: raise TwythonError("trendsByLocation() failed with a %d error code." % e.code, e.code) From f82702e70643621c0a82ed74e94a912e2b04115c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 10 Dec 2009 03:14:33 -0500 Subject: [PATCH 134/687] Proxy authentication support for Twython. Experimental, needs testing - pass a proxy object (username/password/host/port) to the setup method, Twython will try and route everything through the proxy and properly handle things. Headers can now be added to un-authed requests; proxies also work with both authed/un-authed requests. --- twython.py | 7 +++--- twython3k.py | 65 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/twython.py b/twython.py index 5c2de4b..f9e94bd 100644 --- a/twython.py +++ b/twython.py @@ -69,6 +69,7 @@ class setup: self.username = username self.apiVersion = version self.proxy = proxy + self.headers = headers if self.proxy is not None: self.proxyobj = urllib2.ProxyHandler({'http': 'http://%s:%s@%s:%d' % (self.proxy["username"], self.proxy["password"], self.proxy["host"], self.proxy["port"])}) # Check and set up authentication @@ -81,8 +82,8 @@ class setup: self.opener = urllib2.build_opener(self.proxyobj, self.handler) else: self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] try: simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) self.authenticated = True @@ -95,7 +96,7 @@ class setup: else: self.opener = urllib2.build_opener() if self.headers is not None: - self.opener.addheaders = [('User-agent', headers)] + self.opener.addheaders = [('User-agent', self.headers)] # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): diff --git a/twython3k.py b/twython3k.py index b05b1f2..5c1653e 100644 --- a/twython3k.py +++ b/twython3k.py @@ -52,8 +52,8 @@ class AuthError(TwythonError): return repr(self.msg) class setup: - def __init__(self, username = None, password = None, headers = None, version = 1): - """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) + def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): + """setup(authtype = "OAuth", username = None, password = None, proxy = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -68,22 +68,35 @@ class setup: self.authenticated = False self.username = username self.apiVersion = version + self.proxy = proxy + self.headers = headers + if self.proxy is not None: + self.proxyobj = urllib.request.ProxyHandler({'http': 'http://%s:%s@%s:%d' % (self.proxy["username"], self.proxy["password"], self.proxy["host"], self.proxy["port"])}) # Check and set up authentication if self.username is not None and password is not None: # Assume Basic authentication ritual self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib.request.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] + if self.proxy is not None: + self.opener = urllib.request.build_opener(self.proxyobj, self.handler) + else: + self.opener = urllib.request.build_opener(self.handler) + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] try: simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) self.authenticated = True except HTTPError as e: raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) else: - pass + # Build a non-auth opener so we can allow proxy-auth and/or header swapping + if self.proxy is not None: + self.opener = urllib.request.build_opener(self.proxyobj) + else: + self.opener = urllib.request.build_opener() + if self.headers is not None: + self.opener.addheaders = [('User-agent', self.headers)] # URL Shortening function huzzah def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -119,7 +132,7 @@ class setup: version = version or self.apiVersion try: if rate_for == "requestingIP": - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) @@ -139,7 +152,7 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) except HTTPError as e: raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) @@ -223,7 +236,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(userTimelineURL)) else: - return simplejson.load(urllib.request.urlopen(userTimelineURL)) + return simplejson.load(self.opener.open(userTimelineURL)) except HTTPError as e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." % repr(e.code), e.code) @@ -440,7 +453,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(apiURL)) else: - return simplejson.load(urllib.request.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) @@ -537,7 +550,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) else: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) except HTTPError as e: raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." % repr(e.code), e.code) @@ -830,7 +843,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open(apiURL)) else: - return simplejson.load(urllib.request.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: # Catch this for now if e.code == 403: @@ -1092,7 +1105,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: - return simplejson.load(urllib.request.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) @@ -1124,7 +1137,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) try: - return simplejson.load(urllib.request.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) @@ -1188,7 +1201,7 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) try: - return simplejson.load(urllib.request.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) @@ -1254,7 +1267,7 @@ class setup: """ searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) try: - return simplejson.load(urllib.request.urlopen(searchURL)) + return simplejson.load(self.opener.open(searchURL)) except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) @@ -1271,7 +1284,7 @@ class setup: if excludeHashTags is True: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) @@ -1295,7 +1308,7 @@ class setup: else: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) @@ -1319,7 +1332,7 @@ class setup: else: apiURL += "?exclude=hashtags" try: - return simplejson.load(urllib.urlopen(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) @@ -1531,7 +1544,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) else: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) except HTTPError as e: if e.code == 404: raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") @@ -1570,7 +1583,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) else: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) except HTTPError as e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) @@ -1610,7 +1623,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) else: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1665,7 +1678,7 @@ class setup: if self.authenticated is True: return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) else: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -1684,8 +1697,8 @@ class setup: version = version or self.apiVersion try: if latitude is not None and longitude is not None: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json" % version)) except HTTPError as e: raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) @@ -1702,7 +1715,7 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) except HTTPError as e: raise TwythonError("trendsByLocation() failed with a %d error code." % e.code, e.code) From c0370abed66b6b778ff740756bc9bf580b0cb0b6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 01:55:12 -0500 Subject: [PATCH 135/687] Adding installation notes to README, thanks to a note from Idris (that I, sadly, missed for the longest time) --- README.markdown | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.markdown b/README.markdown index 8501a38..9dd6689 100644 --- a/README.markdown +++ b/README.markdown @@ -22,6 +22,17 @@ Twython requires (much like Python-Twitter, because they had the right idea :D) > http://pypi.python.org/pypi/simplejson +Installation +----------------------------------------------------------------------------------------------------- +Installing Twython is fairly easy. You can... + +> easy_install twython + +...or, you can clone the repo and install it the old fashioned way. + +> git clone git://github.com/ryanmcgrath/twython.git +> cd twython +> sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- From f74aae38a991ea221e88aed7c920eb7ab27e1163 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 01:56:48 -0500 Subject: [PATCH 136/687] Adding installation notes to README, thanks to a note from Idris (that I, sadly, missed for the longest time) --- README.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 9dd6689..c649b06 100644 --- a/README.markdown +++ b/README.markdown @@ -30,9 +30,9 @@ Installing Twython is fairly easy. You can... ...or, you can clone the repo and install it the old fashioned way. -> git clone git://github.com/ryanmcgrath/twython.git -> cd twython -> sudo python setup.py install +> git clone git://github.com/ryanmcgrath/twython.git +> cd twython +> sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- From cdba60ecca808f820c28ec51a09e50254d4b7a61 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 01:57:15 -0500 Subject: [PATCH 137/687] Adding installation notes to README, thanks to a note from Idris (that I, sadly, missed for the longest time) --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index c649b06..bc3371e 100644 --- a/README.markdown +++ b/README.markdown @@ -38,7 +38,7 @@ Example Use ----------------------------------------------------------------------------------------------------- > import twython > -> twitter = twython.setup(username="example", password="example") +> twitter = twython.setup(username="example", password="example") > twitter.updateStatus("See how easy this was?") From 0b552d4d0ecbb6a1f8f185c2fcb3138a9b821cd5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 01:58:28 -0500 Subject: [PATCH 138/687] Changing simplejson requirement notice (Python 2.6 doesn't need it) --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index bc3371e..cad0554 100644 --- a/README.markdown +++ b/README.markdown @@ -17,7 +17,7 @@ Twitter's API Wiki (Twython calls mirror most of the methods listed there). Requirements ----------------------------------------------------------------------------------------------------- -Twython requires (much like Python-Twitter, because they had the right idea :D) a library called +Twython (for versions of Python before 2.6) requires a library called "simplejson". You can grab it at the following link: > http://pypi.python.org/pypi/simplejson From 00246ff4ecb293364c8c5f2ddbb68cccc3a95ab0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 02:06:10 -0500 Subject: [PATCH 139/687] Cleaning old builds... --- dist/twython-0.5-py2.5.egg | Bin 15702 -> 0 bytes dist/twython-0.5.tar.gz | Bin 7977 -> 0 bytes dist/twython-0.5.win32.exe | Bin 71547 -> 0 bytes dist/twython-0.6-py2.5.egg | Bin 15685 -> 0 bytes dist/twython-0.6.tar.gz | Bin 7130 -> 0 bytes dist/twython-0.6.win32.exe | Bin 71809 -> 0 bytes dist/twython-0.8.macosx-10.5-i386.tar.gz | Bin 35001 -> 0 bytes dist/twython-0.8.tar.gz | Bin 12644 -> 0 bytes dist/twython-0.8.win32.exe | Bin 81487 -> 0 bytes dist/twython-0.9-py2.5.egg | Bin 63786 -> 0 bytes dist/twython-0.9.macosx-10.5-i386.tar.gz | Bin 58121 -> 0 bytes dist/twython-0.9.tar.gz | Bin 16158 -> 0 bytes dist/twython-0.9.win32.exe | Bin 90830 -> 0 bytes 13 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dist/twython-0.5-py2.5.egg delete mode 100644 dist/twython-0.5.tar.gz delete mode 100644 dist/twython-0.5.win32.exe delete mode 100644 dist/twython-0.6-py2.5.egg delete mode 100644 dist/twython-0.6.tar.gz delete mode 100644 dist/twython-0.6.win32.exe delete mode 100644 dist/twython-0.8.macosx-10.5-i386.tar.gz delete mode 100644 dist/twython-0.8.tar.gz delete mode 100644 dist/twython-0.8.win32.exe delete mode 100644 dist/twython-0.9-py2.5.egg delete mode 100644 dist/twython-0.9.macosx-10.5-i386.tar.gz delete mode 100644 dist/twython-0.9.tar.gz delete mode 100644 dist/twython-0.9.win32.exe diff --git a/dist/twython-0.5-py2.5.egg b/dist/twython-0.5-py2.5.egg deleted file mode 100644 index 8b246282df73b8dd5a616430e2b46606401f9c4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15702 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 diff --git a/dist/twython-0.5.tar.gz b/dist/twython-0.5.tar.gz deleted file mode 100644 index 83e61dff7498ef0dafa7725b8a5f14af47ee454d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7977 zcmV+^AJ*U>iwFqpZFWim19W$JbZBpGEif)ME_7jX0PH<&bK5r3`D*?U zcAoK_&*#R@)zdik#m>{|bUHkWge0CRQYA<`>P`Op?JfWkq$pXEB{}UxGl?k@*j?-! zb{8O&+>et%6mIXd_dCzl^x1*W>-~NC_dx#L+1-%SUm%Fd__YMwT@5B9plC^eG)_dS3mx9`~3ae%XP^AU~g}l{15hb_Db@9 zwX?VXjO~1t|JpwnJaIfHakl@T$HI@oL-w-0Q+w|W`5{xfQOh2IlGLu#;n0c4hwOxf z{GK_X$G9WL?A{r(Bw~*1av|8wy`LmJW(bGa`o z+HJehu!BMvy8YNm1|5aHlf;~NhK>MmwUZQX#fR(zv||_UKe3^@xy^@;9~?3S@sB$m zMjjV_KNRhdC$)FJ%R>PuFV1h6`cgaP!j1h=LPNWe*w{8Z!_jE^)5H(_#OH$fp&CMs zsn7N%J{h)ib{|Ix&j{^u*GUD3`@JXyAtVDQVNo&wq9vW#7y`c=J28&op2s|_C$eoi zLV1CJc$>Z9apL&-(2{{4_6793<#5f}HFsinz)r5tTkI0~-C{g(Tg;0PoZx(ju)3V} z(jXW!H@X8%UYp$veBhj5!!dJ!mfU zMUnwIuitUTp&xJxM;_)fYcUbwKnQXgcu1-MMPwpLdp&GN&yb9I5M-!^4vgP{@i`(2 zxu+R>$6?Ga=pL{Hi5YOmYq2P1qeuw9iwtmrfB|?v6ix{IJx}U_MI#_dL}~2u7NCmK z{-Blf0z`OTAIe5(N8y-+plM?Ma1;YmNE~{^jsWWJ2T@SPCz2Br>1Y(i2{H^uiJkAG z_*S%7H%*9HW2H1BCq@xa6RkNQZzcwsVYyWAi-s@&;sa2xEwEqIcS8ch;W1=mg^Hyr{pNifg`gd_v-O&|-&G;M%Np#@57 z=*Kb0zsx(p4La_`eU9$2%}xy60)B&&O;FRuYUl8ten zXcQInXBQv%Z>b-1YWGAgOLHogqzKj+reHGURir6xD{i$icLppRgF<=4%h>M^Ky^Lt zu*1`);V+O=+{+MI0<0t8{{btt+U(~jg$5ymX{5`1^f&~p7X?9dk0urP;qBoh$JBH* z8jt+;NGeMd_vw2lM=*osXP+Ew#XiF1@-!R=D$&@Jl7Z%aT`A{L$sEdN(_$ctI0OYf zvbY=-+#5MUfcNn7d(ATBb~?ftY}LA;64YAd&|&CB{0GC(91D*5JhN2&=ITz+C^!S z&oSV`oqGr7Idb# z>oW+10!bhRPksoOLpfRIXny2lG+uF-c>y7#I%nj=EzWblaRu`Zf;eO!-~agj@~8K; zHv!nb-}5;H->1OZAR3`gFlp%Q@R02RS>Ho4QD}M&x*nn`pQN78LziPq)gM&4&{mHQ zT3lYAvy&0UJ05;Fqjs~)O*Dd70~%aMy@WK|r0o!4>d+UyDC@k4ycD99YCx##X{aI% z>h%V6s=6%>F2?NQ&yxRtI6FDLI9pHsXGZ?Nx4*Zm^Z&iqyRRYt-#yqp_?rK3;<04JN9V1z_2;SU%1_srm)6#vhps07tknv6=4M>P+Vje# z^2yqq`D3kS=82o(SSc^8tv(N2F#oI7j682cTC{+4|D$! zqc7;m;d+O@h`Z2^`JGR`5^vU?ef|8o*8ipbkH#C$|95xy_qF}+zJ9rn_P@8k`_=w8 z@p=ADClzt0>xZ3@3Jz7UD8?{kO{wp+*Lp>@Cv!30zX_LWG zy=vIl%FDMD9MzZGAc0_lCk9;ds1LZ%668%p)5oOq~2onJY6E8 z_@Nu59=AsjAv=9;6Vr~^O`LLg>So1R0#)1Es?RP_<1n}C57ee*qxp)`vh8E4(Ip7g zW$02PU4O>gzoIBS-eVVzA2ynYwJk^(;QuXc%o%&(o)(Ep)*2PkYC^NTkID{Tyf7Z~ zPc9!N?401AVcEez=V`kIt&2I_FW#ttSMz&dtZ2*VGQelpHkcZiZ5>x?<*2?!9P6F` zwMTgR{hu)Zzy7-a^Qq*oYeK7v1ZT&82Yas$UTgX9@9nQydNlXhXSk{3KJY?IfsiqWQoPjNe58Mg`Qk&3=&M z8^Asx3RG5N|V#kL-?u@Z7dV^{E&O9)eImM9A3ckzh@|R-;f>njAf7WaMH2T~RN7US5|c0ndb@1u}o(=B}5hNmD7_#b)HGumNMs=fk0oHkJP##N7!$jKx7XMBJ`zxWRRmT^`in|`4=hIB3$q` zxFmtpQu0{@Z9FCE>+bn*GKSG}i5^V^Jd>S7w>-4LbzoMH>Mub5_0kU}m_%Bq(7c|5 zk-6`@P|VgQ5OPlb@01&vP^y8b*4%)Ap39%Fs}`f|{hXafYE_tcC0E^P$$gvFx%ArU zfaXiJG1@zZ6qygzb_0j?aN(7vJ-yh`ZnqIW4=SZ0m*NhK9DwX7gj5)p@RTQSqYJ-M za=z%tKSA1mDi^Ika3>fxn$-~6j_280KN?c$tx2YU{S@@9Ra7hkxEytvG8zT4?g#m} zF+qp{0?Qo*2vnE0ljrvCz%{CwpA&b@^n!i#%HMbyu zY))tgk>fQ?fU+=Cp+lg(Q5q&4m>B)u*mh&?p)!FhXw$lKvuRLQ_83^%pvP*i$Yy77 z_KGO!wvDO6Ds5)yIX>u~tS!+_gKQnrqy3i%wGDu6%+0JGKomTo{wuhq#H?Y`G>a67?Z1oj6cfUw{>)&~KA zMTBTQRWqfw82C%NnFIa{vyLtJU$(#oJVdcJ{!4%lOEi6M>N;a>x*kqyp)t~CK?fZM z>O~<4L#%C6!E&0}jFe3x~EE}Hnoz?yiWD|{FtZ&89 zw*v4qV6JSjJ10nalXSFj1L98!w(W!Q!oQ(tG=FUxD)a%&l7btxuR-Yq+_e!JNJQxe z$p8GRC>C%{dBbu`Y0GLPWj9lnbTS;mk3;PzEBp<(OmsNfu~bvD zkkgwXp9SW4<%Kw17x1db1OaK7|lPDN%;9`n|wzZhrp37~1YWG5x zqDHvg3$6M-2AgYV*-IZ&MBH3Z(rQSCWBI_Ngf<6&_hfj0TME1Uc&dk0N_UlZ1K+)Y zv@L+dB6n{kdtXLtB*VDqj0lIFC?tlI=I7ibsY}>B0C@{Jc<713fLrXvi`#oA?n{$@ zSbeieyH|vUKKe*@F1whS3{qf99BMH|RBaK1nIuV>=d?M#lGkN6EyV2H^yY+}<;JMK z6&6WmmWdFfsgnHsJh{q6$)qfOZz5TdEg-lPnBO!kj5clGvzpkF@~FyY0>TVPDKIWI zn$kd1gQjJ*fT++a(7{OtNQaqbQX43Qm*ZN`nDUfvx)H2r$i*yk%IMd{^hsRW(x0=R zI5=B|5w4qxB$9iP!AFB6?s+{fX>1ECL@;SkMUFXl5lG907(%dnO5|wEo`b=1!d>Jn z(ovgl?wr>Xl-N_ArRVhQRZ!-4+iz!h4M=mT90r3!By0d_Lq~B0=SDwfa_LYU<4#2p z(MCJm?X72SlS!nqU9LJxFNkgWTm#FbY|;2<&x`0}E)aH;%El|Cznrax2 znHfrSEHf<>xHA8oU@-WBxuJw$fr$a4p8G=J?0e-1c~yorQ&LDr=UXKx-~o`aoZ&5l zW%Gvl^TQUYB9dvw$R4uB!e(zNTgeBIG42)|1!GDm>7Jd10&|)vw15n?5%?l$ND$2? z`#XENw=>NSXmhs828LV+r%&ZjdVvAD;bK(mxcJTk);;_o&I4X_AAUp0LhctRWK-)fW13aTHE2+I9}@IM9YsN4EB6v5=nJ?A4>IC78drIV1>E`L#nw@T zWCjc}xw^c*DY%RI*BQO{gbb;q#>&goL?F7*-PQIs&l?6pQ8P z*Kq&KRlbhEJs)2+aCr?Z$afw3S39h zn%nctR%JyD`MsX4Dnd6T8R9ZYxrH>qH~jNi*SHs0xSQn5a&j{>nW}j#&Vu4$DiYG- zE^fvTaUD4sIAJMBw&s``2~t&^UdZVZx4=u*YUGn)UzJWQAN?&O>~h#g;a(P|EFST# zLYm7mUXcKJrt;m5IkZ;Q-hn@wtktR-W%k`DMrQZq3()A-dl<1TJ{?%fmS_0m>Pe%Q zQ7$@iEE7dpS`L&Hk~@}{3ZWj|W3kGKN_fINM>{$N9-_fP7x5Gf`NlNWS@od<>Q!B}1%|e%weu}%uV$?i;yD@kVlVD9kh!N? z5X@c)mWMSMn;dYlcXy8sz!{3RN1ToY;0aX!w1pT=Ibt1TPs%1l!s zm};9VwZa0j8D_Z2AaGJS)>U~}Tz}R4gKPNby}bL`CkW+)5f0jp`rF-P5y+G*Cz2ir z0c}SGfk>~s)75vnOVsD4F=qAtXBwgwi##tFx6ED9^3aA^0+rtPlm|YZ)N{)3d{+l@ z9(~b%#|pv5taXNVy6PlJvOPtUE$Nt5j+HGydjahsY*0eDiX%L_ z;!PAp@ncuhrb%h;HMNrHMv^w$CE_y4DdLusOL4qw5=$A$vQkms~G?AU@b_Fa{*Plt;& zm25iI^XMdj`F7b^JInOmtpOcBmZiBWW{i434@HjnI+Nb#U~Z6A@ghd3B!u$b73&`s zhL`dj`iW7~Z5p<&Q>bvq7)fy>-fXsfCtpQJ#?Y1QV=!QL_sZVuV(ebdSjjra^S~Yi zzL(^L@=UJ6gDktfa{}M1lmV*#Of`cBvM#i<8h%%AowaZuE^VFt++0iq^T1k{K!7K? zs>eaKmS+xSCYyCD;`OVwvRb189_h;!FntEpb+P|d&V7xtS_GyGpD6{Sp;Y>VwMxQ3 zNAXhjZ3Xgp2=*g2{~+hR0lR|z+Rsd*{u`Y19#e6y4tMehbqeCe>&wNUV!c7n!s}6Q zox2D(-RtOc;pL^(WPX~8inXVucIjrLTE`NM34M{YTNa&E=O@`_k@0z2-Ij{(3#ZM+ zfvu}rm&0FtYs(kH^=Z|&rPy8`+Jk)GTC&Wv6yssC^n2=MxVKdG{<4dtCcUB~>-4Lw zl2&4nf7VwmeGut-s-{aRbcM>S+?Q11SW>Ouap#Wzk+eEMg4e0k5&`|cDV3f;`GqR9 zRF<{svqrIC1SpHp=ci)1b?8#WSXA0-otd861j#O$*ye>gse5Iax78dCj4>up}H5K3G z!hBT_xvY;Y0G<^#KE@+fAekK~Zo>ani1!gL5WFct^27d<7V{LVyNZ?-mGrWqt-Q=p z1pbh9S`+-{o{nsukjvRbZ!OJSIgWJ{Obg%_+OdvDU#mq0{k8<%v+2y7(q;+yXpT3D zWy5yi1vGi9_!hA|&Zc?|<~MwC(4ggWb8)AU5xjWZB!4n(iDM|L+{PREYdSYEhWv`p zKn))H{evS0H%>p>hftbJrZY4lU>)^CJ?11)hFnqdz{Rg))$V8j4HIZ+&CB3N2c<=R zJdU@g(05$mtomIKwV^}~a#{9z2t8x2KG5#MiElrO!rOefnu&O)Oq#@SY(PF5FYsXn zJhcRyi?RG>$V8vQf~WbiPL+I(<;5>U6-@CxP7!|M{PwFR{Bbp*Aiu|{!nJe9J?X*u z<|7Az*lnTEFCR1%N3oW7-AWi1J@q2*{xmPO7Z@#%rYPr43p2hvu$A+D62Zc2+>?x_ z5a|hdZ@#WWw2*gGLFCf4nYGHH3dIs-b9>S-->a$JNVZS|} z2eiZQ7x^83UlMTHcnC9aA@asLrIY!WosAt2_8j8)8xn@Zs1AV2&l>)3{fz{s z6!OfC{jqUC?9yBD%i6G(mt}#>!2ewy%sKmTQ9Sw=f5#!CiV>??@;8Hzr|i_PQsl3? zyGq^771r9kj($m+sTeX%EvJiEYo{q_B<%akkMB=UKKzV7!=Qg7q%D7V*zTeQIPsX+ z_vU}?UF%QdI1vAyzrsQ~m9E-S-cnX*#nJ9v1(X#%?mpa}R@ufa5ouH9vC#ki<~Men z2QAA!DALMCLT&2Uq#kz18fS@pvMzvy@inhJlr(QY@$ z&g307t3T#a)C&^wBmq2#i$`^5>|Ndp6(;{J*=i7{skhqgjp}zrNrkvVo62Mo8(xIU zx28Wsbd`+enwFtsE(sUzU~xtc^RmWmuWMl)3)T&dKH^HX3V+o`12e92X{{&5YgT@riqBR?(H61{I^XMYYDCWU#tHg&i`l+hVQRl7XSO`uzOIm|3`a=`&R!qQ7&cb zVd2*PTLJjFVz<+EhO~-ubDU?#{6s?HjI!Cx16s`Y!CO0jMl;#QGZ?P;W_)Vrl%afF zQ3fT{7u)K+>(yw98mGAe+U)d-*Z*Ace3U}Q#zbjvqL|(WfFMbwAvBN74QTg~@jKLB zs-t*o0WR;6fvJ(Ix*0>RbB~ zkx@De!zC^8zfezjZyQONkcVk0JfdYrn{O{Qy?6#BG>bMjFHcaIctd|QlKu#%NTOPa^+a;3O*^At|VJ9^kk4Ia8=3Jm5D-P z1+$h~XgC}kP3owrIi%fZS(?@Rsv;;TwuJU_)FI=%iEYGKnxY0thCyoaF&{x0`t%i4 zoen)?OKsY(wnF_!oYFfEe939PXm%HJ988w9p9LHUs`D;DtUUDMsf2m#h|`j6G31m)nnjZe z4J0K7LooK{Nr-cDPG3$_ZQ|>lyuqZIsMX*zXlwHsA{)2~F;)a~d>u_A-%%(L3E$q4$&Xbxt)YYcIN-}Xfq`(LB@kDEqn~(%J(??V>2H-X+BCw)MpxrntlGeSDKZ} zCGfPR@)llUg#q{w>62lTs;uGD+7wpv4WPW=us==i;8V>rLD~Zb6Vn6uH<2=AW08mA z5svgYhiZ{j5_r%&CCxl}WlJ%i<_GC0XUIlbIQDeqlC-m4@~MkhhSV~^hvq(v|Gd0# zr8O$8xR}g~!C8Me>T`&c3FO5S_HOY^Im@D(bGd}_Ca$pp>QG#Gi-0akS=fyafOW&KdBQgGnH1@!ccVhS35tA(WyE*A)ju*6*~Ls8k&wmAfOb8OiMnZ7C@wt? z3p-BQ9~3 diff --git a/dist/twython-0.5.win32.exe b/dist/twython-0.5.win32.exe deleted file mode 100644 index 1b36a955a369f3b4d0a0b94b4d68355dc9ac87cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71547 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 diff --git a/dist/twython-0.6.tar.gz b/dist/twython-0.6.tar.gz deleted file mode 100644 index e2e40b45339fd7eeeb7a816e2fb0d8e35392828d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7130 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 QX6@+gAE*%j!T`ts01l52KmY&$ diff --git a/dist/twython-0.6.win32.exe b/dist/twython-0.6.win32.exe deleted file mode 100644 index 2a43765fe94e4f4436a29435688727294866d0bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71809 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 diff --git a/dist/twython-0.8.macosx-10.5-i386.tar.gz b/dist/twython-0.8.macosx-10.5-i386.tar.gz deleted file mode 100644 index 96743b196539dba2c15a50fb8162d7973c2b01a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35001 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 O@BarJ@vu$+AOirE?qPNS diff --git a/dist/twython-0.8.tar.gz b/dist/twython-0.8.tar.gz deleted file mode 100644 index ab90687a29a548f0114f54f940c764bc8fbc9301..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12644 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 diff --git a/dist/twython-0.8.win32.exe b/dist/twython-0.8.win32.exe deleted file mode 100644 index 57f21944493abc5fd9dd6a6bcc18d11b34761789..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81487 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

FxT$O>sVRO6w?qs?vxN~ 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 bZvP(xRY?{a=1l<(^vwhX3~&4^4f+28^F0~- diff --git a/dist/twython-0.9-py2.5.egg b/dist/twython-0.9-py2.5.egg deleted file mode 100644 index 871b3c708f17f2967ce9ee9066b972995db4f838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63786 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 z 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 bF!$f$KMH_={PQ7%kB7&H{7$+3^X~rvOyG4| diff --git a/dist/twython-0.9.macosx-10.5-i386.tar.gz b/dist/twython-0.9.macosx-10.5-i386.tar.gz deleted file mode 100644 index 693c10027016d5a00278fa591ef2ef5689ade589..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58121 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 diff --git a/dist/twython-0.9.tar.gz b/dist/twython-0.9.tar.gz deleted file mode 100644 index 3647d59dfb058c48b1b0d7f6264a45587bf92382..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16158 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 V@A>~};mH4mY^4vZ1lj`z`X3-qY~%m{ diff --git a/dist/twython-0.9.win32.exe b/dist/twython-0.9.win32.exe deleted file mode 100644 index c1f416ea0d91f4d504b81d4322276e55593a7341..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90830 zcmeFad0bQ1^EZA&0t7`96%-XUYE%?dEMh@if-Itd1_MD<5EbwmaVaG3C^TS+*RLvj8BlZSvS@A8`9x#O*g(kI%e7w zmiqj82I>%oi4aMc4>-GF^=%c5Ow?55z%YrZSs~OOnTk{nm`!DskVl#1L_hT@!-#}7 z)YO}gwfB|P0(6h)o-X6t|l($QJj8aW_Kpm>biM0YRZj& zSPeaNRNjDcPQIa4&ON;>mX+^NF*N)viV8Wvmk$+FK0@t3eJ7OB<`z3YK#6W!Yj-sM=|)Q|q17 z0zXxK{S69P_FbN)v(C;MqlnANkJI-N#?jfmI*g$kk4{{HG#iZ>SAK5m>qvd$D%Yb3 zBz+Sg=)`=TIiyx?HMm<1ZdTWPFvw**qpEs1%1GjDY9C8Qg%e!(G*!LJ2Fba7u>}$)#7uqBct6-$P~zq#I_Tcav^eYjKv&m z*U=Pm-JDbY!fI7pe$y~aeZB&D%r5hgeHq#&=vxU5><-98Z8?P2xHucG)%7A~Z%nRZ z%-Qxt$rN%Mnm76}`XQ!}8Y~Phq{ixPF{6{3vW8l#x79V`4Rk7!(~>v|m2)A_tRZ#A zd@1nJ3%v~`vLnOWsfAI{39DrS-^aP0Nfo4ApYIFm_1wnA=|@6MDO{+t8XwK;Y0QD< zZmV^SrR_6Xj$|lZfCt$~t!SI$5@j`9S6kTU)api@od;*e3^zvaO4g^A9SLfJRdieD zV2yDD^HjEmxcG;-$d*T9=KMOu+_}g)YyT3ndx>Su7_8EC2_{v~XeMf8M<%wzgt^FC zafxTS&|->wrnXE&Bk0yoMBl5vml_SW+P<)0q1vX6K4^=J1HEO^%qJ7{_L)!m8thcz z8r!sOa^vDFpwP;Eu<4UuCFZ>y79ba_lt2zGmPxZu=$lek@o{E?%5a~Sra8n}&F;j66HrAkE z<$H+s_1Wa(^*(GJc)@(iQQkyI=8$K9$DlsWM#JFBRuKEX&R2=f?aL}isw%+8dRVy= z6^3Wl0an-kuqxi+_B0-s4$pd_N7JI8(8!+V`|2?>p)Y1nehX5QJXZha7i2KXCl^w=HYCP1`?Zx0sQQ>neZzoAy&gKw-B zdnN~5BoC8i--hnOkkYmqrZ`tTJp zvtK168hfHptCLz?7lD#huXN^Wtge2@n;bT}W6{Q$LM3q~U&L>(6Fnp#3B;h&%Q`FYW9?QZtTVNU}R@bp;ta8ZZHPavz+9tuUXASmAQkfQ= zGKEe8j!w=V;yL%N;9U-xgHOI-QO0VOF~eD=Z-baj2rBu3yns@<2NKX|%^~@*ux^a* zjY_T@K~p@W*B=F4lMmw_Kk+%io(B0S;YTN855fDO05%4JHEys9pK6V@OeaG8<^pp> z`EcSf%+pdD4w0+1I+;$U;W~uTOfImnI>F;apsclgWy5Krqqx(0f#(#_G3cv-0R9B_ z{J_z~1fTrzCyvS)m~tLH4zgsUH)c3Wmi<1ala=8t$BLHae1!r+@eFCKWec(!-QdWf zdp&QHX+Yr(6A`K#s3LJkqPCO(wOFE5K#UM=4g5p7U=j*6bG}M^!oGwiHEY3c6l=ZA zRvgDddu}UaqA$Oq&@vHPsDeXs$(kqiqqWvnXNM&*n^ph`oFCNc>H)Go`7|rzU^)0? z^uvsUiDAazuZ7(KQ%>X8Y@}8l!>OB?!lJHJC*ta?m${f{93tde9phQ*8S-ol7gOgMQzI@I^P%U_bMRUrHJ%}L z;$or1^Xv>Nw3U&!f}_%^brMXebSGvFv&gi%I)fXoCg+jCiM*7n3yMDZIge!TlSOVM zr=&d$`DemYss>XRLkr6{5DuK1w~2RJ35Bre)P0~Bj1SC-Yzo6#a)I`^ynX)Ta`Ev^ zL@A-rxJW6nTkMcmgHoZv8AYibU&ye}%cr_LXDmBQ!U%@pvXw5#y3GboWZxDI5#I4Qh@<0Gay_VT}sW1of_ShwO*^g86%_dtjJDXOw1S^fr z7EIIK>bjiw#d2evouQM}moTipJc=GDJnQuuk%O4r>Y9rVRdRm!7iP92l=y1g*P=uK z!qu6Bl?sj8vI6bMdZ;a#$Qg@VG8akOOwhMLotrI~pCpi&D?y#R+A@ZUl#Nr{1j^60 zDxAw!dcmse9a4a80r*&BV_DJ}OXdy@(|UmJ3=*BF%1$Sa<((Gl8@VjpdcYZH7106{ zJaSewg;Tg2(D+>@bjxhi-(STjTO#`2X1lAc`3I>78q=1LTA=UP% z(GN0*RI9aMA5kcLGtaG|hcKL4!)8)1`1dj{(pjr!KN5R?Yzx8x|HYMRR|pQgx1Zgxn#e8wmW< zF3a-UDw*u^yaq4xa#Mwuc)6v{0|rY>VHd4@0J*_#X&RhLI=CNXqwhVKHdI;8CA2j- z0~?>OSZU>M>ireZ88?{B*XNO`cMVplR?8c8OtHNS7BsM+9GeS6({#)a0~bt!B;t-xRH1K|iE9v7c5EJ8V+;akUqZ6vqKS^EV3(0)Uj@f(unmLl-&O_s z6_@EV_!z7UzD8mB{2tv&(7~Z8s{pEH4?4Xh@c~N4!TpOs?;!bD{Sa1CPp}4ygjYd* z^scdtfi2W~Mi;tgdxxmS30^G`c535|;+J{lAgcFgFMWBCn z>h%8phG+Wzbm;7hV8+wugg=fVDlj2iRbY}@W26+z(pk;of|?suR+Bsss=A{;%@LaQGV`sr-L%zEjAoDub!v+QMIs^)KwOVV zHY!Tc7+o8vh?~KC=+m-s2a<#H4uG?Kl?e|f)Ec-EzSfwp;0wa0Zx8>))ftPF0WS=# z*a&p6w84zyU>lWqpONAc_gD*UffS0k#Nm)a>w>4m4gLzFU;wp^G}mEkeZ_@_u^J6) zy#W6GVO+v7iFK#pe1`C+9XI4-a2Rmi%ufK%4LIKo$YRmPSY6j5M-g2Nm{8%sfKPL< zp9ngjIEkw<7J1}6(!b7A)LQNghVwH^hJbkw9U-j&lK6N`rqT~5Kdr@=&ac7>#>}gl zvQlEC6KZKeBhG)Hh|+Y)K;q)~V@4D%!~f4$^CD2j8pmqpYJBno%Jl&_mg@)jht%o& z`G-8ydj-_!J^1CyuhdwJf`pFsiX#!!LXQIM;CZoNi(f8S-n+QG!jJnw!MjK(<-JR_ zFfzH6>q3?s&FKby*gPQ%@4{{svNhyrOd-_)H)Pq%zz2McEIW(O)ae(IpP5U3CXH^u zX23m6W7ndi^#WHH7w|$qlwCn&Fb_7DGMo6T#~8ZG*+e&(hiC63C2BD>wRHgS>Ws(b z*cP&0(7A2V&o}^XqI*~z_cqaOEf-R~iS9fbU-XV@;$9JFODsKo zC%R&^x;{gjkYlip8&ImQ*-ngE)$Um5{2>r)`q?2B!=C{4ctZBff|3@5&5^aSQL#En zZ0uplE(}H29a0#6K(XG}SZ8l=*U4kqB7?iFH%szm%YprpH91Qc zH^LkmhbwFePt+zZwdJutxkNV=al&6@)A8;Qk)nG^x5g}yus8F)8?$8VC$PfzR15rM z6A_!Ja8X9op)^*{Q>a(hY=IuBOO2HaDS?AG$A1WFD8SKpBHT7i9l2o!`qfY9^FG&PWoY3++1m8+1p^c^;cKf z;X;AIzAnqzLq_dm_2+{=S$0np59dQe+;O^qZHHp!U@9XXY=#0Z*(iEvnZx*?ND|XE zWz-ah)Yg6n31!s5hI6V(BmBYGG5|c-Dl9-yTiP~MXw?>H6!;5Lu#e@}-fX6>QT+bJ zmJ+_zRk(AN$6|Zpx4~w#|8c&w85#>zM`&F|#i~sq&eqF9AnKx4v-#-zih(xlS+V4S zfr*&eK$tuJjLF(9LBx;VH5dWo*COE`lL@mz5R1ovmaUJmxzh80I;-Kq2R~<>omy*| zkM#<_NM~C&wQX1xo7s3W&bAP=sx{xvCZdtwYx&8>#qqU&-GY4^w&2SR(`mc~UuJ2i zY3PH~WRu0zc z^-R$wF5E~U7FXE9czRY>HIF}<`8>j=;i}fYyrhC*2uDDFUkaZME*3Smo0qitfDPv$ zGD9AhzuFOv{nLFWUH<+3eP+UmmtniXKF=E#DFo+!uWX<7!Fe{?Uk%AA4z~sBw3XWS zPuev3U6tQzZ98f8t2^ll|43|~4$!oJtxxre5O{GU4=}*6j4ir`Nv;u7%jv<$HCaBV=X&jqP9!9edM;Kfbxl~hQP!{)zNxH%HbqbxwgR>6MSS2A zi=P|+^pvITN5d8gU4%KD0&nU~73gJoNM{<{ZPJ~D@WRVR5(~onoc!^6N30kYJl0>N zQ<)z6_~d65k;uqn;6)y7PJXO@7MCIC3{w7$gA}9HZq6jUwqO{wEKH=zeB!-mWabl} z#T_%B%+xz)K1nkKsE#`{3$60ulj9CeLPw9T>Vc2}n$0t}^K+%{U4MLipa4_xO! zg7py`;@0x}SdN3JTm6l)Gmf8S++cdBqsDtwEpTC_ljB2+h=;JXYJ|M5x5*pS=)}lc zkVR5WHH}_8qF0BoSj{<`X~olF8#lP{Bg?_AP>DTs0|l4)!tf;^V5>PK)X<*B`Reh> z?w}g}=q2<$vY9HdwZo;`sIj@IvQ-dWTWjN>O&4qq*_xom{rBR&{GN?Cm|u1m!5-qY zkgW(0cEI*<6DzQyQEO66@#Q#Qf^CGvL7ZK~-H` zjx|n}Z3L^CJ(%ITCb)s}dBnd+CeBol`xSzD_;dcDSHtbNo0dPr@Iy5U;j3Qb<>x8^ zW7x~v<8gz&wU#a6#R#q%q4Hq>T^sY^Yc3eYX* z-Up6l-H$@Um4mPkAYNqMP_oXESbpjy=uPhg6bUM;uCWZ?hP6s_Au~X@hSz3eSxW)h z59&N@cWPu1xUVlqZEWQls2#q*oMR#9Pcz3%7!cskNV4qR3}oRkQD|3NCepZgQ-D?j zEe2QF;cVYu(6#Z3XkEV|gr=9vLN6XwRg}5em&Ss7rv#&s!;9l0YfKU@Fd(wd6j`&V zy%7>H7JI~EkC31lMx1jE?(QdW^B=g)iJ?nRgR>7E>G8T1dg679C>30(1a23^q{MfT zT&HA9Fnu!u@(`*lRX6Az23d9&6e@p*92yUj)Oe8BVl8j|LCA4>sWqg!lHNKI z-hB!v6pkBs4P2JJld40WRc`weEp;PW`gV;+@oUs#kjGliJrzdz)EZJ-mE0Ip*%%Xt zk@k2PRW+Bc5F$5@LPbBeQuSYR3wF2oWLQX^YFAK?V_ZJh&c zle@*HtRQCk>f|^$ZUBW>mL-;9RT?4VD#!LR*ygJwVq?ZL20aaU9hQs|yEU$~*a-B8 zeJaw(W?ZW9YG8GJdoAnHxKXZC2`#J(Rnz?ey~AL!!hzXdDaWm|#OjStXS{T*>;apJ zE7Zmw=CAK4AktDNiicQw$fgyZq-~f{Of_DI4gC|{Oh}*eHsY%*6 z&Llx{F;X<)#j^;!+|FgVc?QZo1D%bQF6RDnpYx_RcyZ9wCd*%qx7=kpzk#!?Aq%zr zTk2b~C790`%e&T47q8qC$c3iwWct4(>yPWGrAg!BncA>)gTV4N@J1KwAej?pT)$&Joq-yZjg{B z2Hyg5=HlJ)6tRX2@}j93Jh*W8JZP4=P{Bpg1F0zCTOJt9swn$2&Ky$2aZoUjR(4dfcPmUUuoxAgZ?Ll{y_6A(0d5f@jBi) z&l-Wg&_2-=xDIYQLO(jVya#D=e9@?z9HJq->aT&Iwue24v~`6HU1=3CcMus2tGL;7}}f#>{}l zX}n{&>HW2wcPuwgg6Vm1VRBAvtaC?RfhW&yh)P{d(MmbeL8$XWv`Y6WjO?w`xbn6t za~RkT7eowB?`#8!E6!5ZTL(6*7D+#duE=P%y^@OF%#kbX!BHUjtR( zTTGAXDd+4bbCB)uaj z{twav<0g$S;&@9D;41 zdxBW)NI;jW9E{Hu>Fz6tpT4j1!)sEJ_A?hGe^g|r_u)=*M++*YG(c{IvRlDjH@se{ zlR$+Yxxva_sP`1cO8m=MVqvT%GzKhKD+_qa=Z zH;H}%jn$Qg8T;Q1W77EjqwH4vX(aAau^7G@#v^}pOrqCvL4NR1Fa>xIKE?hr;L$NV zeFw4+F5E?}jk1J;2<*Wf8bGZFbisxDdDe+*JWKdhNUJg252O&3=6u#1w^U*W8iRx{ z)hE(K-a#!cXFo|(LC{DZ2Gov6b^zFG)F34pJo(Ns;@aI8kM2zo^ zmsM%)V^mqr>-=br^`pu4<7l+b{AmAq9E}TC5~UqunfYIhbG8}HN?JdToj#g$aLSin>M%H9|SoCUaAuH$+}6Zi>+>Z^J>7 z+4DZk{~tzGH1c65vu-gM6!u>s7nk_tFH2IkEVw1b5)hY|B`s);V{o{HKyP7j(Q%VZ z5r=eHwi(0Oxbk)56;kg$U=IykVJBDH%d)=!G8aT&)^g*{7;iaVxa(P2EJinm_^Is4 zvo01adoc=sConPT6$|L(xx2<&BJQZMLL7-RiYZ(&Hyuu052s#m*REKMne$YQ_v|W5 zAsMO+3QkPJF)t{>35~@;VTlWc);?zr8UGe0Y5Il^$=6M;MRC8E9kq;X!amdSksjTScCC-b3tb(QTV$_Hm}#2@m8mTyY^WH z^wv6e)D%9}SV0vk3O{6~pb1>iSYv^t;I71YuleAPKr)3#lNe=h-N$^53nz4xz4bG4 z#X*VqgbLu*;i?h?!lTpUjL*gCJ}0`RvGJam zSL7|n6jMYH3!;H0o^aY05%u9^cyL(PqnvY=Y~QhfW6+>#&LDDn zUZ7#q4v;CP5tqK~5=uU^aX(Tp`pzu^A1dsIDTC!q`WV^1GXa%kVd>q8W;r}wJxx>! zL_>jLbHHg=Q_y`FEp$jR#S|RQazTkIlC&u(u`&!QHn5;+4zvrf@MJxO_+iQ45*EF_Yq!bHQb zv??20#GAi@$Eq$ii^_)uRmos&5rkJf%#A7p_w}Fb73KnOJyvZFXQOl?%W8Pkm)7b+ z_f=4!59nzAs7AX~WP;T0INHPMm^1uNT2~L=DleY=+zs4KXyvC?a)B9U8pi1CH zb80yq6ufah;DWqySb!X{-lQ-I57Gl~qkI%y`3)BB%LPQMpo9ww<|MhYeWwG?z+h}k zs1kVf=Kdd+kOWJQ`q3chJGUqsJLP`Ne`}2_=NqJGCS!-|DFK4F82S2@h%CmxV8QfM z*apM}^{k-68zCwYlv#FrmsB2vD~w^eAGV zVJZ`-sA|~3W|LBbQHPU=DM(c;CU?pOkxhqS<#eM7n9=;#l@p{2wXK?KT)3)!3gLd> zUR**!WjN1avDk$uhO2EDHWh}wqQW5aW+kxr{h`y#(a`Hp&M>UO8U96+|62_bC;x*6 zm*XU16TtsQgBanz(co6z`9d?o*=;Oo<_p?C1kI9y!Uyp>a&6GjhSG)~;-d@57mHidxWJ?Ea-P7MmnYWaAe>$YdBRpb z0t#TJ?sMc+@DhjfCj-xtDx5liC=T}^yC=tw$OWhoO20X6xdL4EVBr%XKWYO7Kca$4 z1qeHEJWU)}P)3~}5OI_t3$vo=z04_BN~tQ4I$N<)3>I|vupMeFXUlMOpT8+hq9@3f2D)^9<0q=`}P-Ts25Bg0OS(cRjMD{qYu>a1Do-w2ei6^w%I> z*&(nwcu&MWk$|ls9{WTdEQ;YVU-H=wRdhWBnKM;V%JEzLpD5Y(8eDim${KIbGqf}Y zxMleALt^_0P|I<321Zfg6=nIoioZ65k84#d#)*yI-{7wy1wQq_(@MBea$FV;!p7{y zg-Si|a#|P9C*ot`G83+ewWL&-i1;ia5=TG3Z7d`=iERK}-G@tu$nS+_F5k+?t{)IaB*Db&j# z0c}S*r%4QL!GC``s?(Pgh-qB;TkD0}9~FF_mpY$27@}gFhO3y501jce=LcK__>E98 zp97kYR52?7zXQ62qhd^eYtgv6k5Mr#wJIh6 z&>>dE90QyMc#KssD?sBjz-2%cpwac>X}TPm8|BVvJ)*`47rMovq$6BHBnY?D2@O}45QkLuO|a4#cj|O!kGtT8HX4`O(Zs*q zKn=#~LHI&OEUMKULJ$czK^Z;L9MR$rjuqU}Y-dg-=Sno1H8@#5n9qrwCC#r%Q{2}8-9iws7IFcn;6L(Ctk<}$p)(BNm=0dY)pgRgL* zxsevfL`S)7U;c&ro>w5Z zVO!;f$gL__P8)1fP90PI2Z=A1CFdPP|JQOF1T!+e! z1iwywXvYC1{eXb-^p>)HI-bX$d9I!lS0&%kU>AM{+33iw_r+(hUg%_kvB4VtjMjfQ zwLutN`8QL;w1&{Me85rtv{d}GRDYS4@-NfcD%T&i%^mXyC(cpFPVlH;(2e7gNQ3Pz~medln%2L7i+VeSO-~5|FA4lf6Z|4cfghCHE1yzwU*M zhvzXduZ#eT%(xN~^Q41LG@Wmj1&RVgBXb=LkE+rGj-_jW34C#hXDWYD5RKh^iVd5; ze|w2vKM2^09)YfdJc*lV5LR)PmuOY)F9%O|6G**mCAD+k^ZRKuUKN`2x>@}^@5?W~ z_~P-8+}(5cLRRpoxem*sz-Df11oF{*jq$D+OhbOrh96K#SBudw!~`2Ud{wk)j(XV$ zQD|=SsxZjvm@U4e*YHsHYJ#am8!N@eCv+r?o_h@*!NrClP;ywN0wo8su7GHzcY0KS zUfEyvW&s`?RfV9p8socR3!+G+n4!BL&Qj$?4XC6rs>%%kuv2jeLlJOQOQ5T9RYajL z>UjLZ0*lz}d<@O>Hx90Fr|RQb5DDvAXC5XpiOiuQQ=l_CdwU)==G&VBCCK;pJZ?{S zCZ<5SDZ_hWf+@Z~IpINq8!}eU^T~%!D4rQax&Z`hxV6e`41|0{48C+qyDmIEWC$1w zm@ucUOF6RluJDTci(W3PD)&^MRuC;xfIv&8sBq<6)JRnOSVTncu19sX25MFS~Z zw1iC&M@zy|xCqI}6!FOvQ?x{fGqxnvm@hHbbxgaCzZ-FHj*l>Yig8d7qwkD6pdg%| zqFL^J^(AFUkZbI<3R z(JC^ltrRZ;o9x2_1F8&72Rqm!?lR6IH{3v3S-*bh(oe5crf?2)s_u0vH$vG$w!b9c zWQOx`EN09!SlP0w_u@{n{el2FtP^Vr$O1W}pq6yblT34-C4SUFa<5=6MPP6g+cgm!=mg z0?N`AW_@&+Z2u6?V!RPABiz7Y;OQLN(kvJt7&G#)O-EcxIxz}{D*`DGLo@EEO*5Bx zKJh0Jc=bBuI6ySzO#7k??F=j>9A3IzzMqwJ>xS7~KGyKjK;KA+#-1t9!z!e{%40qD!{Nu+t z*SM#$w_2f3JXbs(Pi7`IEocZx=}V};H0wu@U^w?x z8{k{;^wy^^aFpSu@r5ELR`zClTJ0|i?%Vr_NKgpwu5(_WY*?xSN9Q@0RU6*KZTKaw z99AGNsB)t=P~qYvK9~4s(LOFY8Uev9Qz9kHU^GyUOWrv?{mzjkSj%o7e&1QXz~z@j|(pt zuDp>T+Wrr6c*%hegwUuXB*-)LcS3lngjd+$%6%^VMGt}&E^%JG9%u@e{-TVROpq8} zGC=})X&t1-V??0P_^V>HEaxr`yv7%jv@q^6F3o*JFS!!ru}ImH8}uFN7mPiBNmbzJ zxzC__j*P!bMsmf5?{XeZjMsM&_RIfZnzs`kmyVBn`Av5=%v zN+(i?diWcRF zxLcf*Q%ThHj;+qhmafCwajI!5CTSy95Ti~qb)(c+&EmCNQ?dolG5#6;vaPEUqr@4T zxnBh<#%2mi@ZKLPxn|x)*wFEM;UK=`#g{g^Q-Rr}1frz{JfD9DrVvpS{^l$O!}s2Z zoaHAB#X9exxAgE~`5wiM9@L+{_47Wy<)lP`|Fbb6EAVxL4LEMf(UFx<7lyh};&p^DjSm^mIP@&^OYc~(HS-W}Tlk|s{e}Dd!z`qjs zR|5Y^;9m*+|6T%MpF?Q>0Hv4AUCW3orzr2BZKM0CE9q0ZABh9v~F> zwc8lxZRFPh_5h9oN&yx?9UvOu06aH<5-t*X06F6#O=nsCHzc) z3grs4J0L$6;D+)Frj=s=^78xV99tmI0z6ROffNL!xqvW~YtUbUd^Es?ah3jr z{+`I|0sYZlg8m5Rq)7lJ%3F~Vel{Q&WefTfy$Jw!l-DpWjvC|_0S2L5j``pf0%F z9S6XJKBxCFeh~5t00U9Ji1Aw?uLJZ#c@I*O=Q6-Zlxxx79(gUGHKW2*S~>b4KL;=X z?WZw5Zb_vnfbJ-Ng_PvI6cCDXHTpXtp8)8H@>+~P1^LB*At+zR__!UG&II&9`4H0P zfVTmoP=3Y;`j0}n1?F$G>HiIs`=R|P=o7z_0G(0(6sZW11qej>N3;_^ae%fcziZR~ ze3boBK5x_iG?aUyybCGuYXXcw`8V_@eZ~M>8M)L>B-3RL{~0I`M)|5u|1(hTjq(AcWKU*5B+5_GpY%^Q(+cx1wCO(` zW$3}_xJ~~g)2=9gj+F3?fFP9bpg-wVqWn7AzqjdsD#|@j-j0;UdkZiew zvLDLlZ2D(W_CR?jQqos0APnUP=ui6pr}clrrvD`1D1pBXDdA@Wf>Fi+{FU|pp-unw z>;H;P|Ea+D0{*v1odL@M5hy=Ff70hat^Z>-{Z9r?7vO(})DG|_U>M3jqd)0m9H1S_ zt8DsTfbu|;FWL03L%A2qdy$enmjOni{5$%S{{LzHpRwsb1vuS-|20yQ_fkM8%Juvh4U;j64`kw`yzQ8|%R0enlFdAhmBgdb;X@lo~ zt?XMc?d?VK7WVc`TYHhj)jp8vU@ub1?Zu3ny~xhRzBA))FH*F$cVgPvi=?gXvltav zm)qN+e-`?;rT%U$>>a7UgRA`{>YpgLmr#GPi+w-p@7>bA8TEH=ZC@bt$G;0`NiGGi zFgxKr&L)gKo^eVTXI$nq!?&KB<30x*Gt*{HnU$87JVmcaO`bVb&nl*{$y25`uBU%^ zTwhD|b0(!uOHw3FOV>}DIVD-4pEXO7GAT_lc@lN|Yx@-Z){jQ5 zf2NtPNKZzj(op|uAEC9r&*W(hoTnwvPM$>lh@}~mX3npl(yX+X?55A3F?m*MdIN1@ z&xTh&Y(rbRepXs?l45e|tSQs$aX`MIU1-qHpN$E!$&-?j(-c$ErYL4iOP?`GKZRus zGpEm-HFu`M)|QewX=g8VFvtI6keCJM^nKWyzB7NF|{jRG|>A~+f48^GZ~Ugnl?p` zaVCL=V%kj1U!Sb#*XOl9{S-m7X3tNXHkH*Y`uY3&_I}OV+eZ;ODQ&tUFew%6(3PVE z7V7}(mBN0Tz;>I#URyA&m^OGW--+qUcrrdr029P$m?=y;V`Mflh0IS3y>d)H@<08y zKwtW8Y5Q#jO7!bu`*mf6UpW)OU!%rdwq-gq{g^;z5|hOgFd|WwNMt7x zizFfkkyPX?l8f9#3X!)+B}!~Oe*KvBW6>zB@!JN!Zuo7B-*)(Ik6(BEcEGO+zx1=( z+i|Yfq~^aKh;sU{ZP5(JcWQOBeLtI0xoK6>y4P~AHQ$_feSXKvOBK0S&VS$;vCrwd zMLQQe_RjldWrrgxlH~h4{HEBU3wz_+hc^NbXJ=fz+OF#M4>O|nUaT$7xnR_;No|$C zb$Zf*CzGF7pGaOddV>l+zw2vn|$_k>t5f~HM@pgK5_7x>dB$6eo8HW%`o9+$k}F>ug48O_vKgKSGo^m zZbW59m6f!fRC;7@?ViqEi*{>2Ty@~A?6dFk*5%oGNOSEc?z-Z9Tv^$4=Ua6Y{!JVW|HYSK#4geiAwqoz-vJSo+^+&ejE6f^m^d#fB$7k(yc zUr;N3GqyN~1MHiMQM_l}4?4(_h-$(5o zsAmps`qBH~#lX*Qjx1YMK6pmaxy^snUcR8KoEG%#%FOmipnJ-k%me4^W4HglyBGHp7ee5CpQw-oVe1n z%bL<1d$yJpw;8-UF00v|J^>TH+y8ayfqaEA@7})MM?P}?ai933^E>ta!!O>o8LZpa{ut# zL5KIZx%BpqM9ZejmlwZxZe!8Wa=$UZ-5mX3>cMkUV-J1)gTvmT;RAMsUG;2M9?`zZ zH;+b&2P7_#1Qy;;ycu(I>el;j&+Iq<@U+n7yCzI;ORXW$fP^d1RH#g1jHMd+uE5*M6V#$8Vp1x7p!K zKkvVPeNpF|*P32De_`pcA1^+r+x-JO(|EPhnbwE1#&`er(XF{VX3ZVG-|fygY3^+N zNvD$m4k;6widtCLCBHLm`{cR{*6EW%&Zf4W_hXX{zn*Va7HyP_I=WlzKdJlFEst9# zmPQVrHsadcnL|eE&TYRw?(&tCrZ-hJ0p+i2wjbJdY~8^dOU~{ZUTNLy?f%<$-z__O z;H39^yEUJ0+M~1|P(08bc`K^r(UJ15S2P!9*Uo!4@cyKjvh96b?(BWN zZ0{QP)!9b-xHawHblb*!eC=uP9Uq+#`}kq6gZqy5`_!<~-PC`B zjcwih?(Z68o&PZXaF|7x(f(NApk6n|+K--J6TEnG*q(W_#J6>Y=d7!?ZM9PY+2pp`_<-FPR!{EVg1TNxXDjvp48l$8y$b}fNSc8O_zt7 zb3bUldZl_z*^L(4P8XkldaL@qQ%`@NP&PTO!=3rlZ|sd$tw^vKaZ-+GDyUk%^X5UDj z-|zn7#1{|m{Bq>z^B0R&mR=gX;neA;1C|*NIfXmhOaROG}*SM%ZaCZ zopMiYmyeFWKe2ggZRnYyS7t14epDaSa@<3^u7P(8W~UZp2F@)XUN+zZhdaIBKDL+n zEIYgXp?1px-xz;z9J|IcH|WZh)Nvh0_jaAQc!2ZT-tBw*;LZ$b$*lQS)6ckJ#pLbW zInCa8TOX_aA-48@(feDD-oLc^%A?9dj@!4I-S(eXr)~x|IQZ_gqTQViu65S>FVdT#bciC+|x~=`xmD%3iH>~fOxBb}dC+lxKA9(uV z<%Q2JXAe!^mic_#t`CPet>3l8FZ;_KN_VdZu8zL1h4$*0KDVFao?ElSelMLKHNY6U zVD7H?DWl4so|4_Ub^i3;(@)IVWfdb^uKuQJSM$D-*&9AF1s==}jqfUUO!aG5G}N_U zPV?p-!`6(RD&7{oIREMVn5@b{YF3gaPs7` za?Skj+l;<7Lc92>dR6bTg%{mVPj0o$+zWK6KB4LCP2JsxpSazj_3V(? z@vEkN+kZUiUb~|=zjXV3@3gihzxlSbu6f+*^X@xatc*yPyNny;+SU7o%YwJ_T1Vbq z(550Z(yz?x9sjrAJrVG2UDJVsehnVlG<)fgF$41lC9CZQk8P7MU{=Q3*9R0{^%ZS% z>z{ojw%_hctGw?{zwp`*q82_Yex9z}^3JwyFZ$i@abkS$?h%obyDsdpp-Yz!?0@Q8UUx#&>)QnL@Dr`q|%3^`wW)bO+c#Tq+izs|`!dDtU$yH#t)5hy zc<=U)eJvMq?^P}v@MYC^6Qry{>vCSdI>LqXYwEcqeKZX^pdt%r= zp)RbFdAP)Q^tbMVvuj$!hCX;d(5ZHl=c(VHt};J9V;%h{F@D~a*w+t~H#a@I>i^S_ zYlnV+;|8~W_s#R2fB)g<@Yk=GJ5MjWpR(=pzViDQvv&2qB-uQ<)aUyR=XA60T!?R{ zJTHHdaIDAsYl{~5x_W$s%B}dcHumTrgH{!O`sRg#RhAa{Tfd1swf@*U(A0^N2Q8YO z&^rd74w|v_OuGyDXM2BZ_q}TMH!J5po3WzLFwb|}dQ`r*cF5|cx$y|2r@i6Yq5fanhuE%q8lRh#YX+HMNJ3BgNp1XQIEBf?%*;77`${Fa^=B;LF zmzHiQ{c+m|2NYYsm!y30Waj2C7tO2M9^SI&H!Z)K^>w%7Uw`G6^{noDhcF(dKhl~~O-(5`@cjR9GHHT^aCr>?;$CWsJ@#{Bx zTgHAc`JLBlZoGbd;`DCc`jvfh-(%0QWuv7TNfqDC61^oV%R6#;#BX_D&6GQp&uwYnq?Buk*y}4}Qd7#eVC9 zp9i0NYiaVHih)0j8~x7Np&mYSm&$M7{b}ylG0n>Fsn{b?bsOtEYl_x?5Z24P%ec|8 z>wbJRYs8ZGq`wzuY*Afu%YQawROxzO&;4D^$J|!0->{@tkG8{~A6TRaw0rlRTKQv3 z*w(>gT|OQ=bZggrD?0SM@OYz1cXp18Z0pIdZaFSFoSj(caDMiNg#B&Y@5JS8s~vRH zH6rJiqvt9z7f);6_te7aVRH*Vl{Gt6%qaV={}*;N0P^UizLcAEK~_;L4ulocNtbBFd_d2j4{ zJ);wY_w;kDbH2GXvg3iX4?eHH`R)r-UZ2pZZ9a~+o86=OU|7u?H~L1L@Xma4E#{Yy zDQhbq2O6E;*>Y#5PbX=?(q=P0AGmk(E!B4gOP|^I?Kz^&$Wgz4 z^Ked=*H*V!w`|^F&ov#ce4q$FQ9G~C_OSeX(ev}4r3C8S6IMT-@84_m2dme||MsBk z&-QJnM|hhHP-Offz9-rPO-x+SnK zDQbS-C;dCV7kG7Ncg>N@A5~Qzf3kb|#*n6yo7c2;oEEY1aMZ_xt_|ps?fc=otJN?1 z^!f1H=+WPpuwJ82>!3a>SY; zF%LRByqOw2X~@MdzUPjg-S+xozrZDz&Dw_`2Fkohs``cSur!4*d*!vQAsNVO{ zLn1;FQi@6T?Ac59H9L`Yj4>F-3}&p^qE)H1NSlzgMIuSow5K9vYeAc(O-hPN_c>=q z^!y`S6hG3UI`yFKr-y~}x@_cVn+TBF!QQ?#mnu>ZqG4(Pe z@e)_))kR^!qq~QQ1)ACZ2Tt%m-liV=4xo6(1rj;Rke-&jD&a`g`>Zv5dl9+yVC^;U?@ z%bv(d>7JOUPfVY~_jwf+chiO};M4AF^fE6nJYw~%m-p9lJhW)z+x(+|r!qvdvUtL{ zJd5(U=2pk)+HphmF3JnDUa!a}9egdP?(4hDbgQ0r>_PNyb~+|Ev=T&?ogb}qt+=|p z<$gE+{L?|@n?x91@qO+3DsB1@TVN? zCwj4kggA~ap%DBwlT^Dt`XP1{&I9kn*a|kJ%o)A>dEQkwQd#$P!KxvL9rYJqf3Mya z@Aq_Ruka%!tDO(xKYe>>L$WO=tkVx{Gw&{@;tn@d zT$pv?IWOZ}+u_&k7P`mUqR(8v|aVX;|r_mO?=CCwRd!!7|gxDFHxtf>8gG2N2mKWfg^>rZh;1soKG#w zwbpj=t$1{nN6bcRmi3qsN1Si3@9>r4z}|Ig)Lp~onyZM%-V zYOm*ue==M(_i^vCEf3lrjy!yB9Q3r}tHh&J>c)DJyJOWdp0u*)XQEXW(c9*D^#7a} z92UU2>h^rLxn8?T*+0Mfm68b|KOcxtTFi2Ip2u|ZwY|6FNOjSjRnb%wC_`xV75QRC zwy<48jecBuKiImv!{doz@7w_Gu2mmOYD=AaYO=3cl(!_QR{k8StNXa*_NAsY=Zbx* zQs++;UTW2veYcZypG%LM{POO=hKhhV<*Nkii{3$E>H0%Lk*l4eEY3Dla%6_3GS(Tl$Q#WO%h-JAS zK}tl+lqMdRXf+@1kM{QdNuexxXk^fP*MmR%z$U4@oyUb=6qTFf>Pw;tBZJQh2S2qA zTq8Qk;Uv@LVo#MZqOHp}po>pRRjzmA?yf z2_Fq1sn#fb`jV&CbYc6FjL1D)IT{O(*M8&QlzvINq$hTD`LGWCQ|!;+LCbohYhnrO zdlXiGh_#EvAFTUIk_!;@LGdqH~U zJ+C_mw^^r}C3V7YNAxr2+m~v*T~6dsgtw&|bYx`mAMEMOa=3A)Wd7S{6K$U-n)gNc zb$W%w_AfPbsqAsL-*jArUvT|A;pK~@biy9#FE=_u`VmqT4+{Wp&D_#&;R;R@-rGCx=r1Dy=*rrY4Mrn(KJZd~mq6f1)DBaz{O7`NsT8VgBF{PtxkR?y6 zvk<|YR5j-vrG9(%RyyB*I(kjy>hP%Ba8uF#vF^${){>d3mk$;*Xx= z;~k&w=(P;~*pOTjQhTV}<45^%?lq@2=_DyGkt);V5*`s(FjwbNi>6zKJv;9h()u~h zJXMS6T^`*$(P`KAtbg7(ECBR%Zc0C!DQJBmefivDIbk<9)fjD`wcOi$u?~exP1xY7 z96$f&K>IA?M3<6T>tZJwZTy-yZq&IZk+po(r-Hxe*h%5cC$C%(vG4Y39-V%EhwjBX zcUe8NU+6HA=(?d(>44vzB@dO-(-vrC);LMj3}$fVbhr8>etK&`YC1!)%%Da(=cwoj zY7H*u<|>&Z#532+LVtMT?93E|nKknR|NiJHipsQ@u@# z$d#W3H9y{5@FnZ#{Btse0(6@U;nf~OLX)b?MK11&TNwA!R>#)7Pj|Os=c-$Kcj?Xj zT5VwZe)XCyo93==)N|5jcOYr`EL^XBG=Zl1=!%wxXbGE|Tf-*R1ML;+4?5|<=vcqbVGD@DemiyV-Nao0H!1W)JTqgBNSRv-T+=@#*67t`D&E?kcge!$F z8C9%^x}yB~=3y1>yfa~k(;LIr4}77EyO)9|-=XMz(o3Qo?N=}sjzmPZA2CbZ{v;-8 z?FIV;zBwZCEq=>llNtFj#^U?pIC5@pxPJQwrB=BtG|_Y`O=qHqIe;-PCa2db+Vyzko}X)Z|AJo7-M_lRYhbcF6?k&3l`d?fTAllrX6B{N>QQZ4cg@(n@pbA2#XBjF`L4ALTaM|G9a^i7)8~CdTZ4-tCU@_U*c) z?e^&VcK*j}Ug-9Of3EGVIKJcQ=VfhAwAa4s4B_qSCw@EGbzUDJ2aP{bZ+3SxK5^snaS~a=3-nnvM^|#9pQt}$-4<2Z?JzIQx_qF3U zZ*91KYwrA!JEmNWyIVp{S{ffLzQ=)Eh9mG=$FEy^rhbM&CU%c%pB zuRreZOTKoz_Qu!j#4{~NbrxhEn~2TLs-#IBik`jTh+2*H;ZOGy(zk1>?p%xK+Qs+G zXLrjEa$54Sw>yl#Jl@V>m9p*n^2*f1{@YU4^EV`mm-TOa^637ieRo_oI~EeRE)><< z(jLKA^1L;t72tB>7tR1qJ=BpsN_ekiz=ux zFv|HVe>GRnSxoF^mVNrcm%H8edcOUas%saR0oYg$PvK^## zHJy?{mxL|x7oEpAt22(O)%K*FuUpcW+Th{ys6I>M9oqk2tPLccu=4Et-F^q% zw0l0NONZc}9P8AnzII-2IAM-`8m+F+zY`V!tUDSmehlt<+SePsay?PN)nSp_@xcPs zi3x$@LdK`RhWBo0ycu|-=uBk99Fx2EXfg-RB!wx>7ZoJWaTd37)X%OF-F4HGXH{Tj`BtP=#X*OB@GYKqv-^kV`mYXdC2iSy|EXTz`Ig=G z9?>i13|_w!*y6N4`i0@UF21jh4M87Pe9g5yc>Jn|q^iR6Y&ZKPE85tN&vW**8x|#m zes2`jJl<-TzPxJ1@l4TcvI(!R-xv)L= z%LAdPmf+L3i|5rRaXsU_;=9QwC#>q~gIo!I!e zY_V=&XVQwcn2dc?iP2RuUuLBq-V>0aVz5nq-}-mwO?_Q>zjCRG+ZOj0j0X5mm>=U-^zxuhkvlFxn-l$^LRX2m1s7wF2o?`=!4W7_e#WREQyl8`#IF2-m_U+3ACUS+r6xnEed?$v!C z3x~wApKlkn`>k3=d9>A>`=$*ovCX=Ov553aL(VDw!i6eDOfJo7rN8wG1v>wI% z52~%WIW~Sccd`G#2|lY0V&{~_P1@F2extf84Lu&a;ux4wc&DsHg28()E})vQzh^e* z2%8W)SOD0Ajr}6#@Bb2>%i8BK6%U?vKYs)nBJ;_H$jw}4CiIs-4?%2TG?9ON8fFeK zIoOK)a0cvjKI|6NF&6%!4xdiq~1MHtJ zKS=*)@BN>>_x~sM-a8{Ev~^T6mCPW+J~h}p1$%c)XeO{}i9xcWGWV>ZzNIEKs~Kr% z6w*p{Wq%3-dCib=_E;g*JNI1JQiD5&a zGt8K~sh}3DUQ;rNF|1tP-t|LC4mZ?q!0>P8=Am{97;m)h$)S33XMcVF0^0p#b|vqaMGAj zvJq0ozK$CDTg`K3QDBP~?9PMw)}VQz*+e6Me{|m&c%HD?&j10gVte7^N*yk z+6AS$A*Lh>i9tdeVT5Qfu^>l5;{cfe09sLjF?xT8I|E5EN-45Ck?4^?h#+79>_&@5 zY2^?>rbUn$u_hD(+LkW})NB$7n;)rIBhdzq&wx$k-HPUA2Bu0tOs8dn2>AeKsp)p0 zXxkA&QTdPsJVI$vq?r;Rh^9o-IHTf8L_;p5(6AY~NK-He0wV}YS!?EGXZB0V>e*Pd zG^WKEAzWB#G-4#QNC+^c`cG531~H@hI}pfJ*c54wrgM&^Gf1JfBp?$J((@w5q(Cy2 zm0}Z6J-`jyc8KvD{DRmVX!IGJQ$#6YN(r=rLR?5Oz=PB5(eyB&IU~Rb(L`%-AfV(# z=3$tylKw(y7EpsiBG5^uCddzz(r~}UW()LTRtvokS_U)V1S$i&`Os9%8WH3GVoIjN z9!6r&uOg8Yc9#eOm5y#LWC6j)hs}XR3YktLgW?UGro$G;!}bF~616J#&^Y4|kbDwy1D+|6V=AB4l#bF+Iag(cOs$N`3{X}$evJijrom2J zb5YXiy zJ8U#1hQ%VtA@qrkZvJgRW2r>g&wBw&hi>exN1wnnf#($lLvvijo_+eeVyHibimZncpo&niKTsqcnFshmmkWtQVYe>oyuW02E7IFio5 z2l$f#gJ32Gux&XM+d@ubh69+Pjm#48&^h9nJFst~co9iq415sQK}M`ZkSYXE1SN`~ zQ360h955TmAVe@C!>p(QG$Z61$N?~)OoaB=VbAbH2I<3|{UgJW`)qiyc41UPD1uZ# zX-v*$vEeZp*vf{-xcN_`+&{2C6!=4dKNR>wf&UjMz#Pi_8mHsrr0Mb~-oMSuSWsqK zbimG2qsA%cDiJfC$x%DV4|`!@JbW`)tT=!K@Iz(1NEz5iF~MIet0q?-$OQ@Dc{CZYj% z@J2+0;4T37@8#fU1oxM4=Ysn~xG#izE!@%7tJ827f_o0!_rU*G5kv5+VO}v1=AID_ zf-r88A$a{Tuk{cXi@ujC13&SUX@v>3#HYF>{g!f1d&LU|hiQZXZlzz7H*L8HN-Bm_p| zFxG}JJ&Z(z)d_q6jRHPnc$m=yA>W(4oXkvI?48{IlK$_} ztb9fe4yHyfFe#W>pRJkQx__t-10yik_oBmCS1Am91Yr??4#LbT0&ylrki!@#1VNll zoU9z6y_RN1re;pgh=Yxhi;a~r;;_!i486mY6`|5_^hjb5j!8SFo=y4LVeMt_FK(J_nI9po4JebJtz2_9Gy;&hkqC&1OI2Nq5V@@G!dEI?jz8M~LvpMUyEM z@H@nm9je5E+hi;)5}Y)_6B1^15z06d3K?8G?O;}paT}%zWNIV{2M(hqsB!U zXv-r=;gMjh{1tK0L1fULXaXHmjDK0gR2h&L=sA@gv@QZ_0|-l#m4PVe%Ej-90nuP) zr#Ee1nFx6?ZzkYW>m^P1!d7D`6jXAjasEXMI!^8va#L+XJLF$B`)_5U@(7bR3aBCu zwZe3ic_3mmWn~}=x^9Wagke0uK>s2=5sRF*0g===a@g+;#EM4{RttVrh-L)Yg#HEU zi6AMl7|7%nQ-={JSUQMBG5f_`!235T^h4L=FrNOFpB4a&RK{5apfU^$1ovDhC4v+M ztH!9n5@FUGNW!6R&yW<8^Qk&m2#^>c-GOvP91*L}7>x0h2KT!Gz=Csu_ahk2Jz-F@yC|n;Tbhi5wySvLMXa2 zit!sia{(Tarc1**1B{JA!=MKQHo?RljAvyQ3?~v(hrx z(9yaaVxgG>=$t;Lh)G zD~tF%(7U0I&(M%I4On2vMjHzt}gT|N&p?E+k`G0p^btgH9?{MX{?5l zLQ%aAB1FNkW7-p+Kt$g&;4qhYIuCSYm=z8s5QE56^bCe6RcFG{;V(KEhWaemnz`dm z);T$u*}34+#p)@TjlGGH4c^ws#L~*n4CD${`dRjdC9PmYsbm-y`784<4HT*lpwj@u z!FwwPip1Y~$6rXX*yT01jw;1XvALolFJMgJwX#3}$0`c(a~KPS(09j?VC> z!SEQF5hkIlhbv(m0eXvb#)eAMeD`7kumR?jL5AuP2kb=z*iq=*p-+SX2w=ms+<&74 zYS(|`J9C(Y*~Teb!K`MgUzw6yjK^Wq1u8My34)3W2-A+D%7-nvVl?@^9u^1wg@CbW zhi4XwAaEuG0p4byCI*28OUyQagrZXp)>r_}#$bkNEPFl`W*JuWJP0r(z?A?RHcy3L z$U|meh6XT&6;W(F3j-m{e9n+(W-m>OfMFMQbOjN6&4%Scjl)zv=B$kM4i4%FKnGZK z25L7Xae*`tDJSe44!DuFA98@v7XgKke47#Ki$dU}wKjt-=* zt`6C>X%k{>Y>dGADx#*QhRDmyBch_Bh=70q99umHHX8p#3b56G@QL_eyZ(R4H~jM_ zN+7tw!;D`v_&Xd6zn`KD#7{yznhpJS@vmR>;deL|p2~;T0P$EwXr`IhU*TvuQ~7>g zXh8G*yii!!@DlV;WMu3CzwS=xuaU7Q3ZHyGj6I_e0R5J4 z3@3x;D-b{s;j;w-LIUV7d{p8(z$f2ha2f0uLBrE{P8eE4wr`GVMn-BLjv$-puaS|* z47djtD`5RwzA*t|Kz$4+Tv#9=4Ant}0z#z7ErkLiP~I30d&Xi#e#(!Ot~C@^Kj1CL1e)Rv4Az-}5mg|NVRBoz=z}?Zn!QhJV-h_fsV?@8~b< zpG8+@7;wO@Oo3HjuHvdu|Q}WFBOkvh%<(tMB8WPot~igQ?mJw^`UeWLFtIe{xtuD&!RZ|Q+;4V zrgYuONMJr@%O(temzTfi8^bi7sq4b%Ga`+)^OxQ;<@4|Pm>SR20}s%CQ2*cbo~aLL z)3ECoU6>k=$~*j{+F#hfe4f&K6#lEaX?jknI;*!uWqhjVoqnFGZ3Z8vLI2j%FFgOP z)YFOn?3xYH4*?_<(J%0W~N8d9qb|~WnlmX*sRvI++ul#{nxc&KUy4kXbQIxW=WW*d=O2xE+hEW}rYaB>6`Zsm`RSy0;^>hqm zcS=wHXS(DE+z7;QMKy-0A^%D?P`$JQt1zXzsHUSfV_Gv&-bO)bB#4>PN+OhsrV0co z4ts(xO@ORTmxEH~U)71jQSaO2GIXE+Ksl7X}{$#`^}{DMil(#`a^*~6!=4dKNR>wfj<=ZLxDdO_(Op|6!=4d zKNR@CkOH1l&Jx}{gh=q+jL>(AB~Wp;M2iS;NMynABsjf>q8|Y8dTZ!2qA}k@p2!G_eh`BZrl+D32#F&7P`5V~6p2tGc+fF|R9Goh;9Hy|l~6cv zjugSe^6=1CRo3O<@x->Od-I@v9QrQIa=~xe3`KGZi%16eN-|JC25>iIBH~5W|37tk zQLS*%5j1enm_o;oL?nQlD9#GJ5l`yLHKwq>K+UH0Qa^q%-0ieWI4P>gC{-sr-FAG zMk4TJ#l#^DJSR{;4783Z>BR~FESY4QW*bx1y{ODHpy)V1C<13ppo3F{oTZD4gFFru z0`RmaGoA4|9FW0-69?)fkJddUwb*xUndPAc;jptD0?0sqB@jLu z4wat>I0zAtB|}q~(XlvL)X1Rj;QEqSW=UDJ&6t-iOPQ7FsCNnY@T1(3X9^KZaf5)M zrZgfQbFBqp&2RypI_5yhFIH*l85ai5k@Qte;zg6`B=Gn}KQ7FoA7*60jXi{n3I&KN z(<*@opfttca#O8EaRKorqkgF%?-2t3q!Os#yp6OVL1HKV(dZzLqzEk>&Qh?;| zMU|w3q*F-2plXuJIMid42;BvkV?P{t5Q61Gg%b{?pwPh6CJ?eyL-bHXum;C6h!f60>I_R#y{x6vPj3SgHu`?E(;pDnVx}2ml_$0w0i_9Jq)e`hH z0s~3Nf&NW#a%ISWi~ugE@PhaQKS?og^edbuL51^SrVPPM?Z<2{CQojONr1B-81OM{%sQY3j%nY>sLe%9C&-*X)9_DqfArr)3+!eP zxay;)SCARM4geqq(cp|hbfm!mcQ6n^@QMX{4P6ES_55Y(DVeU!q$->M0`9#uDp({F zI8P5fXawsA4gO<9wgp{-E(UeB!#eLNK0_FKJ*FNoW56iNg4Z|JM=GFx&7b$N^qd$bbtVp@(&V3S#yd(xb=z5#V47=;nSMS|JAh`xF`+kTP>H>P4N7VU9@k{t-WBx*4a%=;4h1 zHwUK7B2hw!!O1eZ%uzP#0Ev!v=}PL#n#%Cj zY=f(Y;1BdF8B;;-!^3+6b~EmJL1>53$Unho(l{V8GucBv zwf9T!%H!;(T-2TN?(aP2m94*_&PVozRDC(|xPGEjNuAPW!DZVQrMXxJUw?Ee{>z(% z2*+2+jK#OkUb>WSA=n!8W-@ZtAT@sMUz#(o3S6_|oK)V0QrjN- z-4Ckpj~xDL`B`prlF9f69vjKtov$Pp*bj2OmKJ)^bV(y~L79W=nG?5zFZIm7oAR?} zq3sK{C^_L*TXqZao!k?{X46lcVBhjc^TgVh63w`bJG)~;uhc$08avEi|8Ci=o~yUo zlQTc}ba&sqd)If-c!Z#7o5a;yF5EZVi=yLK^VewzpOjy0+R4b}`jHasr^B0v6XUNb z;gxNiGjv`>N@VFSLazAtB^H7^#Gkdy`YPNpXL83r?Q^%cbIrS|JuuL*eq-Qq&QG{E z#zWaug}Z|7g7s-7eQr0U~ZnGn#rorSjy9>6-9@gc0-d2->CqB1T+2kYDTEc6! zERop4Hk@*9Ys2!kG=^?Ach++5uVUA=>)0wkH$7bLvs(SiD&dCf%~D(Ee@?(75@(+m zT-+$0He9PAxu>Eu64_+si`^OuL{hexq3~#LA=PNlDBx$qacR zhX=Q2kCsQBcoi?TPkjHw6IKW3Zj#9IzVCP~zjqlQ$3@Nu-{W}?58SINc730H{HLso z^tD-U`;|Lj=rB__Z>}=d)F-J zZF%`M?GXRhq-wdc<*B)Az4^p8Co3$zel8Km|HEdrwc}2qR3TA)?(%|#e!45mpFP=; ztSh(#dGm}-{XoEtzy~i6;*NNKxLv>BM`w6Uftz=Auf*%bNcZ?v)@K@B1`jMsKEtN- zj90~C`_WO}EsHWQnVm3>`nf&nedeVM=WPqsBj)VPG#Z>Y{-*Tloc4{no$Ag?VchbA3r8S(1*5}SjdfyCJ&<%X-5Vo%_=-@C&9 z{oq<(wnfjZqkkSOe3`v>T&_`NSh?aBe(tBo-zqxA8fC7@ZYm)<_B-6*sXTakpsZLn z`Pp|01BB*T6gjn(D;y7$DDQjQilEal5hO1QAp>G5D&!=Yu-^@F^LyHkt5wcHyR zN^NltDx4d@+hyl-ald}s(7lCa-M;0ih%5V~wtY=q(vY|BeMDFktUS+8-+UPtM^eleAg9J6Rupc;e5WK>+9ZX!{sNe zeFyyvHcM0#T8t7EtyUVZqa4?MNq>UuklmMexFW|grd1`~G2xl3$VvHj?R=_p+ldp7 zeTG=tZks#lSK3SNbI3KDZlRbBnz?Cy@1xE$ zloT82iu69;5VXGZ(s>-eO|n;0nq*<-*7-Tb_jeR`8Rj0;=^6YK;<;N)n7q^2t>_AG zj?;i-b#OA0(Bx6<`y&;Cwy6=wUW%4r)9qSl~rmT-%=Sf+{)y*LrY$0EKe&) z=6-#Ce)Q6^*`9@q#OvobrMF)g5uBq^Ov)%JTGo^xl}KT4gb>?oME>wMm`zfu?SAFNO7`mC~~{ z64^)AH$P7pc?BPv@=>={3WyFq)O)0S_1kAB!Y__ya1*xB^6&K3%--)mOYD^EV5HNR zvV$@**Hr8rV%WJ)^OYqJEMn8%5qQ$KimsjZnxp}W7 zht=jQYM$RL|Dk&MvPB=_^w%{PN}U!yE51#*OHHkf^IUT)f4O<=%DLAo;uH{%z1kJt zLOmJU*Q-A}uO7iSy6pTgy5ZXCAvUgpIyUN32UXukquM7(~5qEZ9Q}As<->T;-8WLCMi>h+I&C*;x^3vFbz_DnUfEWvX z99^_m=t`fx{+h+wVr8-mYhw>@pgpQul&9+!q{^SrzC%2PO?BQ`Mv1%8yYkq6jc?Wm z{N6W*Mm(6+WT5(>u}N%OplavEM%DZGgr4XdaCyAW3RgHL^F(AYa7zH+Vcpm@8<&5+ zb>X}=pQKiWm1j|9(RK;$D4JHy9o?2YDQZu04EhdV;|o|26a6Y5d7qFwzdZe}+u$*E zl}-tc1B^|n58r5R8VnXF*x@3&Z0R7fL$aL1apc~9>gsE&0|OTe^sF_WyW&v$aq$EE zXKpXmTu8jRR=995hbVteLbU7An{$5GI!;n5I$pn+|2>@}=X0A^r2F9=cP}bccCzi5 zxBh#$@5oi*Yo1Nv_aAoiNgn)majD!wLQ(pwdN%Xse9vX_KD&f`H9oAMX71Rgz?-ip zbo!E~$%=QoLmGv5_VPxtJ?h!An4Ggq|3D7X^}(_Fk*et9FM1!g1@{dcyn9b8MWo}h z-{#uqZ)uiqhZVo|PiWU~7wS9p$+WA*-gn(6zb@ZB4jZ~pE^e4B_u7vyS;o3sC!-8s z6k^^kueYF4gc>m7Wh9_}e)6t*-{*pu`Df2b7v)~qf6ZRq%O-Q#fw9zwM|P(S?aj1) z8s*Ti^|e-g;VVx*JI0%Dja9eBvj?u+d>6ppFYz_qBQE0pir}{BJnJu~48ELQ)0^^9 zNTOkk_xs)zY*z+-`wIqRAH0(zzaK4=8rO3XskqvrU|n2s9``LXu5j{18(UPYTiaq%S-9)i0XpaD zhv}=Wcw{)b^O8>6PH;p<3C56JI&=^EYp3OnCA(Ex#nxHS6qilFgNUPZShF z?MO-Ms~YOJaewiR$tMRS%LWG9?ce$!Vq*JMDd~e(1zrB4JOzFG3j_>}ug%+=~gepf3b~@d2Ums3{Hm$%|8O;Dw1<)ypQsXV;w zrdQbHzHHaNmqQ;^(RQrJP)*t&O$qnglmDUeuBogLbC6ZR=ckIfuxu;NS>CyZ4j&$0!kZpU{b)-feJipfE znE5FevPeCx=t9X(Sy17VphqMZ=&Ib$y)1Vz&im#o z!`ZJx*>sKj+~p=WKKD0k?B05A@#>N#8{#s|?2Zq)oO4|!e|XJ`{5dD~-%MKWCh^(S zAo8om&5BvN8m=XW`zpDNk1cG$dq-VRRny%4?$P->Ll*>i8GWv+soY$ij5l|$pU_-a zawntitdH`xf_p9Le45*70)RxbxiS+1(e<@w#OGe1Dzale^ySXFR^~9Nu7Qc7C>@MB-x}`KZScZfy62&n3Dmw{8BU8oBqjm~Moy__KSB8mUxy#iuAMHt1Ol-cQp1$`=jQY{xn-)^T-L8c@`2=dTgS=ip&UqSrYAnbia`A~{ zYJ6IkI+hx~LxQ;+PO)hY&9gZ5g|~gqoUtvzX%kw-QH!c9G*4D=xm4tN@PCbJteREd z{@hcfA@HmDo-*xt=ZBtsFe;QuP&9Dh^V(NeBH7t@`e-t{HjZKqNFv& z-|qY@+_`w-qu?bSZpk}ITfDv~%;mUvQjfJYaqtt|PpB z@wRVwJOU&Y{4Wh}8+uCTlRw4oKjM<5VJCF-^bgOC^$w%NE#%Ff2_xSGg_`|^Me{|)wHIH&lkx=F>h)W4KfE|Hn7qDb^{k@pYn;1F_guYVAReEcm|Hz?d0=wkYy6j; z_XIUF&-IAh^lzqeAN($v>7m-&qf@gaXBCP>gqDX(lsd^Qg$1}ZI{pnyO%bhm|t4Cnsw3^&R!*o8H^3kasOOEq3*FUR-{A z*8b;7w=!~`{JiXINa{|x?%?!DJ;9scIz=7zo{ zHH+y7t!Qt*Ny>)WuEf7TbN0y2#of2}tC&l#;=b&0QujuyxXL~aJ8_2H@aeS@{TD8s z81oVy622$z`$0VGj)s+Db2_7 zV2+#BaEK7;hdy`HPCec|dV>V2>l>S77f#*&-8W($ZcKRoiEqKv^jkW)NxWI+k*8ez z6-@7UENj<}-y9bH{Wy2L5MQ47y3qNYA3Vj!2h}&eI+kn`r3b~&Znm!hg`3j7Ai{|@vbx+=({1X3EsHr$}yBkNE?wguZ zbM6X`RQ1o>a3ts6nGH`HKR8~*WI{kU(z zCZ~|CeP%1Xzsw%6duiqF)Ol@aDQCTW!MUS($B)Qc6gX`%tXQ|PT;VYnBWb<`j(VaC z&$i37Y}MkE^Y=ZJ*HP^yxeF${d|IZMKF?V+{P~~`n;h>)&B;ES5&cV}jQ!en2b`%Z zZb)1!e52S=Dc<;4qoXO}>7aUT!Z#(w^*6kvm6d9gt5!P67bzZpQFcUV@`oq!LFCeP zq%CURlVPV$FVH*l_Wi8zG`{oA&rE_i=UyOgSmaJlz2+4Z8yK3YGCQR78-q(rtj)44 zT`ApaSMDY~g7V_~{w6=ackB!dJ$Obtui)S&Hg~RISAMM^H3~(Av-r^aA6YrNc-<8AL`VN<`#CW+1cu} zKuJYJVzKkQZ64F`7t9k z_p@JPso*^gqpg((Ej$a0yKQTRB}?v}JyOxVzKF7JJEcqGOpE^&TV27^B-L$OvO43; zhC5rM$F3`LtWx%z7&qsam=)1bvW2bc8fU1QOKrcddxg9UO=pg_p{o^@UtHw8sAz2d zj_`q)XX<6gp1AaDh8A5rQ)HgwyZD-dxj3JS;xmRrhojt)+2zUQuMz7*T&A<5(@V zPOO+)@9b@DHiNx6xlY5o+xK&hPRcoM8t;?go#$R3pggV`uvK%tkMnxR>qqwq`5n5+ z&3{usGrnuvN%^{sZ@D&&zQ0f%w{(Z%rJeiVoQrA;zp|d}6R{|-nk{>ue1-K%XBYWa z=OdhVc^YdqM!5C!Osy_;S!~5??uivVRew9F&|vvvhXt>89a{7C$&LD_<$KeK&+g5p zX)lPk-R5ni;e6~>d>>)ni1zq(le)I8PrL9fTmu)q@FV!AH+-z`iSN+iEAKm;U6YcL zA9EwuD_`a5q80bFi`!!k6>JPx{o48XX?OeTRc-Vv=SA~Ko3h388Q}}llOFJSsyb31 zdW9^Jt^IK#P;%$Rt^`Aul}qaHPRemarg<98eo~kHbrt*CGm`o44ir@jFTWFY?0Zya zo2cFpS~lo-%Ux7!wLm1YD(Q+CQ&Kx=>}j#J1+{^^LY15;=<7-T2PLSDn`yRr%^^A$^b%pRi}E z^|jlor{8@)xwUzYnNAU&O+4cDZRMcgNw-Bmf*mA|%EX-g5g!}Z^6*J%3j^%M(2M39 zk6&I28xQY%JRteGf1!@=`382wO^&4rvds$D_l{=mOn7NGc}eo7ev;)zH*SLyPHcP{ zyI!0!9A_UXU>~Apsgyg79Ev5i<(WvyY~&$q$T((KtLErl_Ia^``t2i)$J-X@H->1Q zwUY3Ebu+=UYUnYe?7c~4)2oLkP@nk@`0TbZdnTS%EWZ^N213G17^R;53uWY(;`KdUw-VPD|l zK+%e)OPmw?jh5~S#)~hd+$2=ilB^HD9MyK;(tbGk(P_O0CmHlqBd)nmo_RW+eb`;O z(BZv5-_>YUBz{CN;GOmD+2YTYmp8;;{dVg_c0!6>eAj}cl>-B=JRwRSmH7G7e7SAB z*XYO{4|l%w`kubD{nEEW@l8pSPZZke?ytx8j*Pqcn<%L;=A3@0n|N-lnKcI#TJFsKx_{Qb*&?qhJ{4LF=<@EjYnl5Z zG%3x;yyFTv?kRaf>1Dz@HNCo`TsyuhUH#t8rZoAulU%Jf$6lu2N^#2-s_;GMjpL}1 z&NI@xJnP}3rG6!qo)@l^PtIvL9Z6l5=_;J=?U<6h_vK^G>z3sw_Gpm~KN1Zv7+7W& zRotk#NcOU&tga!|@Uy}7Qv-LFyjNa+M~3gD=+M6?|j6 zZ{1}R)-9{7_iv*eCi$ei4?P^Mj}KoQ>0nU%-jpNhc3Q#PqMpVLPa-&frX1WFUEuwK zxAgw00_T{^Hy%qztsHuVfq-(*wYt>`)FcEG-z z((@)s#wTxJ=~7*J(a{n4r3P7h-nfXAavXEa-IHtTyGNvcla}bv6}`^d(JAI`Lse58iOQL}W;`XeQoKji28=U&5kovF($j zbAe`OS~M;^co=e_XT!tzbxIm;2`>&U!RsG-St&u}FbqD{GEe{WaBlZYaM6q0*|KE1q+h2f~{fx)V$7)juwerq=4t=DWJcJpaXh(YsNNPP5!}-6*x> z^U{ZO)>dZ;ofQqa*RnNR&&VSC70rC&NX8lq8jp@$RaIlU-l0d;PrW+rbgM$&Ra9>_ zY}>o&SkwE)&kYW}E{pERoBA$v?LEJCYe=-QHom>I-FMyEQm$<(E3@NQS(IuU%X(_< zH(4`WyrnGIzTvt^DUXnvyrSwy)rgLbXN*^N9~E~yf~zcEcTU8MzP!KktwBGYP3n5F zIls>BgO|!rbr9}1D)$x_*}dAm#P&!Gp{pw^(17%9g*Ky+e?z{yTvOgg?k!QGoSUz^ zHPwu`PCUckIkraNi?YyuvCLDqZr-+Ad};qkTPyy1{~q$5XYGYHdKJ~f8<$f9yzE+U^S<_99D7po`q7+Y zH|9EYbXKn|RFfUD*f{dAtyFjS^KBK6-d?EEyz(yS&D=$tS^Te!eYl7TR&O70oypgm z|34VJhal0KFig;8+jiA0+qP}ncHOdV+qP}nwr#s=?jJof-O;`2#o6ZWL}Whsy>HqJ zU*js1rc(lK%#uo}L{QNh@5tSy2gQ)8zDi4U-|i!NPVb>_0fpc!($FE(f^b*Zi`*Y! z9?Vvn)jA6S&wB^MGyz=!_W(KqyUEuY3l?6KHwG}|%@e^aq(@}e`ZNnZ5405KmPd+s z2zfcm6*OzC$W%w23?P2y1J%ca8%5l;lc@RhP zHwvYXAY*(BtwOe`ZjUS(-kJoeYW|2@^|{pe`Iy&ngjgSNF)SwcfWh>G2{aEsiw=5H z0h-t%2HPNu!17T4CRjV7c>w1Mft(b$L8$@rOfDgFFFG4DpK5mtIah`Z`3v?p_*ZG) zM$Ga|NIHQ<#_#i+a5fVZQ?@?}$-bHf=+3PmXf_(d?AwbZ++xi)#FL#e!oB-3z_RzDLUp z@m$qyH7B@`)%IZ-^Sz;r`62Bm(OVaBjx#omlvBSl7G(l-tQzycTykvOjbWBSIsh07 z=Ha;2ZsR>TRPFGyfhBK+fb#bG-GpX9nO9?!0DAl;08rg;)jw7MtG(Y$Mt7pz<;~bJ z9buz^+dn_MT>h(2@nzg>CsV&C>yY3k@|_n(CtgUjj4&h| z*T%B9OvNu;NkU6vg_+3N-Y&9}ou^)SmtIYVSPwlxZ;HK=6R8}j=o89@B5@EwTq@0pLOhzysw;O#^AK_uy=W6g)` zwCfF7A!>H#E#>ZwM@QRg>9n_)7wJ>BR(s&u;{vX#$_Q$6bEbRSHH%fv7lV3}JncQd zjM=(GwRhdaa32plWHgI(Lq6y483FJ@?6Je*Dzp9dQVlIrb$y%MW>sz)T^NB3?Dw!s z6@|K9cf-q1$xEh}CGJo>#b&OvgUPQxeY$>yBPv4aN&^N1ZQlIqPu z-8-M>^@@qq!>1Ozuop3kggOm8*!(JOD6NRq!?SLxFq;a8LkT{YdX_8fwOxA8jG9EN zX18*SsCK8OssLJn-D`)TsuPPCp1YfCovjwLo0Y@|s~+(t5jAxpSZl}q(QVN-EoFs= zm8uJ~%3`98VcypCm@b=n^={N%jgc3-dM*P?MA$6VGH9DxP_vwJF*lIsbbo`7L}eE8 zlq+|Y6IIm?8Vk(c%J)}M!e`I6u%2lI8@QcaI>{#e8SjFT){xd;v3j-oicSWEI(h9- zfS))fGnJh#s8elkd?n=ktVk;X-b_$*HtEz-3;ZE-I>RPbRRJz>+eE6`zF^OXrrGh< z*G4-^x#0)RJYBKU#P9JX#?qV9S%ht(wBGzKjcYt%TRx)IF^YGzcZydsXOoZV;u*N% zLp35-%pFAm@5nc_7%b^#U_lNK`A*ifBV1cWc(nj%NIopc^Ui2yg?z3d8cVpQ*x~w( zs8(1W4WS1f7}0eRyG-FKpNX`NPPbT`cV#=k>*wRb2{4tn1uY*S3nR4_59so2w%ruX z3ZP22>N3jAHCZ*Ddyk5AE%k8sTg!(<23Od`^ zpHJIlwQ^$okPE}Bozjy9nkKGfv(tw^=yuPDr(N?r-McV<_ejHA1&5SkF931sM-E@( zFo7pMsKkR&=fjx!nnt7GBVH0!nzVRTvQDW_9tCHpq-zQ8pz+e%ls?h!9sP;FVjmx! z`eLTxkq%LUFhqnQ(z+~8ta)Iw@qGp>TRISwz2W#sG z&#>Pw58qh-g|m%>80z>u+G5`LS!ez*Liy z<2q6}cOgWAZS{j7Jr#(X3pJyI8Owm_MBgSL)~O6dt(ZB-c3z8%b~%GqH%A*w>uXKm z`uc-IgyeRlF#kHOhLdKS`3=TQ@{oZ*%(Tb&j)K$_MI$CJ0G+dg3No${R(Kw z@F{Vj1ZQ8OQX0BYk6RgD!z=L?mb@`pg6TuGCc+U=FA_Zx<8T&ujkqSu039B2tJ`*% zP>?1+8xL1G3uo!6>fmVaMdz!?NwX8X`I=1;N<<@M#qaF;RYq2drt6Puom_=^M{w`Z z<~q6x|84b5Pp{J52HD))Qn|PIDyPdyn}FaUGhgEdz{_SR)XoLOEiJyB_fguN>=j*O zgJ%Q{Gk3cJR*YsSnr0K5hJJQ|xddOYvL>Cpw&UY7>|}(V=S-bO3|v(=`U-2QmFmz3 zO`692;0>_}?uAXx+jGLbve^%?4m+U#(v-tXx-uC6s1 zj?iKanpXwoWzQV*zK7w)wsNum^@7lCuO~Uop-DkbazY~HnTdG=c%#2Y^^-`{&0KlL zH-k7OFL%byDkGipMSUiF`#>R*BqjZeqIYUKHCi!lbaB=+2|0&$-z#(m;7k0&os3=b zqltX=Hyt!*Z{`3P!=hwQDl*-?^ue(+*kJD?{^{w6=-Gn}6F;=;w^Z_%%_H`GmoL+8 zT4&@<9m{=83>zqb+fLJLI-xu~N`0xYn(SCnc1X%aa&CtEEbInnL)Nnlf>X`u%?mY%!ALN6mBx1t00rp=tVvi5 zKm=%x=bo|58~5)MjV13M63?|RBUHl>umYom^-ZLPSKw=z=p=&(MrBR19Iy$t@Yvyr zvcGb7ELKI4eOi-;xaFqqiJm=hBwgN<3u~vc^H0d|k58?UEliX`rP2Yh(ot%qy!LJE zJ)xX2as~olNj2hCIyzw*T5Q8#II5VkSa$$m%!)@znzOTGsoILWPq5l^Z~ZL2;O?{# zP#b#`_ykCGDz8P%SFNnW5jqn{FHovtbu|;uXVIrN&ieEWHqvx$U{B+qs<_^a9o+1| z;r-Km4(DR2I|89_S;{d!ffBY5h%bi%Sr?je)9Spxl-_4QU5qupoX6%Y<0{!wuGJdOH^yZhflS|rJ%y&Q&Bdr(OPwajUbBM%=~Ph z`eanAw#|xw9(f5ZVxBz$O2>78bbEU<`ZAem7U98K_9*(u4NhUaqzLowT_8kaXHpB; zbi<3^Tw!&b-d80cVh53`h%p2b2+jRld@4yEfZgE5owJ^p_I?aOe;7iWhZO%0jSIhd z`1tX%HYRlPYRD|cifPGlKnzWJ57FlQQ8udH@}IJgLPohH8v}=evJ^8UQfVJ)}85;Es zNkZX6EgCu_%4IYMhqsw_$Uki+`Jp`?gpRK;5vJvwDHy3Mf^ttwOhHVOL?PRxcboo%ZdxCz#=fa}5-`{Q0Xr~arQmbXl6{$F)5vz3-AAi<7ItdZcfF{4mMbPBQ2}rL z&2n8e*?S|KP%2l|IZt}1KKT=@!mYROWy*tK(K_Xn_gMZBS~>e24%0o+HsWFkjY!G3 zT|-(X-D_jI+yGJjnd|bi>PpHxML_$T<0Bn8ptOH*E?k2e9N4-c^1f%+97euB*OSg> z%o^N}+Dmwd%Lsp~BA*7mrsTI$d_>u&?Lf79<~<>!0$sgbv}vAO!Kn$Xp#UCzESlTu zBq8?cM>Ma-l)1EGo0Lc=l#Z*ss7q?QyEFfZ(lSAoA-)-3U6ymeg#6BA?#)mdY9nG+ zj}zSArsU;PrF2Qq8j~k9jF4yH!lF%LXWnXE#N69h8>cqEyuS{(Te>gO&sd=SWf9>J zccEd{S>xbcQfhlMi05shi-n{!Y1Q&4fPA{>^tA%C0|nNrT&~;Y^x1r+J8*fHuDxKX zy8|_;4~ouAG#_2(y#I{>tn)x>`R(rs|JG^doBREY_HO6}`oftGGpLe>`#G9>Jjg0N zL(>We`xy~9zy>LRcTP_5$aKq|niRTL1hN7B(viY24aUz-d+AVR;3ZtH`V^F1g?jGK z*2r9oa0iH<4`L5S(}p@!ggx}!L76+Wkv$_t!(PkTa?k@9hG+GHF;PoiBqLM?uxavq zydCTyl-g4e3^70|gM>4dR-Z_!2Qg%J7hO)4d_*{kNcPu+VT7;z>c}MdQ0PF?>N1fm za0kx_oSsP91OGne&&8-SOSRqGQaQCf&;smX8c^Su0;f25pyixOrDAmFL|OsIE#)f; z2v$sBskk@5pnW*4r6WD4!@C7X4-((LP&+OL-5B|ln;Dp1&dixqP78~?9n1RYerZ1h z*kI2edA}}Yq^OKc+@wAMNR%Z0=I!${z(AqfP{BHR$5yC*!Yx6#5nXf*5~OC|S|8hjI1u6@Bp!1u{~0m_9Lq@`A$q0{X1lk`LNey?zYZ%^>_ zuQ6lB*}De#bLFdUtNo_2mN*$+*s6`3rJN|+vOGD7dW? zk+4jKffWz3BH_2huhV`!Np8XD#aHwG$bN{J6eqVpW#R$nSB0~p&jJ|L*@U-j_}bh) zA8!x$uA${2$!=5;aQiP23vXX&Al5cSQ;u!wOpYP4A~2XbhJ`R@5g;A>{ql9U1l>u1 z@c81bjLpZX*sWV;N=M?RtvvZf?siH;RDp5QQwSn{>1Mjkw7!n}BZMyF1rUnd=Y)*` zIpM>(9>GU>oSp+)o{K#)oHP{77@<8Epd!y0VvUN8UBD!P!!|3h5VM2R((qTKM#D^X zo^w(#>|5ypot#Hi-Y0`08c^=wT4O5aNT_HHD0oOHEozWhcMK)90@5QMX?%-Ui(t1b z%+2zg9gP@o3h_b?)pGbB6i5q^;PCVVV=F{x3w-_D`Hnd!ZN)Pr1_NsnDu0vKpq5KH zp}4e2>eTw$@MF}*Vz=36!X|)wNgA4r_Uy@Gqm+ILdp~>)q$U!q3Pm{DkL}4ms&hM;g-zjjpXGFUpDNxFp^W zXyF&Xm92AIdvHU3Pf0CX30X0_*S?#dUDr%bwr|34H#T!|*;@HB)Hrz^Z!DLloO*Tw zP@q0@I>TiYgM$JOCsI*1AR2cZ|J@C1+|^6%YpMNiitp<_{jA_xVAdK(POWGr4X$8O z3>Pf5lvT9Cp%9%`NV$vyXEYtCXuRyrB{I(ys6q>8@emb6xk})0Izs4O#x-wNo))>1 zvquEx{G>fSUBlPd(N$R~yGX!j;TnLSt2XS`Wk6~7XJuzav`OBKQ%S71u>e^a04RJt zNPT{!Jb!9un<@&@N?Ix{9eGi0(Y;k90}k)1g}ivOV}2=wazz*sBYBrsf2rAr#_D<{{mVM zkp;{bM0ntlY?I)PjL}xEWsij|n@pncj*a6TAHBR-pypn$Rkt<-ueeqS{JykU7FJAO zt1x8+PJ7g8?6yx^`W^B#XxeR;w%WC3Xs^pwP-oMxvD>DD^kC;eQqC_XQ6sf1jP`$r zt^X}l#5j3I0geG^C;x%ChYmbs(o}W$6=69$OTThNP!za$ zOcyoclI_FNIIr6E7`e2}02;2y1({0=Z+!5H*Bm=7PD8W?cdSWB#FRabLhYb?t0}SQ zN$M{UL_tBiF4r42ma&;DCLh%ThIzf}Tvj5jg)*=;Y#;HVP;rd31B=mY(wFRuEKd@g zE~Tp_ov~+BcLzXcE|mxWZH4F-H#J)%wf|UWkFf_@?5EDOTFrcgvi@t0rh9ZUL03Oj zjY=1<`=bePBDZ%pCI`yvN@kaAnWLKD%}aOxrAqXQiCOX<=xpucRN`vq|4uaRng$$% z7rkDtih5;4U02$0QmBSqrmIht1&88J8=ni88y1)G=O zQYxwWa?kHfy3|6EgVxP`rTs>}#X6N75T54r24OY1VbD4y{nrgbPw}$dzx-dfW zPe|f9TgbN1C#$858Kz8cxG7!O1&z#2+n3+tB{&ERf4>vd9C39$qa4j(h=fS*2eXz# zsqc+v5VcUN*}fV^4{tBsKs;hpG`0rFJPbGSWvc@_wsREsjUdxPfaXRWSZUia=kmkQ zWe`syAm-7Jc0T8hn4G}mVc%uscZOeD>}ST7Ko9936EYgNxBcFKH^HXv2@8NyQW3uV z%)q&=0?jzWG(3CV05xx6l{xg~2g*NCSM5z|xm+(hPiuX9{ysgZsn@szh$D>}aPE2U z+6RT5ky2%J-Rkjv143ng$vyDRLwfmCa3IWfb*dpD7Zj)j`af3SV!bHEA$xt7dpde} z5cME+^yhpQnpQy9jgvph<7^kA+#09tmdeM1M}gO!qu+o)NUH%*@}C zqr1&~kuKe~eX=Ak9NR|DEmaRpbEj;~r}5k7gmO`mXJdXt?sbTq6ALD6I|$r8B)H%m zw!4-kAjTl$Y9!-toC3!4x4m40FvqNZ$hy2?*8#QS!m6bav%J5@hTr|eL{T}1HX0=L zmAA;h)k*(W^C2U;6HU%h@qn{g<@EwQ|~IeQ5lqlJ`)QaDz%N>GrXoJ#{)f zN=PrdAHH$sY=?m-A`E8{sQ@uiyFTjOy#XTV4>gWYLE4q(iWWiayD{8V_gk7rFD#Z8 zJEX^>QC+7Fhlj@<_59xc;G!p@_oX+iZ6TO`?**w`F_6V6D+mXFeC+5uBv5WGnYib8yU z+yJ}*Kp(s4;h1JGr4VKKcN{s5fjT{S#}1D|Nzm4w0Wx*WK^6M+j^>9hMrCE!*yM8b z#ir5ar&ss6r#|L9_2vEZt)xMiP}dnnd}tPW{Q2mGPXVAVRu{L#4L!4vu+XQ);}*@4 z%OZ?J;V`05C_Y%)LcOpUjXA>@$L!wplP256c__U4w0nJNRNtr5_W;eiesrbB1#M`r z$>?9_z7ML}HGNjBo74bsGgYO0**-x*4qoLsbrRNf? z5Iyyu07^n9s02KjOk!#O79gIVmMwLsCfRu3zb|;` z!zAMi)wtfCc4v~*jm(|#3OPiMAJTU+m$@ChfMu~Mi8Jht>tFP9a{Grl{FNwtxBPwtx zkKr#Wt!Tk7kYWcx1Zdmfp`fDq9nM$p9YJL}sW^9fo0QM(LQ_w^=prqUS z=kG*#d_s?6e5_S?5F6ZSeKulnG z9&cZB&bSZkR!m7Ztf)64XY@wUAn#I_8qEJ=X zf^h-9PV4wy;rhA)33bt*Rgv@batk7}4m5~kVeFEsXl|0E4n4c9vixnhn zB1(dYgyH&-;)S@<&~g@Lnj+euOoM%vSr$wz)d8QPj#%8S#e95$yV1c9Ge-RrNdr83 zCWS-FqU33U{q~@+zf?$L46X8;YM3f&MHRD0sCgiT7@E&EP^_mXq<$2t`t-ueO&?q% zO3MiH-DLoNuYIg3h`~wq>^g}ON0M&!IAZy}s52rUm5)SO|EwW$g**h3LlXgxZc&_? z!e+LevmEyd&#y~d2ob!2d7toeQm@9&ol}L(xAo&>{JaPT*852!K4Oj5lsJ$%4JtH5 z8o7Dg%L+{Ck3{BahjvT+stRkfp{s$1*1!z3bXgB9n#PQ;{jz+#p1LwQlma8^Bz@U& zMFlvuA&F^(H@KRLE(SaBl>}+%h%J&Lq`0R2w~0$}I4w;^hb5qbfwF#(0GN2FxY8=v znZZeOE(hx5`Lk$UE=8)^o0VxVUCF8grW9X56J8e0U~*P(k_Mxh2QFYj8)|n`LA?-l zqu^X#0NWuOr1)egj$nh9^T!;BPp;8SE+jrjq_c7*ik0DMOHSTEj%Cu$LL-~N`i@QS z+8)aqNAXdbFEJA9KW`Iv0x=>cS)K&7wI5S@fm%y|ndf^dAdlT*O%yljcPR6pfHuUp z$N12tH3>cG<)AG{bPuc(NWZ}(Il-EO&g-bmz`}S9he74+2nt40Yl($)g*0-qE+a5U z#2~nAry-p&ke`loGf~GT4R(&gvk(A;_lvR(Sp}UeWt+;qZ4u9ouVNB)O=Y3fCS9lG z^^2;fZ3}jHsPnC>?Qw6IfvbsQ@L-`wSA3vIE=a`I zM}-;e-bo5nk}D5Fo?(Ck8MpZgri485G5B*)k|yL~e*uH7x*cl0{ZYMmIl@y{l(UEF*5)N01oVnyN720$xEY0RONJy9aHIAe${i~hLIl30(o@1)&mv@wvL18Ew` zV8Ro}bbwT%T80s6fq{&9EV956Ha5FM{+iS|P5%;XMw4>##r8?%u(0u!y>N4pnoWTAxL}_-l zohoV>Gqa!(!6EAr=M^iRn~*=K!Z0`Vpr+&ta$?L{r!6=Zk10y1Zxp0%A zvf1#rJhwdHVw^g!lMYs_8PE1X*6C{I3*ol*cZ9gnV3sjNGs5>9@5BH0uw#~A>n`s7CzvkvvIciFr4gH{8I><`0 zkzR7>%SYuQ{LOxE2FhUWFIah2(}Fbn;)tS3A1(!gn0`vo5?+A2>i zNtCVCbuU0@{+LW5Re=`a?cV#jJyP7xro_M#Kg-&+dhqJC+zZf8k2ZCs@xk)&LKmP& zqy0?Tl=MceH+~$tly*)tX)=CzlbE-rJdRU&uFFbBWhBAptB6NA@I@)hs@V+N#U`0- z!BVaj=?U)eG1D8Xr6W0-ptq;&cAS}F*80mP00PMbuXj{dlLB<7E4vf?LzBgri+n( zzn^tAY~%rBr;g#$dH^qa)_z_3y#pIGKw6-8$w4a%6+i4=DIuY8kEPyQamNm6*It*t z?4WnF>af@BzNR=Ydi(O+awD9aI+TpdtPomj=~5FTzR7s1Uki2bGC|ji5(Dc1+i+BV;}9%s5QrAKi7fQzCAvQ>Tx^uPZgx`s{q0j{WVu zb)Kt{H!GDo6K}zBC#AD7;QNtssY}m30xEXkJH;&*sl+N|dp9X9h{2xgdo9ni#b1?J zu8ZbiDI1;i7QzY(HG9#%O7;aJLo1gNk-FrHRUyu9OBK(tCcrD^BsE?tX2C=zVdPQs z^|B{zKj)I8>Z_WjIXkC+X2^qlqcn-L!-=-4d`7Y!`N1#cwybYl!ULzw(>I zmoL%t72aAFVd@Vvze!L{|zFM{ydwF-;aj5 z<4;nu6!TPLADq_g-Suw~pdQ@4gD@f4gZp=*LnC!V}lqwyzLP(oF zVzy_2f4t1pa>sh3NK5T$Wvic-^DSmVomfD2oX&GOQHGK#FQUmIRmRw!12Vf*excNR zWXn9_P1U~b<%vMs#5Or&ar9q5mPid}ywv8KJ7UkKP~^aNOEMgLfEq8mtF?H}C2j0sHF#-GX5BJRY}^ zwh&VKT_>C9Ree1inh(|Tc2f$$2kG!#O7`_@DJbFdsuV2cLkx7>KU_MCLt^+^B)ZFE zK|X6pH`}2?-{gHM!tnLTy`*c{n>c+`UjlCweY+9>mK2%?f5wo@|eJ0j(HqNmI2=2d@+bGoZWk0SaFRCbWP;ro=tk{30Gyb_VWD}M ze&ROaU1gf4Sr=jRo!mzS`{kjIIu0|qw-VjS*5@BJW5E1KOUj}WtHNqv$(pU7XC!{e zM0}Or+$g$12fOfg$MBL&FTrS(rd*0xH!Y$uBfWfY;`_4mdA>4@{rQE&Tg@d62Z{AZ z>sv&vzg_QKrOQ*g;`A}EZ; z4RqXxP&88y{o6A9yegnR8sMT7DAlEOBdFKB$RA$mX3;y+$%^bRO(|s0ZDI%@4b)ziYHB}D7GXDi4S?*NP^FNnd=Mx zf0k1Hhh;V*6pFiu006J%003D3Czk!M5h1b=h`0KoE)9^ zQMjEwlg5k|A;se2#<4`Ge%vDGZ@q4AfN=yO=fXM8Ju^j%>Rteyk=}Fw2Y9}W6h&#$ zht3V!r~iJZEcO_ufj!^d-fj&_rkO*QMO!sz$6djwxc=cm6>m49?mn2N@=Q!1<#TU7*^8dLKl3 zcg${bvUnE4;z79$hVRU?IUznX8c1@%bSu^=kT0^$RZX$)93`>3}vAC$dtNX9##11drgZUJ#Q~x(8E0Fl<-AgVgc#Uy6P9uTV-rF$; zvK8O=C#+m!+~nUJNw#AS$a4~1YNq%`^4V1S2nH%)!*3s6)5yR5kIxBG-svXc|FY-d z)&?8&u!o(Mm2PxFV?*QNr|ZE^f<7mril zk)KoFQ-nH<3!W3YZ;m<*-2msK91XZZHNt{M6T=@*NAV?+19k(y!~!($c7S>(SE$_t zlKJVD)|y6KU(CwGlA)vq0l%6<0aU@|A1NCwRZW*pua$_F5R}zY zUVPcos+GxFMTEJ-6!6>9Q_Z9@Sqet%n2asnBvN_-#sMHcN#pa81`;Hf;cUQ_I>&p>hG>#rIU_2zJPp>rB>V??6y*oVr8r z2%51ll=NoN;rdWH=s@qyC_=!##$mL8%1d^o?a4zo?4(`VEyMz{g(roa3C$e(XbI3y zx_2<|%%)?`2qx;h1c2_VUNn;U+|b$TD+)Owo2X;P09 zM5EdnYAgd4*rcnR+IOrLDag%dwd%M{&nZQA9cqIVn4-6+9?Xb$(z-c1k4wO-fXXoP z-=t^0HPjoRu`GlOWX&gmP*M0G+7sUSYt=RcM_C8E-ptV0l3WPe8CnQ)PQ#xrEQZS6 z_>Ga$@3x{{3GNJ;3@~PU+XdFc#!09WW!gVULe}DgL7}HtNJNn!p^mUb226m41$D2L zuCFzhL|!>fl84wZ)h*28U(kHdr+Z?vQGeX#gL@YpeVEYsM%Pkpj-8uyVUx}o~T;5-L@rgdP@e!+^I0a0@OV99~g zP%Ze&CsxaziQpr&6^Tl~su=Ng2a9o@qfP~m1iEwt&2HkWElR6#GGFkHEGejY6>(3w zUn82B&*m!I(RWuVm9bQM22sB zKjQ-^mp#oZ0D&+8rWR*MO8_al6-w1R9_~6u$jFvqWf+GI|>VI7YAi%nnXD(&huvZyU9Tf z!j_v^tc~07{bY{{ARbO9+YHfA&~ZRR5wWvMo0xzfYjAQN%MX9RE5i3@XOP`rgn@Dh zhb9<0?-K)J#l@`P34WFJw=ts7f3(K-&r;)y1#kPQDcakozPrd>fO0$_U`Yy|fscMK zbabfqGAcr*QzlZL#WQS6D8_YkL*z-siAdzd3$KmYi||WFOa<*0Quz>71w$`zu+Su_6reb%T`WQ~~bNCE*tU(UMNJc&b!8+=iLM1F6V2 zk;thjUsvj0c{K|$Mvjdo*t@ie>x-KBPFW?c4PF6`NSF33$gP0!8u`A@J&q&W8Q(XT zTVXaDIhmj4%_^K0hp&v|z!vxB(ku#G({DritrLo-owTCtj$IOEveR_ZcXH|`H!zl4 zhl$3gBoPf+m*e!bQPq;k(-TZ^Y*DBxKuO$?E;I83*GtetXCSl`ahErw1v3CiK~IWR zm9WeW=9kory_8X386u}RRdrlj&ZYX6~-TR03X=SNU5 zmfA}!BwCA?ZF>gEx4k9MY0n|(C}4vYNxpo;{0C@o1-4O_kQl;z>W9Lmr33OStLk{Ld2VSDPdehOS zVKWbsG@6p6Xa1Qm(y0;Q;iTDr+Ea!uflg3B;9Iz*bk2InyqBXwP4l&TrFBr$Y|By_ zTs@n`1A3}NWF$kg;RqE@Ajc(L{B|040JG@RMX#`d(9SSgv;T{U;AN?)VCPi$FY0`K zRl6Z?=TiPjiFS1*DO2`GXa7?dPO$$fF@z<^GfLPRSfmx}8!3lR`#4j4>+wDm>}9nt z!z(u57ZyYZ3h*wE|4E<;x2QLp~@4xWQSCofQ=%t%4Cv*^w z$E8wC;1&Vi+?@udAKuj2w~nX!I5twHx3H5<6#Z2g%WA?X3XYVB!#|DBWlbL=_K0cdY9%9{G;5@mXh8jV#x%9w9`rA?aS!FQg~w`4aO6-hT2D(VKIRu8Y&UW# zN6eVtMy-TW#=^>Hh;zVn#CFY04bAThC^O6nHlQLM4VQ>r|8z8nadnm~t5bsOV9#^_ z#EP$44#CuhAO23HhY4RavxJ7RQF5xJvDCZL{DZS)k2cI~mtRWs6hJUgB7vmsH7RJ7 z2u%Cp^FtchhbgIsPMBH8c6GRiwiRGd%dhE{<<__tYg9Y3%nJLWUs8@}-8>zrX#l$p z$J8}TVz0yz!}2(%$lqej5$`Mob@t8)DeSSF zSnkev&A`_h%@F#^w}}v~ffh3EVSO9%;*@X>=TCX-*`N3p68Q|;8xEW}7VK>F6_Xec zeVs}1BzZkml}I!W%_5UiV6^I7WeIrDDE>3&6w%xtK`hg3tADN^2sOYJKkZj%*GPiw zrX#aSR@}Y1gjJwLxzXhpMnzg0e)%W$<8LfU3Ryyr;`~Dy>9!Xjikg6~@~T7$n@&nik;Z-WC5$fd^J;*;9(n0!9 zhBhJ_I=b%@*{zcSD}H2I0>LbG&(;mzwtHMw`n}tlTwVstG9h04U8wy5}{-UG4!cMp%uF-mxqEv-Qp7(NAkWf1((IvZq z$Bo+5-Il%UsdTmKx7O{xrQ0m|1_<2JGR<6{%O>aB3T%1%wC5<}u$`ZG;XN<(j@vJG zkMnZT(czXnFex`x@jP!7YsFk(NYS%C(VDboI4@-LO|V8L%x<38SS)lqt!v}2)tg>Y zzHPU*a3j>%WqMx@znsk39)A|*EJ}C(lOe{O^Vogeg{s%V9FpeqWMmdO-!+W|RhWy@ zzm3JML_9N97IeY33h9BL392|e%O5PScAC6Rf7>*(3HnEsErb1jy6KX4_}}?aH7O<) zp4aov(f}(Y&eYHwb~&AijjE_@I%jSgdzixadsj@3PiBg6J38qKwyDNxAlxkM*os@9vb9-cyVBvP__eO*2``bCRyism3bPQ3sXzhhnLwT z(6oJ%aC{@uVii=&331Cy82dWWmGQF|N6Cw0hAzRLZm6D@8e7D7dA7S6|C1r;M;i%a zJ$wz|E-IU@NHJ@H1}=pz_}^8c9j-S1yAluz*;9NM#(y$YVZkw%q|4D1DCe}~ziXO3 z1J>(q&X^fHcFZ{E=H~P4fMYb!1lF?{w zEWFKSUJ`RivfIYO=*aOuTj& zCDa`2JSS;*-I`K3o(m}$O^1V@1dCy?a#E&fI#^SJiCtG2dj6x=W+LGYM|vJ5h%dKh z=8VE^+dFk;{&i|8m0-aR*5|J$&e>@JtDeY`-7u!Akg}&%B!Nea!5zJ8HW?MuTm{FU z%T(ETvwpuq=w22gfcx$_0zrHqrS z1vuJET4qSH(|dHz5d-E%7KL%}#6Q4`<|T)1k`?x;ok6^Ai1*QC+#$Xpw<}tMP4Ppw ztXftWV|ld7$gG14yY5EqEr%6*S4SI@KG7k3AVbp{xn^EUV@`6F*~;hTd;j$4T3ih+{oJtU3&;Iwr8Ng?R0B2bbi>4!t zR5$^Hc#Oh!Sx4UaJ)_*>;m-mlKq;VB;b)#tEqOVjnnt1wS=~OlvX9OIRd`Jz!v+wh z%@s-#v}YXWA?8WTFl?*9+DL$nP@t5TqMcythDF}cvc^X3eYp$>rC_-+awMRr$;=W2 zOiki~Fxz&bx4NfI9u1rdFvV|OG#A}jtnkv?!(*c!MwmB%k>t}#k_6fjw=d;6-3^TF z5BUFNA@({qu0*g^M&N%O1o-d!S6TkQ&rto>lul#s{$J&}iJ2MAe|__G#wPYA|H|rZ zjokJAd!fRK*7>jV;lJ0$|Njp3|JE(3vw<`K1prWo1puJ>zgt&cN{mKAR#a|SP0DFg z6sG4=P2V{_X`VRy3CSgEsh%gA?7lF5K{}QS38|H+ed=Zb>eri(TFu$%Gn`#L!1?e{O*sy-|)R8Mzx}`38 zqS4$gSq?24AKES*J>5~Bt&RGAg7zHw=#2G41|&0;X(mZFm4wpA`)F(E>WSCwG3$G; zFoxZY-o0}}{*k`T6kS8-4dJMyMU<%{{!a5(s#fWmg=@hbdB87Yci8v&lDg{5Jo53CK5>TyEr#ebYN{iSzIZ)pqD1a`r8*Egpth z2@4fAsq=eX#zMC~H|fiKk|FU0juTl&i*zTW3{}JwG4-(11__oq#{{L~9Vr#}kE^Qk zq2@As?`HlKIa7)iEP0gspDkQj=bI*%9;F5>9!7>6K#Fkg+ec(Qls+!X9nk9u4PzoY084Q z=YCHQls|Rv^I@DmM>F?G@C>7;b~C?4D73B)4CD5E|0LAI>9z37H5(XImn`m$u&gNe z@u}ilK4JNS^4@KMGc}x zSWx`dqVtlGE55dRuY2I}-X!kauUn=nd6BZ*5mJp;gG75p-u+nhpgZw`p$sRJsL>^% zyLUF~cYjn-DJWX^L&{yqp*~mXd2WWtUSZP*d+#h}{p67}J9l+t^)jWR zrQl^@Wpd-1)&3XFZlZH77$u^%;!0x~KGv*LjB=`yuE3x=H2nu4m_2 zKXvY(cB`l@X2-jyZ~p(VJ$&ifI)$f^oqb$Og3E8MX-v4aQTEYFhB}U(HTTq4Jf0z{ zyIx(rMCnDr9Z4xG6aQ273p?A3e+hSX85kKvZd=kip=r}x&*0)IYM!Na5q9oxe61IE zt-A1e(K_+u?++_4&aSB2Y<;IDSjlz1VPPH5v-}qKCBOfc%l>}ma>V>>-N)Ey1xB;u zaRr7?tR_4ZQF$oyL;2l(-+9@yrnjD%xix%W$>WE!W(pOrna-XWXwp|*UG#DO?{`-p zx{Izh5sDIQJ$#>QTa=aiQr-BulR^@5OqO>oUA3$5&#MdP53krV6Fk?fJ@2RLOoWB=ed0aJV>*jm5x~Us77ddDfEVn)8=j$Dq)boW;Z;_=!y4QVw zxeq(6v?k;x2)sQv!L|Iy-W2s3wxGR6kqZ4AIQRT@jNHj8nUKV_gmJPNYXzH6ssFY3 zhQk`i{!IA#z!kX8%yq`5cM6lI?fX{9dZ2tR>%sivbJ+SgfK{DR^Bwm%wLK~pQ$9$v zPng8X;rosC#VV;)o+T4>yX)@!-hBGyejk|`8|F9}#~j$#Zjkc!ZFoe^>KlifX3K|% zo0q?qUgMtmHX&kVNnf(qv3H#_`C~kP?@GP?b=|$MAqzit?Ofi~9#R>&amo2|`FHR3 z-{D;sRI>j}{F;Z?8_Y~wudrP0^*gQlmuLIEhw?q@i?pv)=`F2^JT~idhwG-D{gdjH zZo9E}{{Gsf*S&%D=%<=p`{&2qoWb;)k>y(2bLZ6gzqX0Yee2ZIzRWt`Hzq0Ba=k~^ z-ZQi3s@~b<QVPT2kL-yu&08ETKh#Sf99(8g@exsN~o7)78|~IC=iO z<}co*At+Fj#Z@TNKdjwLlEIEFEp7F1A`G*grJL*)zq7s-Z^dAvgNPSo-KX)n!RH8>#AQnecJd`Aln;Hu>SZ-jnqE1ePgGk}#gntt%QXLJ+L zr+*M8O!dMr0ckP_*&J-`1%x?kVld3X7O&V^59pSlHy#j{u*4E&39KoBZZLXUM;N>= zi711SQ$D)k=!qO*xKJ`th9f0;bhFWuCc^9;#F>qfPy@VK*+3?60O5Ql1_pr?5Dx&p CfJ>nO From 2ae4bdde93aa70c086ec520a11bdeef425f3a235 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 02:11:52 -0500 Subject: [PATCH 140/687] Cleaning up useless junk, this'll return at some point --- twythonoauth/__init__.py | 0 twythonoauth/models.py | 3 - twythonoauth/tests.py | 23 - twythonoauth/twython.py | 1322 -------------------------------------- twythonoauth/views.py | 6 - 5 files changed, 1354 deletions(-) delete mode 100644 twythonoauth/__init__.py delete mode 100644 twythonoauth/models.py delete mode 100644 twythonoauth/tests.py delete mode 100644 twythonoauth/twython.py delete mode 100644 twythonoauth/views.py diff --git a/twythonoauth/__init__.py b/twythonoauth/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/twythonoauth/models.py b/twythonoauth/models.py deleted file mode 100644 index 71a8362..0000000 --- a/twythonoauth/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/twythonoauth/tests.py b/twythonoauth/tests.py deleted file mode 100644 index 2247054..0000000 --- a/twythonoauth/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} - diff --git a/twythonoauth/twython.py b/twythonoauth/twython.py deleted file mode 100644 index 7015fa8..0000000 --- a/twythonoauth/twython.py +++ /dev/null @@ -1,1322 +0,0 @@ -#!/usr/bin/python - -""" - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.8" - -"""Twython - Easy Twitter utilities in Python""" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TwythonError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class AuthError(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, username = None, password = None, consumer_key = None, consumer_secret = None, signature_method = None, headers = None): - """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - username - Your Twitter username, if you want Basic (HTTP) Authentication. - password - Password for your twitter account, if you want Basic (HTTP) Authentication. - consumer_secret - Consumer secret, if you want OAuth. - consumer_key - Consumer key, if you want OAuth. - signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() - headers - User agent header. - """ - self.authenticated = False - self.username = username - # OAuth specific variables below - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None - self.consumer = None - self.connection = None - self.signature_method = None - # Check and set up authentication - if self.username is not None and password is not None: - # Assume Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - elif consumer_secret is not None and consumer_key is not None: - self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = httplib.HTTPSConnection(SERVER) - pass - else: - pass - - def getOAuthResource(self, url, access_token, params, http_method="GET"): - """getOAuthResource(self, url, access_token, params, http_method="GET") - - Returns a signed OAuth object for use in requests. - """ - newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) - oauth_request.sign_request(self.signature_method, consumer, access_token) - return oauth_request - - def getResponse(self, oauth_request, connection): - """getResponse(self, oauth_request, connection) - - Returns a JSON-ified list of results. - """ - url = oauth_request.to_url() - connection.request(oauth_request.http_method, url) - response = connection.getresponse() - return simplejson.load(response.read()) - - def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) - oauth_request.sign_request(signature_method, consumer, None) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - - def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) - oauth_request.sign_request(signature_method, consumer, token) - return oauth_request.to_url() - - def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - # May not be needed... - self.request_token = request_token - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) - oauth_request.sign_request(signature_method, consumer, request_token) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - - # URL Shortening function huzzah - def shortenURL(self, 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 url shorterning service other that is.gd. - """ - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() - except HTTPError, e: - raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - """getRateLimitStatus() - - Returns the remaining number of API requests available to the requesting user before the - API limit is reached for the current hour. Calls to rate_limit_status do not count against - the rate limit. If authentication credentials are provided, the rate limit status for the - authenticating user is returned. Otherwise, the rate limit status for the requesting - IP address is returned. - """ - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TwythonError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): - """getPublicTimeline() - - Returns the 20 most recent statuses from non-protected users who have set a custom user icon. - The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. - """ - try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError, e: - raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getHomeTimeline(self, **kwargs): - """getHomeTimeline(**kwargs) - - Returns the 20 most recent statuses, including retweets, posted by the authenticating user - and that user's friends. This is the equivalent of /timeline/home on the Web. - - Usage note: This home_timeline is identical to statuses/friends_timeline, except it also - contains retweets, which statuses/friends_timeline does not (for backwards compatibility - reasons). In a future version of the API, statuses/friends_timeline will go away and - be replaced by home_timeline. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - homeTimelineURL = self.constructApiURL("http://twitter.com/statuses/home_timeline.json", kwargs) - return simplejson.load(self.opener.open(homeTimelineURL)) - except HTTPError, e: - raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) - else: - raise AuthError("getHomeTimeline() requires you to be authenticated.") - - def getFriendsTimeline(self, **kwargs): - """getFriendsTimeline(**kwargs) - - Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. - This is the equivalent of /timeline/home on the Web. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise AuthError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - """getUserTimeline(id = None, **kwargs) - - Returns the 20 most recent statuses posted from the authenticating user. It's also - possible to request another user's timeline via the id parameter. This is the - equivalent of the Web / page for your own user, or the profile page for a third party. - - Parameters: - id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. - user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. - screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % `id`, kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/%s.json" % self.username, kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, **kwargs): - """getUserMentions(**kwargs) - - Returns the 20 most recent mentions (status containing @username) for the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getUserMentions() requires you to be authenticated.") - - def reTweet(self, id): - """reTweet(id) - - Retweets a tweet. Requires the id parameter of the tweet you are retweeting. - - Parameters: - id - Required. The numerical ID of the tweet you are retweeting. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/retweet/%s.json" % `id`, "POST")) - except HTTPError, e: - raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - - def retweetedOfMe(self, **kwargs): - """retweetedOfMe(**kwargs) - - Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweets_of_me.json", kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedOfMe() requires you to be authenticated.") - - def retweetedByMe(self, **kwargs): - """retweetedByMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_by_me.json", kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedByMe() requires you to be authenticated.") - - def retweetedToMe(self, **kwargs): - """retweetedToMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user's friends. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://twitter.com/statuses/retweeted_to_me.json", kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedToMe() requires you to be authenticated.") - - def showUser(self, id = None, user_id = None, screen_name = None): - """showUser(id = None, user_id = None, screen_name = None) - - Returns extended information of a given user. The author's most recent status will be returned inline. - - Parameters: - ** Note: One of the following must always be specified. - id - The ID or screen name of a user. - user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - - Usage Notes: - Requests for protected users without credentials from - 1) the user requested or - 2) a user that is following the protected user will omit the nested status element. - - ...will result in only publicly available data being returned. - """ - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/users/show/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/users/show.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/users/show.json?screen_name=%s" % screen_name - if apiURL != "": - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = "1") - - Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. - (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access - older friends. With no user specified, the request defaults to the authenticated users friends. - - It's also possible to request another user's friends list via the id, screen_name or user_id parameter. - - Parameters: - ** Note: One of the following is required. (id, user_id, or screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of friends. - user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page of friends to receive. - """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/statuses/friends/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/statuses/friends.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/statuses/friends.json?screen_name=%s" % screen_name - try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) - except HTTPError, e: - raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFriendsStatus() requires you to be authenticated.") - - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = "1") - - Returns the authenticating user's followers, each with current status inline. - They are ordered by the order in which they joined Twitter, 100 at a time. - (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) - - Use the page option to access earlier followers. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of followers. - user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page to retrieve. - """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/statuses/followers/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/statuses/followers.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/statuses/followers.json?screen_name=%s" % screen_name - try: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) - except HTTPError, e: - raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFollowersStatus() requires you to be authenticated.") - - - def showStatus(self, id): - """showStatus(id) - - Returns a single status, specified by the id parameter below. - The status's author will be returned inline. - - Parameters: - id - Required. The numerical ID of the status to retrieve. - """ - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - """updateStatus(status, in_reply_to_status_id = None) - - Updates the authenticating user's status. Requires the status parameter specified below. - A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. - - Parameters: - status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. - in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - - ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references - is mentioned within the status text. Therefore, you must include @username, where username is - the author of the referenced tweet, within the update. - """ - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": self.unicode2utf8(status), "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - - def destroyStatus(self, id): - """destroyStatus(id) - - Destroys the status specified by the required ID parameter. - The authenticating user must be the author of the specified status. - - Parameters: - id - Required. The ID of the status to destroy. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json" % `id`, "DELETE")) - except HTTPError, e: - raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - """endSession() - - Ends the session of the authenticating user, returning a null cookie. - Use this method to sign users out of client-facing applications (widgets, etc). - """ - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError, e: - raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent to the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=%s" % `page` - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - """getSentMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - """ - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=%s" % `page` - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - """sendDirectMessage(user, text) - - Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. - Returns the sent message in the requested format when successful. - - Parameters: - user - Required. The ID or screen name of the recipient user. - text - Required. The text of your direct message. Be sure to keep it under 140 characters. - """ - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("Your message must not be longer than 140 characters") - else: - raise AuthError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - """destroyDirectMessage(id) - - Destroys the direct message specified in the required ID parameter. - The authenticating user must be the recipient of the specified direct message. - - Parameters: - id - Required. The ID of the direct message to destroy. - """ - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError, e: - raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") - - Allows the authenticating users to follow the user specified in the ID parameter. - Returns the befriended user in the requested format when successful. Returns a - string describing the failure condition when unsuccessful. If you are already - friends with the user an HTTP 403 will be returned. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to befriend. - user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. - follow - Optional. Enable notifications for the target user in addition to becoming friends. - """ - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) - if screen_name is not None: - apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) - try: - if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create/%s.json" % `id`, "?folow=%s" % follow)) - else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/create.json", apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") - raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - """destroyFriendship(id = None, user_id = None, screen_name = None) - - Allows the authenticating users to unfollow the user specified in the ID parameter. - Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to unfollow. - user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - """ - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "?screen_name=%s" % screen_name - try: - if id is not None: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy/%s.json" % `id`, "lol=1")) # Random string appended for POST reasons, quick hack ;P - else: - return simplejson.load(self.opener.open("http://twitter.com/friendships/destroy.json", apiURL)) - except HTTPError, e: - raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - """checkIfFriendshipExists(user_a, user_b) - - Tests for the existence of friendship between two users. - Will return true if user_a follows user_b; otherwise, it'll return false. - - Parameters: - user_a - Required. The ID or screen_name of the subject user. - user_b - Required. The ID or screen_name of the user to test for following. - """ - if self.authenticated is True: - try: - friendshipURL = "http://twitter.com/friendships/exists.json?%s" % urllib.urlencode({"user_a": user_a, "user_b": user_b}) - return simplejson.load(self.opener.open(friendshipURL)) - except HTTPError, e: - raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None): - """showFriendship(source_id, source_screen_name, target_id, target_screen_name) - - Returns detailed information about the relationship between two users. - - Parameters: - ** Note: One of the following is required if the request is unauthenticated - source_id - The user_id of the subject user. - source_screen_name - The screen_name of the subject user. - - ** Note: One of the following is required at all times - target_id - The user_id of the target user. - target_screen_name - The screen_name of the target user. - """ - apiURL = "http://twitter.com/friendships/show.json?lol=1" # Another quick hack, look away if you want. :D - if source_id is not None: - apiURL += "&source_id=%s" % `source_id` - if source_screen_name is not None: - apiURL += "&source_screen_name=%s" % source_screen_name - if target_id is not None: - apiURL += "&target_id=%s" % `target_id` - if target_screen_name is not None: - apiURL += "&target_screen_name=%s" % target_screen_name - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - # Catch this for now - if e.code == 403: - raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") - raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) - - def updateDeliveryDevice(self, device_name = "none"): - """updateDeliveryDevice(device_name = "none") - - Sets which device Twitter delivers updates to for the authenticating user. - Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) - - Parameters: - device - Required. Must be one of: sms, im, none. - """ - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": self.unicode2utf8(device_name)})) - except HTTPError, e: - raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - """updateProfileColors(**kwargs) - - Sets one or more hex values that control the color scheme of the authenticating user's profile page on twitter.com. - - Parameters: - ** Note: One or more of the following parameters must be present. Each parameter's value must - be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). - - profile_background_color - Optional. - profile_text_color - Optional. - profile_link_color - Optional. - profile_sidebar_fill_color - Optional. - profile_sidebar_border_color - Optional. - """ - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - """updateProfile(name = None, email = None, url = None, location = None, description = None) - - Sets values that users are able to set under the "Account" tab of their settings page. - Only the parameters specified will be updated. - - Parameters: - One or more of the following parameters must be present. Each parameter's value - should be a string. See the individual parameter descriptions below for further constraints. - - name - Optional. Maximum of 20 characters. - email - Optional. Maximum of 40 characters. Must be a valid email address. - url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. - location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. - description - Optional. Maximum of 160 characters. - """ - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) - else: - updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) - else: - updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError, e: - raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - """getFavorites(page = "1") - - Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. - - Parameters: - page - Optional. Specifies the page of favorites to retrieve. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=%s" % `page`)) - except HTTPError, e: - raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - """createFavorite(id) - - Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. - - Parameters: - id - Required. The ID of the status to favorite. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/%s.json" % `id`, "")) - except HTTPError, e: - raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - """destroyFavorite(id) - - Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. - - Parameters: - id - Required. The ID of the status to un-favorite. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/%s.json" % `id`, "")) - except HTTPError, e: - raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - """notificationFollow(id = None, user_id = None, screen_name = None) - - Enables device notifications for updates from the specified user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=%s" % screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - """notificationLeave(id = None, user_id = None, screen_name = None) - - Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - """ - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/%s.json" % id - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=%s" % screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = "1") - - Returns an array of numeric IDs for every user the specified user is following. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - """ - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/%s.json?page=%s" %(id, `page`) - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = "1") - - Returns an array of numeric IDs for every user following the specified user. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - """ - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/%s.json?page=%s" %(`id`, `page`) - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=%s&page=%s" %(`user_id`, `page`) - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=%s&page=%s" %(screen_name, `page`) - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): - """createBlock(id) - - Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. - Returns the blocked user in the requested format when successful. - - Parameters: - id - The ID or screen name of a user to block. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/%s.json" % `id`, "")) - except HTTPError, e: - raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - """destroyBlock(id) - - Un-blocks the user specified in the ID parameter for the authenticating user. - Returns the un-blocked user in the requested format when successful. - - Parameters: - id - Required. The ID or screen_name of the user to un-block - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/%s.json" % `id`, "")) - except HTTPError, e: - raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - """checkIfBlockExists(id = None, user_id = None, screen_name = None) - - Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and - error with an HTTP 404 response code otherwise. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen_name of the potentially blocked user. - user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - """ - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/%s.json" % `id` - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=%s" % screen_name - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): - """getBlocking(page = "1") - - Returns an array of user objects that the authenticating user is blocking. - - Parameters: - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=%s" % `page`)) - except HTTPError, e: - raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - """getBlockedIDs() - - Returns an array of numeric user ids the authenticating user is blocking. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError, e: - raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - """searchTwitter(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. - lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. - locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. - rpp - Optional. The number of tweets to return per page, up to a max of 100. - page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) - since_id - Optional. Returns tweets with status ids greater than the given id. - geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. - show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. - - Usage Notes: - Queries are limited 140 URL encoded characters. - Some users may be absent from search results. - The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. - This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. - - Applications must have a meaningful and unique User Agent when using this method. - An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than - applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - """getCurrentTrends(excludeHashTags = False) - - Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used - on Twitter Search results page for that topic. - - Parameters: - excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - """getDailyTrends(date = None, exclude = False) - - Returns the top 20 trending topics for each hour in a given day. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - """getWeeklyTrends(date = None, exclude = False) - - Returns the top 30 trending topics for each day in a given week. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): - """getSavedSearches() - - Returns the authenticated user's saved search queries. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError, e: - raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - """showSavedSearch(id) - - Retrieve the data for a saved search owned by the authenticating user specified by the given id. - - Parameters: - id - Required. The id of the saved search to be retrieved. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/%s.json" % `id`)) - except HTTPError, e: - raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - """createSavedSearch(query) - - Creates a saved search for the authenticated user. - - Parameters: - query - Required. The query of the search the user would like to save. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=%s" % query, "")) - except HTTPError, e: - raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - """destroySavedSearch(id) - - Destroys a saved search for the authenticated user. - The search specified by id must be owned by the authenticating user. - - Parameters: - id - Required. The id of the saved search to be deleted. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/%s.json" % `id`, "")) - except HTTPError, e: - raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - """updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - """ - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - """updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def unicode2utf8(self, text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text diff --git a/twythonoauth/views.py b/twythonoauth/views.py deleted file mode 100644 index d3ac084..0000000 --- a/twythonoauth/views.py +++ /dev/null @@ -1,6 +0,0 @@ -import twython - -def auth(request): - - - From d1c579af31c1ed98fe68421be68d2391ede0f31c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 02:14:44 -0500 Subject: [PATCH 141/687] Rearranging Twython to be a proper package structure --- twython.py => core/twython.py | 0 twython3k.py => core/twython3k.py | 0 oauth.py => oauth/oauth.py | 0 twython_oauth.py => oauth/twython_oauth.py | 0 .../twython_streaming.py | 0 twython.egg-info/PKG-INFO | 64 ------------------- twython.egg-info/SOURCES.txt | 7 -- twython.egg-info/dependency_links.txt | 1 - twython.egg-info/requires.txt | 2 - twython.egg-info/top_level.txt | 1 - 10 files changed, 75 deletions(-) rename twython.py => core/twython.py (100%) rename twython3k.py => core/twython3k.py (100%) rename oauth.py => oauth/oauth.py (100%) rename twython_oauth.py => oauth/twython_oauth.py (100%) rename twython_streaming.py => streaming/twython_streaming.py (100%) delete mode 100644 twython.egg-info/PKG-INFO delete mode 100644 twython.egg-info/SOURCES.txt delete mode 100644 twython.egg-info/dependency_links.txt delete mode 100644 twython.egg-info/requires.txt delete mode 100644 twython.egg-info/top_level.txt diff --git a/twython.py b/core/twython.py similarity index 100% rename from twython.py rename to core/twython.py diff --git a/twython3k.py b/core/twython3k.py similarity index 100% rename from twython3k.py rename to core/twython3k.py diff --git a/oauth.py b/oauth/oauth.py similarity index 100% rename from oauth.py rename to oauth/oauth.py diff --git a/twython_oauth.py b/oauth/twython_oauth.py similarity index 100% rename from twython_oauth.py rename to oauth/twython_oauth.py diff --git a/twython_streaming.py b/streaming/twython_streaming.py similarity index 100% rename from twython_streaming.py rename to streaming/twython_streaming.py diff --git a/twython.egg-info/PKG-INFO b/twython.egg-info/PKG-INFO deleted file mode 100644 index 8359b77..0000000 --- a/twython.egg-info/PKG-INFO +++ /dev/null @@ -1,64 +0,0 @@ -Metadata-Version: 1.0 -Name: twython -Version: 0.9 -Summary: An easy (and up to date) way to access Twitter data with Python. -Home-page: http://github.com/ryanmcgrath/twython/tree/master -Author: Ryan McGrath -Author-email: ryan@venodesigns.net -License: MIT License -Description: Twython - Easy Twitter utilities in Python - ========================================================================================= - I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain - things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at - a library that offers more coverage. - - This is my first library I've ever written in Python, so there could be some stuff in here that'll - make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, - and I'm open to anything that'll improve the library as a whole. - - OAuth and Streaming API support is in the works, but every other part of the Twitter API should be covered. Twython - handles both Basic (HTTP) Authentication and OAuth (Older versions (pre 0.9) of Twython need Basic Auth specified - - to override this, specify 'authtype="Basic"' in your twython.setup() call). - - Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or - Twitter's API Wiki (Twython calls mirror most of the methods listed there). - - Requirements - ----------------------------------------------------------------------------------------------------- - Twython requires (much like Python-Twitter, because they had the right idea :D) a library called - "simplejson". You can grab it at the following link: - - > http://pypi.python.org/pypi/simplejson - - - Example Use - ----------------------------------------------------------------------------------------------------- - > import twython - > - > twitter = twython.setup(username="example", password="example") - > twitter.updateStatus("See how easy this was?") - - - Twython 3k - ----------------------------------------------------------------------------------------------------- - There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed - to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, - be aware of this. - - - Questions, Comments, etc? - ----------------------------------------------------------------------------------------------------- - My hope is that Twython is so simple that you'd never *have* to ask any questions, but if - you feel the need to contact me for this (or other) reasons, you can hit me up - at ryan@venodesigns.net. - - Twython is released under an MIT License - see the LICENSE file for more information. - -Keywords: twitter search api tweet twython -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Topic :: Communications :: Chat -Classifier: Topic :: Internet diff --git a/twython.egg-info/SOURCES.txt b/twython.egg-info/SOURCES.txt deleted file mode 100644 index 26a7786..0000000 --- a/twython.egg-info/SOURCES.txt +++ /dev/null @@ -1,7 +0,0 @@ -setup.py -twython.py -twython.egg-info/PKG-INFO -twython.egg-info/SOURCES.txt -twython.egg-info/dependency_links.txt -twython.egg-info/requires.txt -twython.egg-info/top_level.txt \ No newline at end of file diff --git a/twython.egg-info/dependency_links.txt b/twython.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/twython.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/twython.egg-info/requires.txt b/twython.egg-info/requires.txt deleted file mode 100644 index 58cf212..0000000 --- a/twython.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools -simplejson \ No newline at end of file diff --git a/twython.egg-info/top_level.txt b/twython.egg-info/top_level.txt deleted file mode 100644 index 292a670..0000000 --- a/twython.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -twython From fc5aaebda3b4fca970dbcdad4acac38027156fe9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 02:34:56 -0500 Subject: [PATCH 142/687] Removing more useless build cruft --- build/lib/twython.py | 1792 ---------------------------------- build/lib/twython/twython.py | 636 ------------ build/lib/twython2k.py | 654 ------------- 3 files changed, 3082 deletions(-) delete mode 100644 build/lib/twython.py delete mode 100644 build/lib/twython/twython.py delete mode 100644 build/lib/twython2k.py diff --git a/build/lib/twython.py b/build/lib/twython.py deleted file mode 100644 index ffe31e5..0000000 --- a/build/lib/twython.py +++ /dev/null @@ -1,1792 +0,0 @@ -#!/usr/bin/python - -""" - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.9" - -"""Twython - Easy Twitter utilities in Python""" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -class TwythonError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class AuthError(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, username = None, password = None, headers = None, version = 1): - """setup(authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - username - Your Twitter username, if you want Basic (HTTP) Authentication. - password - Password for your twitter account, if you want Basic (HTTP) Authentication. - headers - User agent header. - version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. - - ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. - """ - self.authenticated = False - self.username = username - self.apiVersion = version - # Check and set up authentication - if self.username is not None and password is not None: - # Assume Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) - self.authenticated = True - except HTTPError, e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) - else: - pass - - # URL Shortening function huzzah - def shortenURL(self, 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 url shorterning service other that is.gd. - """ - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() - except HTTPError, e: - raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP", version = None): - """getRateLimitStatus() - - Returns the remaining number of API requests available to the requesting user before the - API limit is reached for the current hour. Calls to rate_limit_status do not count against - the rate limit. If authentication credentials are provided, the rate limit status for the - authenticating user is returned. Otherwise, the rate limit status for the requesting - IP address is returned. - - Params: - rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) - 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 = version or self.apiVersion - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - raise TwythonError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self, version = None): - """getPublicTimeline() - - Returns the 20 most recent statuses from non-protected users who have set a custom user icon. - The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. - - Params: - 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 = version or self.apiVersion - try: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) - except HTTPError, e: - raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getHomeTimeline(self, version = None, **kwargs): - """getHomeTimeline(**kwargs) - - Returns the 20 most recent statuses, including retweets, posted by the authenticating user - and that user's friends. This is the equivalent of /timeline/home on the Web. - - Usage note: This home_timeline is identical to statuses/friends_timeline, except it also - contains retweets, which statuses/friends_timeline does not (for backwards compatibility - reasons). In a future version of the API, statuses/friends_timeline will go away and - be replaced by home_timeline. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(homeTimelineURL)) - except HTTPError, e: - raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) - else: - raise AuthError("getHomeTimeline() requires you to be authenticated.") - - def getFriendsTimeline(self, version = None, **kwargs): - """getFriendsTimeline(**kwargs) - - Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. - This is the equivalent of /timeline/home on the Web. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise AuthError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, version = None, **kwargs): - """getUserTimeline(id = None, **kwargs) - - Returns the 20 most recent statuses posted from the authenticating user. It's also - possible to request another user's timeline via the id parameter. This is the - equivalent of the Web / page for your own user, or the profile page for a third party. - - Parameters: - id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. - user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. - screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, `id`), kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) - else: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, version = None, **kwargs): - """getUserMentions(**kwargs) - - Returns the 20 most recent mentions (status containing @username) for the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getUserMentions() requires you to be authenticated.") - - def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): - """reportSpam(self, id), user_id, screen_name): - - Report a user account to Twitter as a spam account. *One* of the following parameters is required, and - this requires that you be authenticated with a user account. - - Parameters: - id - Optional. The ID or screen_name of the user you want to report as a spammer. - user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. - if id is not None or user_id is not None or screen_name is not None: - try: - apiExtension = "" - if id is not None: - apiExtension = "id=%s" % id - if user_id is not None: - apiExtension = "user_id=%s" % `user_id` - if screen_name is not None: - apiExtension = "screen_name=%s" % screen_name - return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) - except HTTPError, e: - raise TwythonError("reportSpam() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") - else: - raise AuthError("reportSpam() requires you to be authenticated.") - - def reTweet(self, id, version = None): - """reTweet(id) - - Retweets a tweet. Requires the id parameter of the tweet you are retweeting. - - Parameters: - id - Required. The numerical ID of the tweet you are retweeting. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, `id`), "POST")) - except HTTPError, e: - raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - - def getRetweets(self, id, count = None, version = None): - """ getRetweets(self, id, count): - - Returns up to 100 of the first retweets of a given tweet. - - Parameters: - id - Required. The numerical ID of the tweet you want the retweets of. - count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, `id`) - if count is not None: - apiURL += "?count=%s" % `count` - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getRetweets failed with a %s eroror code." % `e.code`, e.code) - else: - raise AuthError("getRetweets() requires you to be authenticated.") - - def retweetedOfMe(self, version = None, **kwargs): - """retweetedOfMe(**kwargs) - - Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedOfMe() requires you to be authenticated.") - - def retweetedByMe(self, version = None, **kwargs): - """retweetedByMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedByMe() requires you to be authenticated.") - - def retweetedToMe(self, version = None, **kwargs): - """retweetedToMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user's friends. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedToMe() requires you to be authenticated.") - - def searchUsers(self, q, per_page = 20, page = 1, version = None): - """ searchUsers(q, per_page = None, page = None): - - Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) - - Parameters: - q (string) - Required. The query you wanna search against; self explanatory. ;) - per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) - page (number) - Optional, defaults to 1. The page of users you want to pull 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) - except HTTPError, e: - raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("searchUsers(), oddly, requires you to be authenticated.") - - def showUser(self, id = None, user_id = None, screen_name = None, version = None): - """showUser(id = None, user_id = None, screen_name = None) - - Returns extended information of a given user. The author's most recent status will be returned inline. - - Parameters: - ** Note: One of the following must always be specified. - id - The ID or screen name of a user. - user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - 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. - - Usage Notes: - Requests for protected users without credentials from - 1) the user requested or - 2) a user that is following the protected user will omit the nested status element. - - ...will result in only publicly available data being returned. - """ - version = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) - if apiURL != "": - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") - - Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. - (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access - older friends. With no user specified, the request defaults to the authenticated users friends. - - It's also possible to request another user's friends list via the id, screen_name or user_id parameter. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, or screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of friends. - user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) - try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) - else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) - except HTTPError, e: - raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFriendsStatus() requires you to be authenticated.") - - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns the authenticating user's followers, each with current status inline. - They are ordered by the order in which they joined Twitter, 100 at a time. - (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) - - Use the page option to access earlier followers. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of followers. - user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) - try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) - else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) - except HTTPError, e: - raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFollowersStatus() requires you to be authenticated.") - - def showStatus(self, id, version = None): - """showStatus(id) - - Returns a single status, specified by the id parameter below. - The status's author will be returned inline. - - Parameters: - id - Required. The numerical ID of the status to retrieve. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): - """updateStatus(status, in_reply_to_status_id = None) - - Updates the authenticating user's status. Requires the status parameter specified below. - A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. - - Parameters: - status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. - in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - latitude (string) - Optional. The location's latitude that this tweet refers to. - longitude (string) - Optional. The location's longitude that this tweet refers 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. - - ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references - is mentioned within the status text. Therefore, you must include @username, where username is - the author of the referenced tweet, within the update. - - ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. - This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. - """ - version = version or self.apiVersion - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({ - "status": self.unicode2utf8(status), - "in_reply_to_status_id": in_reply_to_status_id, - "lat": latitude, - "long": longitude - }))) - except HTTPError, e: - raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - - def destroyStatus(self, id, version = None): - """destroyStatus(id) - - Destroys the status specified by the required ID parameter. - The authenticating user must be the author of the specified status. - - Parameters: - id - Required. The ID of the status to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, `id`), "DELETE")) - except HTTPError, e: - raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyStatus() requires you to be authenticated.") - - def endSession(self, version = None): - """endSession() - - Ends the session of the authenticating user, returning a null cookie. - Use this method to sign users out of client-facing applications (widgets, etc). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") - self.authenticated = False - except HTTPError, e: - raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent to the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, `page`) - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getSentMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, `page`) - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text, version = None): - """sendDirectMessage(user, text) - - Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. - Returns the sent message in the requested format when successful. - - Parameters: - user - Required. The ID or screen name of the recipient user. - text - Required. The text of your direct message. Be sure to keep it under 140 characters. - 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 = version or self.apiVersion - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("Your message must not be longer than 140 characters") - else: - raise AuthError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id, version = None): - """destroyDirectMessage(id) - - Destroys the direct message specified in the required ID parameter. - The authenticating user must be the recipient of the specified direct message. - - Parameters: - id - Required. The ID of the direct message to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") - except HTTPError, e: - raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): - """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") - - Allows the authenticating users to follow the user specified in the ID parameter. - Returns the befriended user in the requested format when successful. Returns a - string describing the failure condition when unsuccessful. If you are already - friends with the user an HTTP 403 will be returned. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to befriend. - user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. - follow - Optional. Enable notifications for the target user in addition to becoming friends. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) - if screen_name is not None: - apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") - raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): - """destroyFriendship(id = None, user_id = None, screen_name = None) - - Allows the authenticating users to unfollow the user specified in the ID parameter. - Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to unfollow. - user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "?user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "?screen_name=%s" % screen_name - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, `id`), "lol=1")) # Random string hack for POST reasons ;P - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) - except HTTPError, e: - raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b, version = None): - """checkIfFriendshipExists(user_a, user_b) - - Tests for the existence of friendship between two users. - Will return true if user_a follows user_b; otherwise, it'll return false. - - Parameters: - user_a - Required. The ID or screen_name of the subject user. - user_b - Required. The ID or screen_name of the user to test for following. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.urlencode({"user_a": user_a, "user_b": user_b})) - return simplejson.load(self.opener.open(friendshipURL)) - except HTTPError, e: - raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): - """showFriendship(source_id, source_screen_name, target_id, target_screen_name) - - Returns detailed information about the relationship between two users. - - Parameters: - ** Note: One of the following is required if the request is unauthenticated - source_id - The user_id of the subject user. - source_screen_name - The screen_name of the subject user. - - ** Note: One of the following is required at all times - target_id - The user_id of the target user. - target_screen_name - The screen_name of the target user. - - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D - if source_id is not None: - apiURL += "&source_id=%s" % `source_id` - if source_screen_name is not None: - apiURL += "&source_screen_name=%s" % source_screen_name - if target_id is not None: - apiURL += "&target_id=%s" % `target_id` - if target_screen_name is not None: - apiURL += "&target_screen_name=%s" % target_screen_name - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - # Catch this for now - if e.code == 403: - raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") - raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) - - def updateDeliveryDevice(self, device_name = "none", version = None): - """updateDeliveryDevice(device_name = "none") - - Sets which device Twitter delivers updates to for the authenticating user. - Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) - - Parameters: - device - Required. Must be one of: sms, im, none. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.urlencode({"device": self.unicode2utf8(device_name)})) - except HTTPError, e: - raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, version = None, **kwargs): - """updateProfileColors(**kwargs) - - Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. - - Parameters: - ** Note: One or more of the following parameters must be present. Each parameter's value must - be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). - - profile_background_color - Optional. - profile_text_color - Optional. - profile_link_color - Optional. - profile_sidebar_fill_color - Optional. - profile_sidebar_border_color - Optional. - - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) - except HTTPError, e: - raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): - """updateProfile(name = None, email = None, url = None, location = None, description = None) - - Sets values that users are able to set under the "Account" tab of their settings page. - Only the parameters specified will be updated. - - Parameters: - One or more of the following parameters must be present. Each parameter's value - should be a string. See the individual parameter descriptions below for further constraints. - - name - Optional. Maximum of 20 characters. - email - Optional. Maximum of 40 characters. Must be a valid email address. - url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. - location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. - description - Optional. Maximum of 160 characters. - - 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 = version or self.apiVersion - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) - else: - updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) - else: - updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) - except HTTPError, e: - raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1", version = None): - """getFavorites(page = "1") - - Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. - - Parameters: - page - Optional. Specifies the page of favorites to retrieve. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, `page`))) - except HTTPError, e: - raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id, version = None): - """createFavorite(id) - - Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. - - Parameters: - id - Required. The ID of the status to favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id, version = None): - """destroyFavorite(id) - - Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. - - Parameters: - id - Required. The ID of the status to un-favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): - """notificationFollow(id = None, user_id = None, screen_name = None) - - Enables device notifications for updates from the specified user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): - """notificationLeave(id = None, user_id = None, screen_name = None) - - Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user the specified user is following. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user following the specified user. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, `id`, breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id, version = None): - """createBlock(id) - - Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. - Returns the blocked user in the requested format when successful. - - Parameters: - id - The ID or screen name of a user to block. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id, version = None): - """destroyBlock(id) - - Un-blocks the user specified in the ID parameter for the authenticating user. - Returns the un-blocked user in the requested format when successful. - - Parameters: - id - Required. The ID or screen_name of the user to un-block - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): - """checkIfBlockExists(id = None, user_id = None, screen_name = None) - - Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and - error with an HTTP 404 response code otherwise. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen_name of the potentially blocked user. - user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, `id`) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1", version = None): - """getBlocking(page = "1") - - Returns an array of user objects that the authenticating user is blocking. - - Parameters: - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, `page`))) - except HTTPError, e: - raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self, version = None): - """getBlockedIDs() - - Returns an array of numeric user ids the authenticating user is blocking. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) - except HTTPError, e: - raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - """searchTwitter(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. - lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. - locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. - rpp - Optional. The number of tweets to return per page, up to a max of 100. - page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) - since_id - Optional. Returns tweets with status ids greater than the given id. - geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. - show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. - - Usage Notes: - Queries are limited 140 URL encoded characters. - Some users may be absent from search results. - The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. - This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. - - Applications must have a meaningful and unique User Agent when using this method. - An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than - applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - """getCurrentTrends(excludeHashTags = False) - - Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used - on Twitter Search results page for that topic. - - Parameters: - excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - """getDailyTrends(date = None, exclude = False) - - Returns the top 20 trending topics for each hour in a given day. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - """getWeeklyTrends(date = None, exclude = False) - - Returns the top 30 trending topics for each day in a given week. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - """ - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self, version = None): - """getSavedSearches() - - Returns the authenticated user's saved search queries. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) - except HTTPError, e: - raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id, version = None): - """showSavedSearch(id) - - Retrieve the data for a saved search owned by the authenticating user specified by the given id. - - Parameters: - id - Required. The id of the saved search to be retrieved. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, `id`))) - except HTTPError, e: - raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query, version = None): - """createSavedSearch(query) - - Creates a saved search for the authenticated user. - - Parameters: - query - Required. The query of the search the user would like to save. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) - except HTTPError, e: - raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id, version = None): - """ destroySavedSearch(id) - - Destroys a saved search for the authenticated user. - The search specified by id must be owned by the authenticating user. - - Parameters: - id - Required. The id of the saved search to be deleted. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroySavedSearch() requires you to be authenticated.") - - def createList(self, name, mode = "public", description = "", version = None): - """ createList(self, name, mode, description, version) - - Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - name - Required. The name for the new list. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), - urllib.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError, e: - raise TwythonError("createList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("createList() requires you to be authenticated.") - - def updateList(self, list_id, name, mode = "public", description = "", version = None): - """ updateList(self, list_id, name, mode, description, version) - - Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look - at this... - - Parameters: - list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). - name - Required. The name of the list, possibly for renaming or such. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), - urllib.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError, e: - raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("updateList() requires you to be authenticated.") - - def showLists(self, version = None): - """ showLists(self, version) - - Show all the lists for the currently authenticated user (i.e, they own these lists). - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) - except HTTPError, e: - raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("showLists() requires you to be authenticated.") - - def getListMemberships(self, version = None): - """ getListMemberships(self, version) - - Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) - except HTTPError, e: - raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("getLists() requires you to be authenticated.") - - def deleteList(self, list_id, version = None): - """ deleteList(self, list_id, version) - - Deletes a list for the authenticating user. - - Parameters: - list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("deleteList() requires you to be authenticated.") - - def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): - """ getListTimeline(self, list_id, cursor, version, **kwargs) - - Retrieves a timeline representing everyone in the list specified. - - Parameters: - list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. - Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - try: - baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) - return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) - except HTTPError, e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) - - def getSpecificList(self, list_id, version = None): - """ getSpecificList(self, list_id, version) - - Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). - - Parameters: - list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - except HTTPError, e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) - - def addListMember(self, list_id, version = None): - """ addListMember(self, list_id, id, version) - - Adds a new Member (the passed in id) to the specified list. - - Parameters: - list_id - Required. The slug of the list to add the new member to. - id - Required. The ID of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % `id`)) - except HTTPError, e: - raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("addListMember requires you to be authenticated.") - - def getListMembers(self, list_id, version = None): - """ getListMembers(self, list_id, version = None) - - Show all members of a specified list. This method requires authentication if the list is private/protected. - - Parameters: - list_id - Required. The slug of the list to retrieve members for. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - except HTTPError, e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - - def removeListMember(self, list_id, id, version = None): - """ removeListMember(self, list_id, id, version) - - Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. - - Parameters: - list_id - Required. The slug of the list to remove the specified user from. - id - Required. The ID of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("removeListMember() requires you to be authenticated.") - - def isListMember(self, list_id, id, version = None): - """ isListMember(self, list_id, id, version) - - 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) - else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def subscribeToList(self, list_id, version): - """ subscribeToList(self, list_id, version) - - Subscribe the authenticated user to the list provided (must be public). - - Parameters: - list_id - Required. The list to subscribe 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) - except HTTPError, e: - raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("subscribeToList() requires you to be authenticated.") - - def unsubscribeFromList(self, list_id, version): - """ unsubscribeFromList(self, list_id, version) - - Unsubscribe the authenticated user from the list in question (must be public). - - Parameters: - list_id - Required. The list to unsubscribe from. - 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("unsubscribeFromList() requires you to be authenticated.") - - def isListSubscriber(self, list_id, id, version = None): - """ isListSubscriber(self, list_id, id, version) - - Check if a specified user (id) is a subscriber 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) - else: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def availableTrends(self, latitude = None, longitude = None, version = None): - """ availableTrends(latitude, longitude, version): - - Gets all available trends, optionally filtering by geolocation based stuff. - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - latitude (string) - Optional. A latitude to sort by. - longitude (string) - Optional. A longitude to sort by. - 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 = version or self.apiVersion - try: - if latitude is not None and longitude is not None: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/available.json" % version)) - except HTTPError, e: - raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) - - def trendsByLocation(self, woeid, version = None): - """ trendsByLocation(woeid, version): - - Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - woeid (string) - Required. WoeID of the area you're searching in. - 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 = version or self.apiVersion - try: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) - except HTTPError, e: - raise TwythonError("trendsByLocation() 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. - def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """ updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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 = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename, version = None): - """ updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def unicode2utf8(self, text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text diff --git a/build/lib/twython/twython.py b/build/lib/twython/twython.py deleted file mode 100644 index 2d37f50..0000000 --- a/build/lib/twython/twython.py +++ /dev/null @@ -1,636 +0,0 @@ -#!/usr/bin/python - -""" - NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. - - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.8.0.1" - -"""Twython - Easy Twitter utilities in Python""" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TangoError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, oauth_keys = None, headers = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - self.oauth_keys = oauth_keys - if self.username is not None and self.password is not None: - if self.authtype == "OAuth": - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - # Do OAuth type stuff here - how should this be handled? Seems like a framework question... - elif self.authtype == "Basic": - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - - # OAuth functions; shortcuts for verifying the credentials. - def fetch_response_oauth(self, oauth_request): - pass - - def get_unauthorized_request_token(self): - pass - - def get_authorization_url(self, token): - pass - - def exchange_tokens(self, request_token): - pass - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): - try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getUserMentions() requires you to be authenticated.") - - def showStatus(self, id): - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("Your message must not be longer than 140 characters") - else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) - except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.urlencode({"url": url}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.urlencode({"location": location}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.urlencode({"description": description}) - else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' diff --git a/build/lib/twython2k.py b/build/lib/twython2k.py deleted file mode 100644 index 051884c..0000000 --- a/build/lib/twython2k.py +++ /dev/null @@ -1,654 +0,0 @@ -#!/usr/bin/python - -""" - NOTE: Tango is being renamed to Twython; all basic strings have been changed below, but there's still work ongoing in this department. - - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "0.5" - -"""Twython - Easy Twitter utilities in Python""" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -try: - import oauth -except ImportError: - pass - -class TangoError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TangoError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, authtype = "OAuth", username = None, password = None, consumer_secret = None, consumer_key = None, headers = None): - self.authtype = authtype - self.authenticated = False - self.username = username - self.password = password - # OAuth specific variables below - self.request_token_url = 'https://twitter.com/oauth/request_token' - self.access_token_url = 'https://twitter.com/oauth/access_token' - self.authorization_url = 'http://twitter.com/oauth/authorize' - self.signin_url = 'http://twitter.com/oauth/authenticate' - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None - # Check and set up authentication - if self.username is not None and self.password is not None: - if self.authtype == "Basic": - # Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://twitter.com", self.username, self.password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - self.opener = urllib2.build_opener(self.handler) - if headers is not None: - self.opener.addheaders = [('User-agent', headers)] - try: - simplejson.load(self.opener.open("http://twitter.com/account/verify_credentials.json")) - self.authenticated = True - except HTTPError, e: - raise TangoError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`, e.code) - else: - self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() - # Awesome OAuth authentication ritual - if consumer_secret is not None and consumer_key is not None: - #req = oauth.OAuthRequest.from_consumer_and_token - #req.sign_request(self.signature_method, self.consumer_key, self.token) - #self.opener = urllib2.build_opener() - pass - else: - raise TwythonError("Woah there, buddy. We've defaulted to OAuth authentication, but you didn't provide API keys. Try again.") - - def getRequestToken(self): - response = self.oauth_request(self.request_token_url) - token = self.parseOAuthResponse(response) - self.request_token = oauth.OAuthConsumer(token['oauth_token'],token['oauth_token_secret']) - return self.request_token - - def parseOAuthResponse(self, response_string): - # Partial credit goes to Harper Reed for this gem. - lol = {} - for param in response_string.split("&"): - pair = param.split("=") - if(len(pair) != 2): - break - lol[pair[0]] = pair[1] - return lol - - # URL Shortening function huzzah - def shortenURL(self, url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): - try: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: url_to_shorten})).read() - except HTTPError, e: - raise TangoError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def getRateLimitStatus(self, rate_for = "requestingIP"): - try: - if rate_for == "requestingIP": - return simplejson.load(urllib2.urlopen("http://twitter.com/account/rate_limit_status.json")) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/account/rate_limit_status.json")) - else: - raise TangoError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TangoError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self): - try: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/public_timeline.json")) - except HTTPError, e: - raise TangoError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getFriendsTimeline(self, **kwargs): - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://twitter.com/statuses/friends_timeline.json", kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TangoError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise TangoError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, **kwargs): - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + id + ".json", kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline/" + self.username + ".json", kwargs) - else: - userTimelineURL = self.constructApiURL("http://twitter.com/statuses/user_timeline.json", kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(urllib2.urlopen(userTimelineURL)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, **kwargs): - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://twitter.com/statuses/mentions.json", kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TangoError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getUserMentions() requires you to be authenticated.") - - def showStatus(self, id): - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://twitter.com/statuses/show/%s.json" % id)) - else: - return simplejson.load(urllib2.urlopen("http://twitter.com/statuses/show/%s.json" % id)) - except HTTPError, e: - raise TangoError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None): - if self.authenticated is True: - if len(list(status)) > 140: - raise TangoError("This status message is over 140 characters. Trim it down!") - try: - return simplejson.load(self.opener.open("http://twitter.com/statuses/update.json?", urllib.urlencode({"status": status, "in_reply_to_status_id": in_reply_to_status_id}))) - except HTTPError, e: - raise TangoError("updateStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateStatus() requires you to be authenticated.") - - def destroyStatus(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/status/destroy/%s.json", "POST" % id)) - except HTTPError, e: - raise TangoError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyStatus() requires you to be authenticated.") - - def endSession(self): - if self.authenticated is True: - try: - self.opener.open("http://twitter.com/account/end_session.json", "") - self.authenticated = False - except HTTPError, e: - raise TangoError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1"): - if self.authenticated is True: - apiURL = "http://twitter.com/direct_messages/sent.json?page=" + page - if since_id is not None: - apiURL += "&since_id=" + since_id - if max_id is not None: - apiURL += "&max_id=" + max_id - if count is not None: - apiURL += "&count=" + count - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text): - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://twitter.com/direct_messages/new.json", urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TangoError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("Your message must not be longer than 140 characters") - else: - raise TangoError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/direct_messages/destroy/%s.json" % id, "") - except HTTPError, e: - raise TangoError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false"): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/create/" + id + ".json" + "?follow=" + follow - if user_id is not None: - apiURL = "http://twitter.com/friendships/create.json?user_id=" + user_id + "&follow=" + follow - if screen_name is not None: - apiURL = "http://twitter.com/friendships/create.json?screen_name=" + screen_name + "&follow=" + follow - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise TangoError("You've hit the update limit for this method. Try again in 24 hours.") - raise TangoError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friendships/destroy/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/friendships/destroy.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/friendships/destroy.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TangoError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/friendships/exists.json", urllib.urlencode({"user_a": user_a, "user_b": user_b}))) - except HTTPError, e: - raise TangoError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def updateDeliveryDevice(self, device_name = "none"): - if self.authenticated is True: - try: - return self.opener.open("http://twitter.com/account/update_delivery_device.json?", urllib.urlencode({"device": device_name})) - except HTTPError, e: - raise TangoError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, **kwargs): - if self.authenticated is True: - try: - return self.opener.open(self.constructApiURL("http://twitter.com/account/update_profile_colors.json?", kwargs)) - except HTTPError, e: - raise TangoError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None): - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": url}) - else: - updateProfileQueryString += urllib.urlencode({"url": url}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": location}) - else: - updateProfileQueryString += urllib.urlencode({"location": location}) - useAmpersands = True - else: - raise TangoError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": description}) - else: - updateProfileQueryString += urllib.urlencode({"description": description}) - else: - raise TangoError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://twitter.com/account/update_profile.json?", updateProfileQueryString) - except HTTPError, e: - raise TangoError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites.json?page=" + page)) - except HTTPError, e: - raise TangoError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/favorites/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/follow/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/follow/follow.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None): - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/notifications/leave/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/notifications/leave/leave.json?screen_name=" + screen_name - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TangoError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/friends/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/friends/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/friends/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = "1"): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/followers/ids/" + id + ".json" + "?page=" + page - if user_id is not None: - apiURL = "http://twitter.com/followers/ids.json?user_id=" + user_id + "&page=" + page - if screen_name is not None: - apiURL = "http://twitter.com/followers/ids.json?screen_name=" + screen_name + "&page=" + page - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/create/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None): - apiURL = "" - if id is not None: - apiURL = "http://twitter.com/blocks/exists/" + id + ".json" - if user_id is not None: - apiURL = "http://twitter.com/blocks/exists.json?user_id=" + user_id - if screen_name is not None: - apiURL = "http://twitter.com/blocks/exists.json?screen_name=" + screen_name - try: - return simplejson.load(urllib2.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1"): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking.json?page=" + page)) - except HTTPError, e: - raise TangoError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/blocks/blocking/ids.json")) - except HTTPError, e: - raise TangoError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": search_query}) - try: - return simplejson.load(urllib2.urlopen(searchURL)) - except HTTPError, e: - raise TangoError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def getCurrentTrends(self, excludeHashTags = False): - apiURL = "http://search.twitter.com/trends/current.json" - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - apiURL = "http://search.twitter.com/trends/daily.json" - questionMarkUsed = False - if date is not None: - apiURL += "?date=" + date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(urllib.urlopen(apiURL)) - except HTTPError, e: - raise TangoError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches.json")) - except HTTPError, e: - raise TangoError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/show/" + id + ".json")) - except HTTPError, e: - raise TangoError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/create.json?query=" + query, "")) - except HTTPError, e: - raise TangoError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id): - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://twitter.com/saved_searches/destroy/" + id + ".json", "")) - except HTTPError, e: - raise TangoError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("destroySavedSearch() requires you to be authenticated.") - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true"): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_background_image.json?tile=" + tile, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileBackgroundImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename): - if self.authenticated is True: - try: - files = [("image", filename, open(filename).read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://twitter.com/account/update_profile_image.json", body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TangoError("updateProfileImage() failed with a %s error code." % `e.code`, e.code) - else: - raise TangoError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' From a3edbb23484d10d1ac175f8802c541d660fcee9b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 03:05:39 -0500 Subject: [PATCH 143/687] New package structure; twython is now separated out into core/oauth/streaming. To maintain compatibility with older Twython versions, simply import twython like: 'import twython.core as twython' - this will allow for easier oauth/streaming development, and should hopefully fix a lot of the installation issues people kept running into with easy_install --- .gitignore | 4 ++++ __init__.py | 1 - examples/current_trends.py | 2 +- examples/daily_trends.py | 2 +- examples/get_friends_timeline.py | 4 ++-- examples/get_user_mention.py | 4 ++-- examples/get_user_timeline.py | 2 +- examples/public_timeline.py | 2 +- examples/rate_limit.py | 4 ++-- examples/search_results.py | 2 +- examples/shorten_url.py | 2 +- examples/twython_setup.py | 14 +++++--------- examples/update_profile_image.py | 4 ++-- examples/update_status.py | 4 ++-- examples/weekly_trends.py | 2 +- setup.py | 4 +--- twython/__init__.py | 1 + core/twython.py => twython/core.py | 2 +- .../oauth/__init__.py | 6 +++--- twython/oauth/__init__.pyc | Bin 0 -> 4397 bytes {oauth => twython/oauth}/oauth.py | 0 twython/oauth/oauth.pyc | Bin 0 -> 18223 bytes .../streaming/__init__.py | 2 ++ twython/streaming/__init__.pyc | Bin 0 -> 2475 bytes twython3k/__init__.py | 1 + core/twython3k.py => twython3k/core.py | 0 26 files changed, 35 insertions(+), 34 deletions(-) create mode 100644 .gitignore delete mode 100644 __init__.py create mode 100644 twython/__init__.py rename core/twython.py => twython/core.py (99%) rename oauth/twython_oauth.py => twython/oauth/__init__.py (97%) create mode 100644 twython/oauth/__init__.pyc rename {oauth => twython/oauth}/oauth.py (100%) create mode 100644 twython/oauth/oauth.pyc rename streaming/twython_streaming.py => twython/streaming/__init__.py (96%) create mode 100644 twython/streaming/__init__.pyc create mode 100644 twython3k/__init__.py rename core/twython3k.py => twython3k/core.py (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..996a1e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.pyc +build +dist +twython.egg-info diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 73b19e2..0000000 --- a/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import twython as twython diff --git a/examples/current_trends.py b/examples/current_trends.py index 425cdee..8ee4d74 100644 --- a/examples/current_trends.py +++ b/examples/current_trends.py @@ -1,4 +1,4 @@ -import twitter +import twython.core as twython """ Instantiate Twython with no Authentication """ twitter = twython.setup() diff --git a/examples/daily_trends.py b/examples/daily_trends.py index 987611e..38ca507 100644 --- a/examples/daily_trends.py +++ b/examples/daily_trends.py @@ -1,4 +1,4 @@ -import twitter +import twython.core as twython """ Instantiate Twython with no Authentication """ twitter = twython.setup() diff --git a/examples/get_friends_timeline.py b/examples/get_friends_timeline.py index 9f3fa06..9e67583 100644 --- a/examples/get_friends_timeline.py +++ b/examples/get_friends_timeline.py @@ -1,7 +1,7 @@ -import twython, pprint +import twython.core as twython, pprint # Authenticate using Basic (HTTP) Authentication -twitter = twython.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(username="example", password="example") friends_timeline = twitter.getFriendsTimeline(count="150", page="3") for tweet in friends_timeline: diff --git a/examples/get_user_mention.py b/examples/get_user_mention.py index 08b3dff..0db63a0 100644 --- a/examples/get_user_mention.py +++ b/examples/get_user_mention.py @@ -1,6 +1,6 @@ -import twitter +import twython.core as twython -twitter = twython.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(username="example", password="example") mentions = twitter.getUserMentions(count="150") print mentions diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py index b4191b0..bdb9200 100644 --- a/examples/get_user_timeline.py +++ b/examples/get_user_timeline.py @@ -1,4 +1,4 @@ -import twython +import twython.core as twython # We won't authenticate for this, but sometimes it's necessary twitter = twython.setup() diff --git a/examples/public_timeline.py b/examples/public_timeline.py index b6be0f7..4670037 100644 --- a/examples/public_timeline.py +++ b/examples/public_timeline.py @@ -1,4 +1,4 @@ -import twython +import twython.core as twython # Getting the public timeline requires no authentication, huzzah twitter = twython.setup() diff --git a/examples/rate_limit.py b/examples/rate_limit.py index aca5095..4b632b0 100644 --- a/examples/rate_limit.py +++ b/examples/rate_limit.py @@ -1,7 +1,7 @@ -import twython +import twython.core as twython # Instantiate with Basic (HTTP) Authentication -twitter = twython.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(username="example", password="example") # This returns the rate limit for the requesting IP rateLimit = twitter.getRateLimitStatus() diff --git a/examples/search_results.py b/examples/search_results.py index b046b22..e0df48b 100644 --- a/examples/search_results.py +++ b/examples/search_results.py @@ -1,4 +1,4 @@ -import twython +import twython.core as twython """ Instantiate Tango with no Authentication """ twitter = twython.setup() diff --git a/examples/shorten_url.py b/examples/shorten_url.py index 2a744d6..f0824bf 100644 --- a/examples/shorten_url.py +++ b/examples/shorten_url.py @@ -1,4 +1,4 @@ -import twython +import twython as twython # Shortening URLs requires no authentication, huzzah twitter = twython.setup() diff --git a/examples/twython_setup.py b/examples/twython_setup.py index 362cb43..6eea228 100644 --- a/examples/twython_setup.py +++ b/examples/twython_setup.py @@ -1,11 +1,7 @@ -import twython +import twython.core as twython -# Using no authentication and specifying Debug -twitter = twython.setup(debug=True) +# Using no authentication +twitter = twython.setup() -# Using Basic Authentication -twitter = twython.setup(authtype="Basic", username="example", password="example") - -# Using OAuth Authentication (Note: OAuth is the default, specify Basic if needed) -auth_keys = {"consumer_key": "yourconsumerkey", "consumer_secret": "yourconsumersecret"} -twitter = twython.setup(username="example", password="example", oauth_keys=auth_keys) +# Using Basic Authentication (core is all about basic auth, look to twython.oauth in the future for oauth) +twitter = twython.setup(username="example", password="example") diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py index ad6f9ad..37ee00e 100644 --- a/examples/update_profile_image.py +++ b/examples/update_profile_image.py @@ -1,5 +1,5 @@ -import twython +import twython.core as twython # Instantiate Twython with Basic (HTTP) Authentication -twitter = twython.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(username="example", password="example") twitter.updateProfileImage("myImage.png") diff --git a/examples/update_status.py b/examples/update_status.py index 52d2e87..0f7fe38 100644 --- a/examples/update_status.py +++ b/examples/update_status.py @@ -1,5 +1,5 @@ -import twython +import twython.core as twython # Create a Twython instance using Basic (HTTP) Authentication and update our Status -twitter = twython.setup(authtype="Basic", username="example", password="example") +twitter = twython.setup(username="example", password="example") twitter.updateStatus("See how easy this was?") diff --git a/examples/weekly_trends.py b/examples/weekly_trends.py index 5871fbd..e48fb95 100644 --- a/examples/weekly_trends.py +++ b/examples/weekly_trends.py @@ -1,4 +1,4 @@ -import twython +import twython.core as twython """ Instantiate Twython with no Authentication """ twitter = twython.setup() diff --git a/setup.py b/setup.py index f5c956f..1a52d20 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,7 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '0.9' - -# For the love of god, use Pip to install this. +__version__ = '1.0' # Distutils version METADATA = dict( diff --git a/twython/__init__.py b/twython/__init__.py new file mode 100644 index 0000000..083a571 --- /dev/null +++ b/twython/__init__.py @@ -0,0 +1 @@ +import core, oauth, streaming diff --git a/core/twython.py b/twython/core.py similarity index 99% rename from core/twython.py rename to twython/core.py index f9e94bd..b88d637 100644 --- a/core/twython.py +++ b/twython/core.py @@ -16,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.9" +__version__ = "1.0" """Twython - Easy Twitter utilities in Python""" diff --git a/oauth/twython_oauth.py b/twython/oauth/__init__.py similarity index 97% rename from oauth/twython_oauth.py rename to twython/oauth/__init__.py index db9ed1d..4d88b1f 100644 --- a/oauth/twython_oauth.py +++ b/twython/oauth/__init__.py @@ -7,17 +7,17 @@ Questions, comments? ryan@venodesigns.net """ -import twython, httplib, urllib, urllib2, mimetypes, mimetools +import httplib, urllib, urllib2, mimetypes, mimetools from urlparse import urlparse from urllib2 import HTTPError try: - import oauth + import oauth as oauthlib except ImportError: pass -class twyauth: +class oauth: def __init__(self, username, consumer_key, consumer_secret, signature_method = None, headers = None, version = 1): """oauth(username = None, consumer_secret = None, consumer_key = None, headers = None) diff --git a/twython/oauth/__init__.pyc b/twython/oauth/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b8a4983c01ca631e566d232f355fa9dba7be1b7 GIT binary patch literal 4397 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 Oqi!#XE{lFEwSNKCkwlpQ literal 0 HcmV?d00001 diff --git a/twython3k/__init__.py b/twython3k/__init__.py new file mode 100644 index 0000000..d93c168 --- /dev/null +++ b/twython3k/__init__.py @@ -0,0 +1 @@ +import core diff --git a/core/twython3k.py b/twython3k/core.py similarity index 100% rename from core/twython3k.py rename to twython3k/core.py From 850c1011d1c399f2c4cff9ca9051bb992508873a Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 03:19:42 -0500 Subject: [PATCH 144/687] Breaking down package structure a bit more --- setup.py | 2 +- twython/__init__.py | 2 +- twython/{oauth => }/oauth.py | 0 twython/oauth/__init__.pyc | Bin 4397 -> 0 bytes twython/oauth/oauth.pyc | Bin 18223 -> 0 bytes twython/{streaming/__init__.py => streaming.py} | 0 twython/streaming/__init__.pyc | Bin 2475 -> 0 bytes twython/{oauth/__init__.py => twyauth.py} | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename twython/{oauth => }/oauth.py (100%) delete mode 100644 twython/oauth/__init__.pyc delete mode 100644 twython/oauth/oauth.pyc rename twython/{streaming/__init__.py => streaming.py} (100%) delete mode 100644 twython/streaming/__init__.pyc rename twython/{oauth/__init__.py => twyauth.py} (100%) diff --git a/setup.py b/setup.py index 1a52d20..e21d0fc 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ __version__ = '1.0' METADATA = dict( name = "twython", version = __version__, - py_modules = ['twython'], + py_modules = ['twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], author = 'Ryan McGrath', author_email = 'ryan@venodesigns.net', description = 'An easy (and up to date) way to access Twitter data with Python.', diff --git a/twython/__init__.py b/twython/__init__.py index 083a571..00deffe 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1 +1 @@ -import core, oauth, streaming +import core, twyauth, streaming diff --git a/twython/oauth/oauth.py b/twython/oauth.py similarity index 100% rename from twython/oauth/oauth.py rename to twython/oauth.py diff --git a/twython/oauth/__init__.pyc b/twython/oauth/__init__.pyc deleted file mode 100644 index 3b8a4983c01ca631e566d232f355fa9dba7be1b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4397 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 Oqi!#XE{lFEwSNKCkwlpQ diff --git a/twython/oauth/__init__.py b/twython/twyauth.py similarity index 100% rename from twython/oauth/__init__.py rename to twython/twyauth.py From c40b6a6ebe7c2ec1c003091bfdd6c604cee9e55f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 03:27:23 -0500 Subject: [PATCH 145/687] import oauth as oauth to avoid namespacing conflicts in builds; setup.py now properly includes all necessary modules, fixes build problems people reported --- setup.py | 2 +- twython/twyauth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e21d0fc..3af5b8a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ __version__ = '1.0' METADATA = dict( name = "twython", version = __version__, - py_modules = ['twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], + py_modules = ['twython/__init__', 'twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], author = 'Ryan McGrath', author_email = 'ryan@venodesigns.net', description = 'An easy (and up to date) way to access Twitter data with Python.', diff --git a/twython/twyauth.py b/twython/twyauth.py index 4d88b1f..bf4183f 100644 --- a/twython/twyauth.py +++ b/twython/twyauth.py @@ -13,7 +13,7 @@ from urlparse import urlparse from urllib2 import HTTPError try: - import oauth as oauthlib + import oauth except ImportError: pass From 221b3377987951dee4c8af7668096bc94f03e7ab Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 17 Dec 2009 03:30:34 -0500 Subject: [PATCH 146/687] Properly instantiating an instance of twython.core in the README example --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index cad0554..302ac4d 100644 --- a/README.markdown +++ b/README.markdown @@ -38,7 +38,7 @@ Example Use ----------------------------------------------------------------------------------------------------- > import twython > -> twitter = twython.setup(username="example", password="example") +> twitter = twython.core.setup(username="example", password="example") > twitter.updateStatus("See how easy this was?") From 68ac67e85db94e451116e0924b1d1555089424cb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 21 Dec 2009 03:04:46 -0500 Subject: [PATCH 147/687] Documented the new proxy use/authentication methods (see the setup() method) --- twython/core.py | 9 +++++++++ twython3k/core.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/twython/core.py b/twython/core.py index b88d637..b56b0e4 100644 --- a/twython/core.py +++ b/twython/core.py @@ -61,6 +61,15 @@ class setup: username - Your Twitter username, if you want Basic (HTTP) Authentication. password - Password for your twitter account, if you want Basic (HTTP) Authentication. headers - User agent header. + proxy - An object detailing information, in case proxy use/authentication is required. Object passed should be something like... + + proxyobj = { + "username": "fjnfsjdnfjd", + "password": "fjnfjsjdfnjd", + "host": "http://fjanfjasnfjjfnajsdfasd.com", + "port": 87 + } + version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. diff --git a/twython3k/core.py b/twython3k/core.py index 5c1653e..677afc9 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -60,6 +60,15 @@ class setup: Parameters: username - Your Twitter username, if you want Basic (HTTP) Authentication. password - Password for your twitter account, if you want Basic (HTTP) Authentication. + proxy - An object detailing information, in case proxy use/authentication is required. Object passed should be something like... + + proxyobj = { + "username": "fjnfsjdnfjd", + "password": "fjnfjsjdfnjd", + "host": "http://fjanfjasnfjjfnajsdfasd.com", + "port": 87 + } + headers - User agent header. version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. From cf20f2975ac370cd2f8b591a7fb9d2c6d81f632b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 21 Dec 2009 22:43:11 -0500 Subject: [PATCH 148/687] Typo'd follow in a param specification, fixing... --- twython/core.py | 2 +- twython3k/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/core.py b/twython/core.py index b56b0e4..ca41a36 100644 --- a/twython/core.py +++ b/twython/core.py @@ -760,7 +760,7 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) else: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) except HTTPError, e: diff --git a/twython3k/core.py b/twython3k/core.py index 677afc9..3b1542e 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -760,7 +760,7 @@ class setup: apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?folow=%s" % follow)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) else: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) except HTTPError as e: From 25f68b26087d595199d17bc294c7018595619e04 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 22 Dec 2009 04:06:33 -0500 Subject: [PATCH 149/687] getListMembers() needs to always pass the id --- twython/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/core.py b/twython/core.py index ca41a36..b4ca4a9 100644 --- a/twython/core.py +++ b/twython/core.py @@ -1609,7 +1609,7 @@ class setup: version = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE&id=%s" % `id`)) except HTTPError, e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) else: From a1bd6bfb856c28a492f772920662474399038863 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 30 Dec 2009 03:27:59 -0500 Subject: [PATCH 150/687] Don't auto-kill at the 140 limit, as other languages apparently treat this differently. Leave it up to the programmer to determine length issues, I guess. --- twython/core.py | 2 -- twython3k/core.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/twython/core.py b/twython/core.py index b4ca4a9..7a664c9 100644 --- a/twython/core.py +++ b/twython/core.py @@ -585,8 +585,6 @@ class setup: This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. """ version = version or self.apiVersion - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({ "status": self.unicode2utf8(status), diff --git a/twython3k/core.py b/twython3k/core.py index 3b1542e..fb3e232 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -585,8 +585,6 @@ class setup: This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. """ version = version or self.apiVersion - if len(list(status)) > 140: - raise TwythonError("This status message is over 140 characters. Trim it down!") try: return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.parse.urlencode({ "status": self.unicode2utf8(status), From 0e878ce75d7dc8eec312cc06e4e286043e8dcfb0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 2 Jan 2010 06:06:20 -0500 Subject: [PATCH 151/687] Don't pass odd null parameters for updateStatus(), Twitter has seemingly decided to barf on them --- twython/core.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/twython/core.py b/twython/core.py index 7a664c9..9314abb 100644 --- a/twython/core.py +++ b/twython/core.py @@ -586,12 +586,12 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.urlencode({ - "status": self.unicode2utf8(status), - "in_reply_to_status_id": in_reply_to_status_id, - "lat": latitude, - "long": longitude - }))) + postExt = urllib.urlencode({"status": self.unicode2utf8(status)}) + if latitude is not None and longitude is not None: + postExt += "&lat=%s&long=%s" % (latitude, longitude) + if in_reply_to_status_id is not None: + postExt += "&in_reply_to_status_id=%s" % `in_reply_to_status_id` + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, postExt)) except HTTPError, e: raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) From 08c02000206ddd3e9e59d3f759e79918b329e72e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 10 Jan 2010 15:28:46 -0500 Subject: [PATCH 152/687] Fix for destroyStatus() method returning consistent 404's - properly initiate POST, fix url to reference properly, always require string instead of number for tweet id --- twython/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/core.py b/twython/core.py index 9314abb..a49610e 100644 --- a/twython/core.py +++ b/twython/core.py @@ -608,7 +608,7 @@ class setup: version = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, `id`), "DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/destroy/%s.json?" % (version, id), "_method=DELETE")) except HTTPError, e: raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) else: From 3f5fceb38b9bbf0c5fc57abca732b925d7975fbb Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 14 Jan 2010 00:48:30 -0500 Subject: [PATCH 153/687] Merging recent changes over to the Twython3k build --- twython3k/core.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/twython3k/core.py b/twython3k/core.py index fb3e232..b456786 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -16,20 +16,14 @@ from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "0.9" +__version__ = "1.0" """Twython - Easy Twitter utilities in Python""" try: - import simplejson + import json as simplejson except ImportError: - try: - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + raise Exception("Twython requires a json library to work. (Try http://www.undefined.org/python/ ?") class TwythonError(Exception): def __init__(self, msg, error_code=None): @@ -60,6 +54,7 @@ class setup: Parameters: username - Your Twitter username, if you want Basic (HTTP) Authentication. password - Password for your twitter account, if you want Basic (HTTP) Authentication. + headers - User agent header. proxy - An object detailing information, in case proxy use/authentication is required. Object passed should be something like... proxyobj = { @@ -69,7 +64,6 @@ class setup: "port": 87 } - headers - User agent header. version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. @@ -586,12 +580,12 @@ class setup: """ version = version or self.apiVersion try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, urllib.parse.urlencode({ - "status": self.unicode2utf8(status), - "in_reply_to_status_id": in_reply_to_status_id, - "lat": latitude, - "long": longitude - }))) + postExt = urllib.parse.urlencode({"status": self.unicode2utf8(status)}) + if latitude is not None and longitude is not None: + postExt += "&lat=%s&long=%s" % (latitude, longitude) + if in_reply_to_status_id is not None: + postExt += "&in_reply_to_status_id=%s" % repr(in_reply_to_status_id) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, postExt)) except HTTPError as e: raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) @@ -608,7 +602,7 @@ class setup: version = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/status/destroy/%s.json" % (version, repr(id)), "DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/destroy/%s.json?" % (version, id), "_method=DELETE")) except HTTPError as e: raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) else: @@ -1607,7 +1601,7 @@ class setup: version = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE")) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE&id=%s" % repr(id))) except HTTPError as e: raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) else: From 844c1ae23514594eb37dd6f2dfe9cc2842c4cf38 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 10 Feb 2010 18:51:58 -0500 Subject: [PATCH 154/687] Removing redundant code block --- twython/core.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/twython/core.py b/twython/core.py index a49610e..9b0209e 100644 --- a/twython/core.py +++ b/twython/core.py @@ -459,10 +459,7 @@ class setup: apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) if apiURL != "": try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(self.opener.open(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) From 30fbacb066834017b574dd6e3bffcd8694dc7c0e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 22 Feb 2010 23:03:30 -0500 Subject: [PATCH 155/687] Merging in a changeset to fix updateProfileColors() to use a proper POST method, instead of using a GET (thanks to Pedro Varangot for the submitted patch) --- twython/core.py | 62 +++++++++++++++++++++++++++++++++----- twython3k/core.py | 77 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/twython/core.py b/twython/core.py index 9b0209e..5c1ed75 100644 --- a/twython/core.py +++ b/twython/core.py @@ -139,12 +139,13 @@ class setup: 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 = version or self.apiVersion + try: if rate_for == "requestingIP": return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: @@ -872,9 +873,15 @@ class setup: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, version = None, **kwargs): - """updateProfileColors(**kwargs) + + def updateProfileColors(self, + profile_background_color = None, + profile_text_color = None, + profile_link_color = None, + profile_sidebar_fill_color = None, + profile_sidebar_border_color = None, + version = None): + """updateProfileColors() Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. @@ -890,15 +897,56 @@ class setup: 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 = version or self.apiVersion if self.authenticated is True: + useAmpersands = False + updateProfileColorsQueryString = "" + + def checkValidColor(str): + if len(str) != 6: + return False + for c in str: + if c not in "1234567890abcdefABCDEF": return False + + return True + + if profile_background_color is not None: + if checkValidColor(profile_background_color): + updateProfileColorsQueryString += "profile_background_color=" + profile_background_color + useAmpersands = True + else: + raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") + if profile_text_color is not None: + if checkValidColor(profile_text_color): + updateProfileColorsQueryString += "profile_text_color=" + profile_text_color + useAmpersands = True + else: + raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") + if profile_link_color is not None: + if checkValidColor(profile_link_color): + updateProfileColorsQueryString += "profile_link_color=" + profile_link_color + useAmpersands = True + else: + raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") + if profile_sidebar_fill_color is not None: + if checkValidColor(profile_sidebar_fill_color): + updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color + useAmpersands = True + else: + raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") + if profile_sidebar_border_color is not None: + if checkValidColor(profile_sidebar_border_color): + updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color + useAmpersands = True + else: + raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") + try: - return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) + return self.opener.open("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, updateProfileColorsQueryString) except HTTPError, e: raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - + def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): """updateProfile(name = None, email = None, url = None, location = None, description = None) diff --git a/twython3k/core.py b/twython3k/core.py index b456786..a2af614 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -21,9 +21,15 @@ __version__ = "1.0" """Twython - Easy Twitter utilities in Python""" try: - import json as simplejson + import simplejson except ImportError: - raise Exception("Twython requires a json library to work. (Try http://www.undefined.org/python/ ?") + try: + import json as simplejson + except ImportError: + try: + from django.utils import simplejson + except: + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") class TwythonError(Exception): def __init__(self, msg, error_code=None): @@ -133,12 +139,13 @@ class setup: 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 = version or self.apiVersion + try: if rate_for == "requestingIP": return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: @@ -453,10 +460,7 @@ class setup: apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) if apiURL != "": try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(self.opener.open(apiURL)) + return simplejson.load(self.opener.open(apiURL)) except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) @@ -869,9 +873,15 @@ class setup: raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, version = None, **kwargs): - """updateProfileColors(**kwargs) + + def updateProfileColors(self, + profile_background_color = None, + profile_text_color = None, + profile_link_color = None, + profile_sidebar_fill_color = None, + profile_sidebar_border_color = None, + version = None): + """updateProfileColors() Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. @@ -887,15 +897,56 @@ class setup: 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 = version or self.apiVersion if self.authenticated is True: + useAmpersands = False + updateProfileColorsQueryString = "" + + def checkValidColor(str): + if len(str) != 6: + return False + for c in str: + if c not in "1234567890abcdefABCDEF": return False + + return True + + if profile_background_color is not None: + if checkValidColor(profile_background_color): + updateProfileColorsQueryString += "profile_background_color=" + profile_background_color + useAmpersands = True + else: + raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") + if profile_text_color is not None: + if checkValidColor(profile_text_color): + updateProfileColorsQueryString += "profile_text_color=" + profile_text_color + useAmpersands = True + else: + raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") + if profile_link_color is not None: + if checkValidColor(profile_link_color): + updateProfileColorsQueryString += "profile_link_color=" + profile_link_color + useAmpersands = True + else: + raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") + if profile_sidebar_fill_color is not None: + if checkValidColor(profile_sidebar_fill_color): + updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color + useAmpersands = True + else: + raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") + if profile_sidebar_border_color is not None: + if checkValidColor(profile_sidebar_border_color): + updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color + useAmpersands = True + else: + raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") + try: - return self.opener.open(self.constructApiURL("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, kwargs)) + return self.opener.open("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, updateProfileColorsQueryString) except HTTPError as e: raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) else: raise AuthError("updateProfileColors() requires you to be authenticated.") - + def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): """updateProfile(name = None, email = None, url = None, location = None, description = None) From 8bea592d97e06ac0204fb40bb6537e76d077a049 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 25 Feb 2010 02:14:14 -0500 Subject: [PATCH 156/687] Increment version number to 1.2; fixed a bug in updateProfileColors() wherein multiple values wouldn't get properly concatenated/url-encoded. Changed getRateLimitStatus() to accept a boolean of 'checkRequestingIP', which should hopefully make the method a little more clear for debugging purposes. --- setup.py | 4 ++-- twython/core.py | 21 +++++++-------------- twython3k/core.py | 21 +++++++-------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/setup.py b/setup.py index 3af5b8a..7a436b2 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,13 @@ import sys, os __author__ = 'Ryan McGrath ' -__version__ = '1.0' +__version__ = '1.2' # Distutils version METADATA = dict( name = "twython", version = __version__, - py_modules = ['twython/__init__', 'twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], + py_modules = ['setup', 'twython/__init__', 'twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], author = 'Ryan McGrath', author_email = 'ryan@venodesigns.net', description = 'An easy (and up to date) way to access Twitter data with Python.', diff --git a/twython/core.py b/twython/core.py index 5c1ed75..89b6dc9 100644 --- a/twython/core.py +++ b/twython/core.py @@ -125,7 +125,7 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - def getRateLimitStatus(self, rate_for = "requestingIP", version = None): + def getRateLimitStatus(self, checkRequestingIP = True, version = None): """getRateLimitStatus() Returns the remaining number of API requests available to the requesting user before the @@ -135,17 +135,16 @@ class setup: IP address is returned. Params: - rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) + checkRequestIP - Boolean, defaults to True. Set to False to check against the currently requesting IP, instead of the account level. 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 = version or self.apiVersion - + version = version or self.apiVersion try: - if rate_for == "requestingIP": - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + if checkRequestingIP is True: + return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError, e: @@ -898,8 +897,7 @@ class setup: 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. """ if self.authenticated is True: - useAmpersands = False - updateProfileColorsQueryString = "" + updateProfileColorsQueryString = "?lol=2" def checkValidColor(str): if len(str) != 6: @@ -912,31 +910,26 @@ class setup: if profile_background_color is not None: if checkValidColor(profile_background_color): updateProfileColorsQueryString += "profile_background_color=" + profile_background_color - useAmpersands = True else: raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") if profile_text_color is not None: if checkValidColor(profile_text_color): updateProfileColorsQueryString += "profile_text_color=" + profile_text_color - useAmpersands = True else: raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") if profile_link_color is not None: if checkValidColor(profile_link_color): updateProfileColorsQueryString += "profile_link_color=" + profile_link_color - useAmpersands = True else: raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") if profile_sidebar_fill_color is not None: if checkValidColor(profile_sidebar_fill_color): updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color - useAmpersands = True else: raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") if profile_sidebar_border_color is not None: if checkValidColor(profile_sidebar_border_color): updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color - useAmpersands = True else: raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") diff --git a/twython3k/core.py b/twython3k/core.py index a2af614..373a1d7 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -125,7 +125,7 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) - def getRateLimitStatus(self, rate_for = "requestingIP", version = None): + def getRateLimitStatus(self, checkRequestingIP = True, version = None): """getRateLimitStatus() Returns the remaining number of API requests available to the requesting user before the @@ -135,17 +135,16 @@ class setup: IP address is returned. Params: - rate_for - Defaults to "requestingIP", but can be changed to check on whatever account is currently authenticated. (Pass a blank string or something) + checkRequestIP - Boolean, defaults to True. Set to False to check against the currently requesting IP, instead of the account level. 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 = version or self.apiVersion - + version = version or self.apiVersion try: - if rate_for == "requestingIP": - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + if checkRequestingIP is True: + return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: if self.authenticated is True: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) else: raise TwythonError("You need to be authenticated to check a rate limit status on an account.") except HTTPError as e: @@ -898,8 +897,7 @@ class setup: 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. """ if self.authenticated is True: - useAmpersands = False - updateProfileColorsQueryString = "" + updateProfileColorsQueryString = "?lol=2" def checkValidColor(str): if len(str) != 6: @@ -912,31 +910,26 @@ class setup: if profile_background_color is not None: if checkValidColor(profile_background_color): updateProfileColorsQueryString += "profile_background_color=" + profile_background_color - useAmpersands = True else: raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") if profile_text_color is not None: if checkValidColor(profile_text_color): updateProfileColorsQueryString += "profile_text_color=" + profile_text_color - useAmpersands = True else: raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") if profile_link_color is not None: if checkValidColor(profile_link_color): updateProfileColorsQueryString += "profile_link_color=" + profile_link_color - useAmpersands = True else: raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") if profile_sidebar_fill_color is not None: if checkValidColor(profile_sidebar_fill_color): updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color - useAmpersands = True else: raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") if profile_sidebar_border_color is not None: if checkValidColor(profile_sidebar_border_color): updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color - useAmpersands = True else: raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") From fe59e56361a0a9e822cc0ba4653bc57024c3caa6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 17 Mar 2010 03:19:41 -0400 Subject: [PATCH 157/687] Added a new bulkUserLookup() method. Takes two optional arrays of user_ids or screen_names and returns data from Twitter concerning all the users in question. (e.g, lol.bulkUserLookup(user_ids=[1,2,3], screen_names=["danmcgrath", "enotionz", "shiftb"])" --- twython/core.py | 32 +++++++++++++++++++++++++++++--- twython3k/core.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/twython/core.py b/twython/core.py index 89b6dc9..d24bbe2 100644 --- a/twython/core.py +++ b/twython/core.py @@ -16,7 +16,7 @@ from urlparse import urlparse from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "1.0" +__version__ = "1.1" """Twython - Easy Twitter utilities in Python""" @@ -53,7 +53,7 @@ class AuthError(TwythonError): class setup: def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): - """setup(authtype = "OAuth", username = None, password = None, proxy = None, headers = None) + """setup(username = None, password = None, proxy = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -115,7 +115,7 @@ class setup: Parameters: url_to_shorten - URL to shorten. - shortener - In case you want to use url shorterning service other that is.gd. + shortener - In case you want to use a url shortening service other than is.gd. """ try: return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() @@ -463,6 +463,32 @@ class setup: except HTTPError, e: raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) + def bulkUserLookup(self, ids = None, screen_names = None, version = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + + 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. + + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + version = version or self.apiVersion + if self.authenticated is True: + apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += `id` + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("bulkUserLookup() requires you to be authenticated.") + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") diff --git a/twython3k/core.py b/twython3k/core.py index 373a1d7..ec00a02 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -16,7 +16,7 @@ from urllib.parse import urlparse from urllib.error import HTTPError __author__ = "Ryan McGrath " -__version__ = "1.0" +__version__ = "1.1" """Twython - Easy Twitter utilities in Python""" @@ -53,7 +53,7 @@ class AuthError(TwythonError): class setup: def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): - """setup(authtype = "OAuth", username = None, password = None, proxy = None, headers = None) + """setup(username = None, password = None, proxy = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -115,7 +115,7 @@ class setup: Parameters: url_to_shorten - URL to shorten. - shortener - In case you want to use url shorterning service other that is.gd. + shortener - In case you want to use a url shortening service other than is.gd. """ try: return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() @@ -463,6 +463,32 @@ class setup: except HTTPError as e: raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) + def bulkUserLookup(self, ids = None, screen_names = None, version = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + + 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. + + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + version = version or self.apiVersion + if self.authenticated is True: + apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += repr(id) + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError as e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) + else: + raise AuthError("bulkUserLookup() requires you to be authenticated.") + def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") From ab2dd1e2f046cf4a1ca33354c5cc061c17c7ccc7 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 10 Apr 2010 01:06:37 -0400 Subject: [PATCH 158/687] Fixing createFriendship parameter passing - never needed the question mark. How did this sit for so long? --- twython/core.py | 8 ++++---- twython3k/core.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/twython/core.py b/twython/core.py index d24bbe2..da0e7d2 100644 --- a/twython/core.py +++ b/twython/core.py @@ -776,9 +776,9 @@ class setup: if self.authenticated is True: apiURL = "" if user_id is not None: - apiURL = "?user_id=%s&follow=%s" %(`user_id`, follow) + apiURL = "user_id=%s&follow=%s" %(`user_id`, follow) if screen_name is not None: - apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) + apiURL = "screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) @@ -809,9 +809,9 @@ class setup: if self.authenticated is True: apiURL = "" if user_id is not None: - apiURL = "?user_id=%s" % `user_id` + apiURL = "user_id=%s" % `user_id` if screen_name is not None: - apiURL = "?screen_name=%s" % screen_name + apiURL = "screen_name=%s" % screen_name try: if id is not None: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, `id`), "lol=1")) # Random string hack for POST reasons ;P diff --git a/twython3k/core.py b/twython3k/core.py index ec00a02..115aae9 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -776,9 +776,9 @@ class setup: if self.authenticated is True: apiURL = "" if user_id is not None: - apiURL = "?user_id=%s&follow=%s" %(repr(user_id), follow) + apiURL = "user_id=%s&follow=%s" %(repr(user_id), follow) if screen_name is not None: - apiURL = "?screen_name=%s&follow=%s" %(screen_name, follow) + apiURL = "screen_name=%s&follow=%s" %(screen_name, follow) try: if id is not None: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) @@ -809,9 +809,9 @@ class setup: if self.authenticated is True: apiURL = "" if user_id is not None: - apiURL = "?user_id=%s" % repr(user_id) + apiURL = "user_id=%s" % repr(user_id) if screen_name is not None: - apiURL = "?screen_name=%s" % screen_name + apiURL = "screen_name=%s" % screen_name try: if id is not None: return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, repr(id)), "lol=1")) # Random string hack for POST reasons ;P From fc6b9e13624abf6581c5882d6248b0e11ef48011 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 23 Apr 2010 00:26:57 -0400 Subject: [PATCH 159/687] Updating Trends API to reflect new endpoints. --- twython/core.py | 20 +++++++++++++------- twython3k/core.py | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/twython/core.py b/twython/core.py index da0e7d2..dee5a6e 100644 --- a/twython/core.py +++ b/twython/core.py @@ -1342,16 +1342,18 @@ class setup: except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - def getCurrentTrends(self, excludeHashTags = False): - """getCurrentTrends(excludeHashTags = False) + def getCurrentTrends(self, excludeHashTags = False, version = None): + """getCurrentTrends(excludeHashTags = False, version = None) Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used on Twitter Search results page for that topic. Parameters: excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/current.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/current.json" % version if excludeHashTags is True: apiURL += "?exclude=hashtags" try: @@ -1359,16 +1361,18 @@ class setup: except HTTPError, e: raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - def getDailyTrends(self, date = None, exclude = False): - """getDailyTrends(date = None, exclude = False) + def getDailyTrends(self, date = None, exclude = False, version = None): + """getDailyTrends(date = None, exclude = False, version = None) Returns the top 20 trending topics for each hour in a given day. Parameters: date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/daily.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/daily.json" % version questionMarkUsed = False if date is not None: apiURL += "?date=%s" % date @@ -1391,8 +1395,10 @@ class setup: Parameters: date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/daily.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/daily.json" % version questionMarkUsed = False if date is not None: apiURL += "?date=%s" % date diff --git a/twython3k/core.py b/twython3k/core.py index 115aae9..7be9f34 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -1342,16 +1342,18 @@ class setup: except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - def getCurrentTrends(self, excludeHashTags = False): - """getCurrentTrends(excludeHashTags = False) + def getCurrentTrends(self, excludeHashTags = False, version = None): + """getCurrentTrends(excludeHashTags = False, version = None) Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used on Twitter Search results page for that topic. Parameters: excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/current.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/current.json" % version if excludeHashTags is True: apiURL += "?exclude=hashtags" try: @@ -1359,16 +1361,18 @@ class setup: except HTTPError as e: raise TwythonError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) - def getDailyTrends(self, date = None, exclude = False): - """getDailyTrends(date = None, exclude = False) + def getDailyTrends(self, date = None, exclude = False, version = None): + """getDailyTrends(date = None, exclude = False, version = None) Returns the top 20 trending topics for each hour in a given day. Parameters: date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/daily.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/daily.json" % version questionMarkUsed = False if date is not None: apiURL += "?date=%s" % date @@ -1391,8 +1395,10 @@ class setup: Parameters: date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. + 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. """ - apiURL = "http://search.twitter.com/trends/daily.json" + version = version or self.apiVersion + apiURL = "http://api.twitter.com/%d/trends/daily.json" % version questionMarkUsed = False if date is not None: apiURL += "?date=%s" % date From 7dbbd954b2a2580e1a768af51a143039cc5e9255 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 11 May 2010 01:19:17 -0400 Subject: [PATCH 160/687] Changing verifyCredentials to not auto-fire on class instantiation. No need to waste network resources; if anybody *wants* to verify credentials from this point onwards, you need to explicitly call instance.verifyCredentials(). This'll help with the upcoming change from Basic Auth... --- twython/core.py | 24 +++++++++++++++++++----- twython3k/core.py | 24 +++++++++++++++++++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/twython/core.py b/twython/core.py index dee5a6e..da5dcbc 100644 --- a/twython/core.py +++ b/twython/core.py @@ -93,11 +93,7 @@ class setup: self.opener = urllib2.build_opener(self.handler) if self.headers is not None: self.opener.addheaders = [('User-agent', self.headers)] - try: - simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) - self.authenticated = True - except HTTPError, e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) + self.authenticated = True # Play nice, people can force-check using verifyCredentials() else: # Build a non-auth opener so we can allow proxy-auth and/or header swapping if self.proxy is not None: @@ -124,7 +120,25 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + + def verifyCredentials(self, version = None): + """ verifyCredentials(self, version = None): + Verifies the authenticity of the passed in credentials. Used to be a forced call, now made optional + (no need to waste network resources) + + Parameters: + None + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % version)) + except HTTPError, e: + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) + else: + raise AuthError("verifyCredentials() requires you to actually, y'know, pass in credentials.") + def getRateLimitStatus(self, checkRequestingIP = True, version = None): """getRateLimitStatus() diff --git a/twython3k/core.py b/twython3k/core.py index 7be9f34..1f5cf79 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -93,11 +93,7 @@ class setup: self.opener = urllib.request.build_opener(self.handler) if self.headers is not None: self.opener.addheaders = [('User-agent', self.headers)] - try: - simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % self.apiVersion)) - self.authenticated = True - except HTTPError as e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) + self.authenticated = True # Play nice, people can force-check using verifyCredentials() else: # Build a non-auth opener so we can allow proxy-auth and/or header swapping if self.proxy is not None: @@ -124,7 +120,25 @@ class setup: def constructApiURL(self, base_url, params): return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) + + def verifyCredentials(self, version = None): + """ verifyCredentials(self, version = None): + Verifies the authenticity of the passed in credentials. Used to be a forced call, now made optional + (no need to waste network resources) + + Parameters: + None + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % version)) + except HTTPError as e: + raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) + else: + raise AuthError("verifyCredentials() requires you to actually, y'know, pass in credentials.") + def getRateLimitStatus(self, checkRequestingIP = True, version = None): """getRateLimitStatus() From d630e02b6efaedb7d4127a8dfdca3ed744391aaf Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 16 Aug 2010 02:32:55 -0700 Subject: [PATCH 161/687] Fixing and closing issue #15 by ebertti - getFollowersStatus() fails when only checking with ID, needs proper query string stuff. --- twython/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/twython/core.py b/twython/core.py index da5dcbc..d380fbd 100644 --- a/twython/core.py +++ b/twython/core.py @@ -572,10 +572,14 @@ class setup: if screen_name is not None: apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) + if apiURL.find("?") == -1: + apiURL += "?" else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) + apiURL += "&" + if page is not None: + return simplejson.load(self.opener.open(apiURL + "page=%s" % page)) + else: + return simplejson.load(self.opener.open(apiURL + "cursor=%s" % cursor)) except HTTPError, e: raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) else: From d9fcd3a264edf62ec5b5a11c67f9d7ec3a740294 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:37:43 +0800 Subject: [PATCH 162/687] Fixing setup.py script to use valid `setuptools` format. This will fix the broken pip / easy_install issues on many platforms (like most linux distros). --- setup.py | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index 7a436b2..feb0be6 100644 --- a/setup.py +++ b/setup.py @@ -1,46 +1,42 @@ #!/usr/bin/python import sys, os +from setuptools import setup +from setuptools import find_packages + __author__ = 'Ryan McGrath ' __version__ = '1.2' -# Distutils version -METADATA = dict( - name = "twython", + +setup( + + # Basic package information. + name = 'twython', version = __version__, - py_modules = ['setup', 'twython/__init__', 'twython/core', 'twython/twyauth', 'twython/streaming', 'twython/oauth'], + packages = find_packages(), + + # Packaging options. + include_package_data = True, + + # Package dependencies. + install_requires = ['setuptools', 'simplejson'], + + # Metadata for PyPI. author = 'Ryan McGrath', author_email = 'ryan@venodesigns.net', - description = 'An easy (and up to date) way to access Twitter data with Python.', - long_description = open("README.markdown").read(), license = 'MIT License', url = 'http://github.com/ryanmcgrath/twython/tree/master', keywords = 'twitter search api tweet twython', -) - -# Setuptools version -SETUPTOOLS_METADATA = dict( - install_requires = ['setuptools', 'simplejson'], - include_package_data = True, + description = 'An easy (and up to date) way to access Twitter data with Python.', + long_description = open('README.markdown').read(), classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Communications :: Chat', - 'Topic :: Internet', + 'Topic :: Internet' ] + ) - -def Main(): - try: - import setuptools - METADATA.update(SETUPTOOLS_METADATA) - setuptools.setup(**METADATA) - except ImportError: - import distutils.core - distutils.core.setup(**METADATA) - -if __name__ == '__main__': - Main() From 9f7e1fa121be6f027181725fc57f737590c9edc5 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:40:10 +0800 Subject: [PATCH 163/687] Adding a proper MANIFEST.in to the project. This file will instruct setuptools to package *all* important distribution packages when the code gets uploaded to PyPI. --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0d9cce8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE README.markdown +recursive-include examples * +recursive-exclude examples *.pyc From fb6d549eedd963ffbba22b55d1af7da56777ee00 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:45:51 +0800 Subject: [PATCH 164/687] Adding blocking for vim swap files. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 996a1e1..0b15049 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build dist twython.egg-info +*.swp From d5b2c98fb880961d382118eaf9f1c4d837c444ce Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:47:59 +0800 Subject: [PATCH 165/687] PEP-8'ing imports. Also removing useless docstring. Only the *first* docstring defined in a source file will be associated with that module's __doc__ string. So I'm removing the second one that was there as it is unnecessary. --- twython/core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/twython/core.py b/twython/core.py index d380fbd..921e461 100644 --- a/twython/core.py +++ b/twython/core.py @@ -10,15 +10,20 @@ Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes, mimetools + +import httplib +import urllib +import urllib2 +import mimetypes +import mimetools from urlparse import urlparse from urllib2 import HTTPError + __author__ = "Ryan McGrath " __version__ = "1.1" -"""Twython - Easy Twitter utilities in Python""" try: import simplejson From a8ae71fd75c0025f817064cefe7d6bcab1f4204f Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:54:47 +0800 Subject: [PATCH 166/687] Adding a bit more PEP-8. --- twython/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/core.py b/twython/core.py index 921e461..496142d 100644 --- a/twython/core.py +++ b/twython/core.py @@ -36,6 +36,7 @@ except ImportError: except: raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + class TwythonError(Exception): def __init__(self, msg, error_code=None): self.msg = msg @@ -44,18 +45,21 @@ class TwythonError(Exception): def __str__(self): return repr(self.msg) + class APILimit(TwythonError): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) + class AuthError(TwythonError): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) + class setup: def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): """setup(username = None, password = None, proxy = None, headers = None) From c2f87f736cad6e1e5248f0f1fbffc91cf78d92d2 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 16:59:34 +0800 Subject: [PATCH 167/687] Adding some more PEP-8. All lines should be at most 79 characters in length. --- twython/core.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/twython/core.py b/twython/core.py index 496142d..61cb00e 100644 --- a/twython/core.py +++ b/twython/core.py @@ -61,7 +61,9 @@ class AuthError(TwythonError): class setup: - def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): + + def __init__(self, username=None, password=None, headers=None, proxy=None, + version=1): """setup(username = None, password = None, proxy = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). From 3580ee22b5b30f67a5621b1e9333640f64f54d78 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Tue, 17 Aug 2010 17:09:52 +0800 Subject: [PATCH 168/687] Don't need to include `setuptools`, heh. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index feb0be6..800c67e 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( include_package_data = True, # Package dependencies. - install_requires = ['setuptools', 'simplejson'], + install_requires = ['simplejson'], # Metadata for PyPI. author = 'Ryan McGrath', From e9aaaa7c39dad0306fec9e83cb377975f5c2d4d5 Mon Sep 17 00:00:00 2001 From: Juan Jose Conti Date: Tue, 24 Aug 2010 09:52:09 +0800 Subject: [PATCH 169/687] Added a method to return a generator based in Twitter search API. --- twython/core.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/twython/core.py b/twython/core.py index 61cb00e..c41ef12 100644 --- a/twython/core.py +++ b/twython/core.py @@ -1371,6 +1371,51 @@ class setup: except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + def searchTwitterGen(self, search_query, **kwargs): + """searchTwitterGen(search_query, **kwargs) + + Returns a generator of tweets that match a specified query. + + Parameters: + callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. + lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. + locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. + rpp - Optional. The number of tweets to return per page, up to a max of 100. + page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) + since_id - Optional. Returns tweets with status ids greater than the given id. + geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. + show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. + + Usage Notes: + Queries are limited 140 URL encoded characters. + Some users may be absent from search results. + The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. + This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. + + Applications must have a meaningful and unique User Agent when using this method. + An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than + applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + data = simplejson.load(self.opener.open(searchURL)) + except HTTPError, e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) + + if not data['results']: + raise StopIteration + + for tweet in data['results']: + yield tweet + + if 'page' not in kwargs: + kwargs['page'] = 2 + else: + kwargs['page'] += 1 + + for tweet in self.searchTwitterGen(search_query, **kwargs): + yield tweet + def getCurrentTrends(self, excludeHashTags = False, version = None): """getCurrentTrends(excludeHashTags = False, version = None) From 2a5d66880134a5dd20484cad30b39f146c42efaa Mon Sep 17 00:00:00 2001 From: Mark Liu Date: Sat, 11 Sep 2010 07:12:23 +0800 Subject: [PATCH 170/687] Added a missing parameter to addListMember --- twython/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/core.py b/twython/core.py index c41ef12..41bcff5 100644 --- a/twython/core.py +++ b/twython/core.py @@ -1701,20 +1701,20 @@ class setup: raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) - def addListMember(self, list_id, version = None): + def addListMember(self, list_id, id, version = None): """ addListMember(self, list_id, id, version) Adds a new Member (the passed in id) to the specified list. Parameters: list_id - Required. The slug of the list to add the new member to. - id - Required. The ID of the user that's being added to the list. + id - Required. The ID or slug of the user that's being added to the list. 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 = version or self.apiVersion if self.authenticated is True: try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % `id`)) + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % (id))) except HTTPError, e: raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) else: From ff46a85e134664d03a157b4aea3ed3b74fb59e12 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 14 Oct 2010 10:11:03 -0400 Subject: [PATCH 171/687] Fixing a bug in the 3k build per request from bsavas --- twython3k/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython3k/core.py b/twython3k/core.py index 1f5cf79..d86a17f 100644 --- a/twython3k/core.py +++ b/twython3k/core.py @@ -115,7 +115,7 @@ class setup: """ try: return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() - except HTTPError as e: + except HTTPError, e: raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) def constructApiURL(self, base_url, params): From 7ccf8a2bafa865d79df04cfd1ad14343a7fa2a69 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 14 Oct 2010 10:31:53 -0400 Subject: [PATCH 172/687] Bump version slightly --- setup.py | 2 +- twython/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 800c67e..60ea710 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.2' +__version__ = '1.2.1' setup( diff --git a/twython/core.py b/twython/core.py index 41bcff5..1d15c85 100644 --- a/twython/core.py +++ b/twython/core.py @@ -22,7 +22,7 @@ from urllib2 import HTTPError __author__ = "Ryan McGrath " -__version__ = "1.1" +__version__ = "1.2.1" try: From eb5541e4332ccf088f03c744014d55816ec17cc5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 16 Oct 2010 23:37:47 -0400 Subject: [PATCH 173/687] Twython 1.3, OAuth support is now finally included and working. Ships with an example Django application to get people started with OAuth, entire library is refactored to not be a royal clusterfsck. Enjoy. --- README.markdown | 72 +- oauth_django_example/__init__.py | 0 oauth_django_example/manage.py | 11 + oauth_django_example/settings.py | 94 + oauth_django_example/templates/tweets.html | 3 + oauth_django_example/test.db | Bin 0 -> 65536 bytes oauth_django_example/twitter/__init__.py | 0 oauth_django_example/twitter/models.py | 7 + .../twitter/twitter_endpoints.py | 299 +++ oauth_django_example/twitter/twython.py | 421 ++++ oauth_django_example/twitter/views.py | 73 + oauth_django_example/urls.py | 9 + setup.py | 8 +- twython/__init__.py | 2 +- twython/core.py | 1956 ----------------- twython/oauth.py | 524 ----- twython/twitter_endpoints.py | 299 +++ twython/twyauth.py | 85 - twython/twython.py | 417 ++++ twython3k/core.py | 1896 ---------------- twython3k/twitter_endpoints.py | 299 +++ twython3k/twython.py | 417 ++++ 22 files changed, 2400 insertions(+), 4492 deletions(-) create mode 100644 oauth_django_example/__init__.py create mode 100644 oauth_django_example/manage.py create mode 100644 oauth_django_example/settings.py create mode 100644 oauth_django_example/templates/tweets.html create mode 100644 oauth_django_example/test.db create mode 100644 oauth_django_example/twitter/__init__.py create mode 100644 oauth_django_example/twitter/models.py create mode 100644 oauth_django_example/twitter/twitter_endpoints.py create mode 100644 oauth_django_example/twitter/twython.py create mode 100644 oauth_django_example/twitter/views.py create mode 100644 oauth_django_example/urls.py delete mode 100644 twython/core.py delete mode 100644 twython/oauth.py create mode 100644 twython/twitter_endpoints.py delete mode 100644 twython/twyauth.py create mode 100644 twython/twython.py delete mode 100644 twython3k/core.py create mode 100644 twython3k/twitter_endpoints.py create mode 100644 twython3k/twython.py diff --git a/README.markdown b/README.markdown index 302ac4d..8357239 100644 --- a/README.markdown +++ b/README.markdown @@ -1,52 +1,76 @@ Twython - Easy Twitter utilities in Python ========================================================================================= -I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain -things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at -a library that offers more coverage. +Ah, Twitter, your API used to be so awesome, before you went and implemented the crap known +as OAuth 1.0. However, since you decided to force your entire development community over a barrel +about it, I suppose Twython has to support this. So, that said... -This is my first library I've ever written in Python, so there could be some stuff in here that'll -make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though, -and I'm open to anything that'll improve the library as a whole. +If you used this library and it all stopped working, it's because of the Authentication method change. +========================================================================================================= +Twitter recently disabled the use of "Basic Authentication", which is why, if you used Twython previously, +you probably started getting a ton of 401 errors. To fix this, we should note one thing... + +You need to change how authentication works in your program/application. If you're using a command line +application or something, you'll probably languish in hell for a bit, because OAuth wasn't really designed +for those types of use cases. Twython cannot help you with that or fix the annoying parts of OAuth. -OAuth and Streaming API support is in the works, but every other part of the Twitter API should be covered. Twython -handles both Basic (HTTP) Authentication and OAuth (Older versions (pre 0.9) of Twython need Basic Auth specified - -to override this, specify 'authtype="Basic"' in your twython.setup() call). - -Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or -Twitter's API Wiki (Twython calls mirror most of the methods listed there). +If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. +Enjoy! Requirements ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called -"simplejson". You can grab it at the following link: +"simplejson". Depending on your flavor of package manager, you can do the following... -> http://pypi.python.org/pypi/simplejson + (pip install | easy_install) simplejson + +Twython also requires the (most excellent) OAuth2 library for handling OAuth tokens/signing/etc. Again... + + (pip install | easy_install) oauth2 Installation ----------------------------------------------------------------------------------------------------- Installing Twython is fairly easy. You can... -> easy_install twython + (pip install | easy_install) twython ...or, you can clone the repo and install it the old fashioned way. -> git clone git://github.com/ryanmcgrath/twython.git -> cd twython -> sudo python setup.py install + git clone git://github.com/ryanmcgrath/twython.git + cd twython + sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- -> import twython -> -> twitter = twython.core.setup(username="example", password="example") -> twitter.updateStatus("See how easy this was?") + from twython import Twython + + twitter = Twython() + results = twitter.searchTwitter(q="bert") +A note about the development of Twython (specifically, 1.3) +---------------------------------------------------------------------------------------------------- +As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored +in a separate Python file, and the class itself catches calls to methods that match up in said table. + +Certain functions require a bit more legwork, and get to stay in the main file, but for the most part +it's all abstracted out. + +As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main +Twython class to import and use. If you need to catch exceptions, import those from twython as well. + +Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that +whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped +as a named argument to any Twitter function. + +For example: the search API looks for arguments under the name "q", so you pass q="query_here" to searchTwitter(). + +Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up +from you using them by this library. Twython 3k ----------------------------------------------------------------------------------------------------- There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed -to work, but it's provided so that others can grab it and hack on it. If you choose to try it out, -be aware of this. +to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. +If you choose to try it out, be aware of this. Questions, Comments, etc? diff --git a/oauth_django_example/__init__.py b/oauth_django_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth_django_example/manage.py b/oauth_django_example/manage.py new file mode 100644 index 0000000..5e78ea9 --- /dev/null +++ b/oauth_django_example/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +try: + import settings # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/oauth_django_example/settings.py b/oauth_django_example/settings.py new file mode 100644 index 0000000..8ceb7a5 --- /dev/null +++ b/oauth_django_example/settings.py @@ -0,0 +1,94 @@ +import os.path + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Ryan McGrath', 'ryan@venodesigns.net'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': os.path.join(os.path.dirname(__file__), 'test.db'), # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + + +AUTH_PROFILE_MODULE = 'twitter.Profile' + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/New_York' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash if there is a path component (optional in other cases). +# Examples: "http://media.lawrence.com", "http://example.com/media/" +MEDIA_URL = '' + +# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a +# trailing slash. +# Examples: "http://foo.com/media/", "/media/". +ADMIN_MEDIA_PREFIX = '/media/' + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '&!_t1t^gmenaid9mkmkuw=4nthj7f)o+!@$#ipfp*s11380t*)' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'twython_testing.urls' + +TEMPLATE_DIRS = ( + os.path.join(os.path.dirname(__file__), 'templates'), +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'twitter', +) diff --git a/oauth_django_example/templates/tweets.html b/oauth_django_example/templates/tweets.html new file mode 100644 index 0000000..a2ec453 --- /dev/null +++ b/oauth_django_example/templates/tweets.html @@ -0,0 +1,3 @@ +{% for tweet in tweets %} + {{ tweet.text }} +{% endfor %} diff --git a/oauth_django_example/test.db b/oauth_django_example/test.db new file mode 100644 index 0000000000000000000000000000000000000000..3082a95365c31ec03e2a58e9352abe89008a2011 GIT binary patch literal 65536 zcmeI5Uu+{uTEM&P?yihyGXD~fvy+{ho@A5VC^I{5JC5yLt#&gRCzF4ki5)w(B`|Gw zJ8@#iUfYw&%q=RAf<0y#3AaYy#?s6?_{_=mLM75qQ=hvH`?kL!QMN%Sq(H_;c+7mnBH%N+HT z?2BDSLU~U&4w~;*$a%?YcUv`>qP~x{Ylz3`&Rk@Q`TANE` zm6cfbswu{-=_Sj{nGyM30!kvb5KF}3^RXm1gUJ#DsO~AVek#7So{EvIww|lXnAPNz zXX;&#>`PvIGB1;b0~@Dv^5}JpeRFf@VYZ7NQ&=||)pE00uI)KEc9#r1ifFgsr_I*3 z?kNG=6`oAlmZgmo)X4$aw>9@<)ZM*yLhmj+ad=9?zAIPIVbR9V&J`P%EGGBB%L<+q z=blzI#*}@nQQX!VZ$$i4_P92OnP(~}%f7`ckL@E<&HKdS_~?=w`=U|wFxAzEyv@)Y zK4x7fy3yH~>Oa<+ zb?57Dl^`zGcmss)J1*I`KK)p_c9%I;mOx2dHSd~YNc|;7%-E0o}Bbqa%9%t+p^`%WLr+TAPLiGoVKUwy44toTM}MNf@^G& z#Wm*YXj~b#B)Bdn!4=W?#UV+UK$qHDSTR*WBa`PXOWW-mY~?7oRhY@ibCz6N3zusv zGLy<#%Np9FCKqDr%=q;)lJGWC9Sr$Yq0k9P;%a#w&ANWts=!ruRN(p@6{ZFyA%v#e z{q2yZhB!ngPg&jK-LDcWwJk&BcJSonNlT8~(3a!sc~bFobiXY`wYDWrSQ1<#U0u4l zIvQ67ED5fQNpM9pei3(^P*zM;(8!o93D?ks120?UxVQqA7?Ug+uECVyDp=xGQ4(H9 zm)j6c0jh+>uDT^5jIKMh*t0d_s_dMrE=!v0v&#@yYUfNMi#5@w*ae8Hx04=n{f@}v z0{%DrH~5?Qckq|-&*6Qn;{<*WPvZ+%kpD&gOZkuF-;{q@eki{$=jAmyLMp%k1b_e# z00J*NfngC%p{}*rh?&`*cFgsg@0^In(EyuZ%$y4>$GOn*nmN28T|5n+7SSbyX)T%@ zVl8(qoX(q>?ONs<=p1WGW);nxvm+u>QIFNn8MA=bcPw2_dqp&jB-(}}caW*;TD_by zGkF_)A##%0Akz9g$HQ`*OOX?14zEa;7Xz%zZd!{b@p%zVppM0bY-aH~Y_%Yr5z%Gj zqLo-$^oi&?I=)VDoB6z|J=43G{aw>PqR;>2OMf_k01yBIKmZ5;fj%NYpZ{V0@1qkA zT?GO_00;m9AV3Jf^FIs&AOHk_01yBI{YL%AfdCKy0zd!= z(C2@IcLef*0|)>CAOHk_01)`p2^VuxE)HjRYg~`5)o`6vzh-AOHk_01yBIK;R`M zaK=5397_+j|KAChTX4DE!|tTm6#qoJEqzY@J?zGR44!#j=pgun{(qj^E5>s-VDB;6 z=WaFFp6_n(31hoy`jPvDc`2L-fgdVGv3%ap9dW4#onAxdN8^X8pPi`D@Bd}@hl2Y< z_xGi*OW&7&3x7i%l$YiI7CscdIuwVo@11FMc#V0%>+XL~-*j zIZ-CLUJ|S`mD*N$Z%om(meDF#jcz40KfgoVCK|-nV?v&(PqNO64PxKK1Ug!?J&*DI zzS$7q@v}Wf0WbO_1A*KHpWDYFj9lWC4o2>aAGtj|h#qtL$?uR3pPysa<Em)ujeaT!EoG6 zdx=$v8^6bkSPLS%MY1F^>k;37u`{wS8+?@SzaCx9t35K$!|(sCMF&KH01yBIKmZ8z z4FNj;58x>Q{{#LD{0;nT#P?qo$4CYoKmZ5;0U!VbfPf%eBo2i~M@PAD>ejt-t7SBJ z>O65J?Dcu6`$G0dgHhr_c=XaJ_ubt7$N#@Ud%sU{>W06b55%3l775nRNT4JZ@@BapHS-{`J z-@^Zn{|bK%|04cbk^u)000KY&2mk>f@caaZ#L-dIxojYAfPG$Mtr+mIcxe=MtrX-} z#ffp`SP(cD2hv&5JBsLp&KCvZpy>6o47wh0pBBA7mSQUb*O2JF!jks%&wl@hq;Ct- zx5e$}?6@A5N->kJ6oZM9z8G!fHbW$Tsg_PG?`Jb>=_>0~8Y zSk$ZW#oWEz&G-!uF(zhI7IqTqLT#rcBr4HZpU8ls|M&-`(Df2Xc3kyW0okd_EQ}CF}XL&{AVO zPq0QML$bt&q)J%b&jgmqAR$AunmCwKQ}x_peY;TA_gAa&eNSO8abMe9y2~oehsxQ_ zI33%&#rvUnA&{sPVs_;l3#oX94Gubr;+0}0TF>2zEhO(PsH;0$0Z+V|uC6V{)Y}`& zmF$CEa#ogi<2O??+3M2F+UC+sZZR9kZe$NU?f3tJe#e`Ev$H`p=)r$S-v9eZz#owS z4j=#ofB+Bx0zlx$n80i9HT0yFw2Rn35ssFkA+;2ojp_kCT+ky%q^RqWh!!YlGpbQA zpa1bc3FHF@5C8%|00;m9An+0qxQIkyeB3zDs`ZLNw-<6m&HewrD&ViaL>-2%00AHX z1b_e#_=zU)Gvvp6=>4BUYA6y6hJ)d2i<%bNO&IfqQmWQShGREwtu9rTTFt%r2g{*_ zVkFthB+KdLR%Cr^e``+L4zH){w^FkY79(q^mF3mhh;fk3-OeOaw;tThF0Hg?=$;$OnQfRFG8u<$es2Wcpt zV&O>^dT8iA!NLI+Vj8++7D_A>X^7k`bg>Z8`Ck}#!4NfJV6dV&q@dXKh&nMA=KzBz(@$vJeG>N0^z*sqf`^Qz%jXNu&9 z-1I!K(r%GgA7xgzpw(8lf!%@>mu-W|>i z>|2SV!PCXbrzDQJqkI-xNv^gxNmPJIpf^t@=juDZ7z{jR$^Jcb7Z>?+0RiMQ#v+>;}XU} zz1%SJbi>D<+djU1PWIiNdcuZjZ8hz^(s+8<+K}|JbNe~^>Q=jtBNT<*C)wiC+G*48pu_vyxfNZ=AwYJ^6Nj*GdKx`^mzmpjPwh%x z=rp^l^jS@BkL%;ze46e5YV0y>)piX>?*l!0ZwfYzV#8?JoH`^uQ$HrV#~!}oHLviu zA9sa!OeM#z%MpH(oLkx+6Y4ds9225fxS2`sKJfhST;4z&2mk>f00e+Qe-MEAzdz11 z^b!aF0U!VbfPj+#%>Par5C;N400;m9AkZHKVE*rq^9;QN0zd!=00AK2BmncjlLo|r z01yBIKmZ8z2LYJ>`{O)AFM$9M00KY&2sjDQ`5)nr$^JjiT!;e!AOHk_01yBIFDC(W H|DXQ@eXup& literal 0 HcmV?d00001 diff --git a/oauth_django_example/twitter/__init__.py b/oauth_django_example/twitter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth_django_example/twitter/models.py b/oauth_django_example/twitter/models.py new file mode 100644 index 0000000..3a07664 --- /dev/null +++ b/oauth_django_example/twitter/models.py @@ -0,0 +1,7 @@ +from django.db import models +from django.contrib.auth.models import User + +class Profile(models.Model): + user = models.ForeignKey(User) + oauth_token = models.CharField(max_length = 200) + oauth_secret = models.CharField(max_length = 200) diff --git a/oauth_django_example/twitter/twitter_endpoints.py b/oauth_django_example/twitter/twitter_endpoints.py new file mode 100644 index 0000000..42bd5a1 --- /dev/null +++ b/oauth_django_example/twitter/twitter_endpoints.py @@ -0,0 +1,299 @@ +""" + 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: + + {{version}}, etc + + 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. + + 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). +""" + +# Base Twitter API url, no need to repeat this junk... +base_url = 'http://api.twitter.com/{{version}}' + +api_table = { + 'getRateLimitStatus': { + 'url': '/account/rate_limit_status.json', + 'method': 'GET', + }, + + # Timeline methods + 'getPublicTimeline': { + 'url': '/statuses/public_timeline.json', + 'method': 'GET', + }, + 'getHomeTimeline': { + 'url': '/statuses/home_timeline.json', + 'method': 'GET', + }, + 'getUserTimeline': { + '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', + }, + 'destroyFriendship': { + 'url': '/friendships/destroy.json', + 'method': 'POST', + }, + 'getFriendsIDs': { + 'url': '/friends/ids.json', + 'method': 'GET', + }, + 'getFollowersIDs': { + 'url': '/followers/ids.json', + 'method': 'GET', + }, + + # Retweets + 'reTweet': { + 'url': '/statuses/retweet/{{id}}.json', + 'method': 'POST', + }, + 'getRetweets': { + 'url': '/statuses/retweets/{{id}}.json', + 'method': 'GET', + }, + 'retweetedOfMe': { + 'url': '/statuses/retweets_of_me.json', + 'method': 'GET', + }, + 'retweetedByMe': { + 'url': '/statuses/retweeted_by_me.json', + 'method': 'GET', + }, + 'retweetedToMe': { + 'url': '/statuses/retweeted_to_me.json', + 'method': 'GET', + }, + + # User methods + 'showUser': { + 'url': '/users/show.json', + 'method': 'GET', + }, + 'searchUsers': { + 'url': '/users/search.json', + 'method': 'GET', + }, + + # Status methods - showing, updating, destroying, etc. + 'showStatus': { + 'url': '/statuses/show/{{id}}.json', + 'method': 'GET', + }, + 'updateStatus': { + 'url': '/statuses/update.json', + 'method': 'POST', + }, + 'destroyStatus': { + 'url': '/statuses/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Direct Messages - getting, sending, effing, etc. + 'getDirectMessages': { + 'url': '/direct_messages.json', + 'method': 'GET', + }, + 'getSentMessages': { + 'url': '/direct_messages/sent.json', + 'method': 'GET', + }, + 'sendDirectMessage': { + 'url': '/direct_messages/new.json', + 'method': 'POST', + }, + 'destroyDirectMessage': { + 'url': '/direct_messages/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Friendship methods + 'checkIfFriendshipExists': { + 'url': '/friendships/exists.json', + 'method': 'GET', + }, + 'showFriendship': { + 'url': '/friendships/show.json', + 'method': 'GET', + }, + + # Profile methods + 'updateProfile': { + 'url': '/account/update_profile.json', + 'method': 'POST', + }, + 'updateProfileColors': { + 'url': '/account/update_profile_colors.json', + 'method': 'POST', + }, + + # Favorites methods + 'getFavorites': { + 'url': '/favorites.json', + 'method': 'GET', + }, + 'createFavorite': { + 'url': '/favorites/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyFavorite': { + 'url': '/favorites/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Blocking methods + 'createBlock': { + 'url': '/blocks/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyBlock': { + 'url': '/blocks/destroy/{{id}}.json', + 'method': 'POST', + }, + 'getBlocking': { + 'url': '/blocks/blocking.json', + 'method': 'GET', + }, + 'getBlockedIDs': { + 'url': '/blocks/blocking/ids.json', + 'method': 'GET', + }, + 'checkIfBlockExists': { + 'url': '/blocks/exists.json', + 'method': 'GET', + }, + + # Trending methods + 'getCurrentTrends': { + 'url': '/trends/current.json', + 'method': 'GET', + }, + 'getDailyTrends': { + 'url': '/trends/daily.json', + 'method': 'GET', + }, + 'getWeeklyTrends': { + 'url': '/trends/weekly.json', + 'method': 'GET', + }, + 'availableTrends': { + 'url': '/trends/available.json', + 'method': 'GET', + }, + 'trendsByLocation': { + 'url': '/trends/{{woeid}}.json', + 'method': 'GET', + }, + + # Saved Searches + 'getSavedSearches': { + 'url': '/saved_searches.json', + 'method': 'GET', + }, + 'showSavedSearch': { + 'url': '/saved_searches/show/{{id}}.json', + 'method': 'GET', + }, + 'createSavedSearch': { + 'url': '/saved_searches/create.json', + 'method': 'GET', + }, + 'destroySavedSearch': { + 'url': '/saved_searches/destroy/{{id}}.json', + 'method': 'GET', + }, + + # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P + 'createList': { + 'url': '/{{username}}/lists.json', + 'method': 'POST', + }, + 'updateList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'POST', + }, + 'showLists': { + 'url': '/{{username}}/lists.json', + 'method': 'GET', + }, + 'getListMemberships': { + 'url': '/{{username}}/lists/followers.json', + 'method': 'GET', + }, + 'deleteList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'DELETE', + }, + 'getListTimeline': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'getSpecificList': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'addListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'POST', + }, + 'getListMembers': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'GET', + }, + 'deleteListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'DELETE', + }, + 'subscribeToList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'POST', + }, + 'unsubscribeFromList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'DELETE', + }, + + # The one-offs + 'notificationFollow': { + 'url': '/notifications/follow/follow.json', + 'method': 'POST', + }, + 'notificationLeave': { + 'url': '/notifications/leave/leave.json', + 'method': 'POST', + }, + 'updateDeliveryService': { + 'url': '/account/update_delivery_device.json', + 'method': 'POST', + }, + 'reportSpam': { + 'url': '/report_spam.json', + 'method': 'POST', + }, +} diff --git a/oauth_django_example/twitter/twython.py b/oauth_django_example/twitter/twython.py new file mode 100644 index 0000000..eb96be7 --- /dev/null +++ b/oauth_django_example/twitter/twython.py @@ -0,0 +1,421 @@ +#!/usr/bin/python + +""" + Twython is a library for Python that wraps the Twitter API. + It aims to abstract away all the API endpoints, so that additions to the library + and/or the Twitter API won't cause any overall problems. + + Questions, comments? ryan@venodesigns.net +""" + +__author__ = "Ryan McGrath " +__version__ = "1.3" + +import urllib +import urllib2 +import urlparse +import httplib +import httplib2 +import mimetypes +import mimetools +import re + +import oauth2 as oauth + +# 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 urllib2 import HTTPError + +# There are some special setups (like, oh, a Django application) where +# 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 +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 + 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. + from django.utils import simplejson + except: + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +class TwythonError(Exception): + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by APILimit and AuthError. + + Note: To use these, the syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: + + from twython import TwythonError, APILimit, AuthError + """ + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + + 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 + 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 __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 + your authentication. + """ + def __init__(self, msg): + self.msg = msg + + 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, headers=None): + """setup(self, oauth_token = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + twitter_token - Given to you when you register your application with Twitter. + twitter_secret - Given to you when you register your application with Twitter. + oauth_token - If you've gone through the authentication process and have a token for this user, + pass it in and it'll be used for all requests going forward. + oauth_token_secret - see oauth_token; it's the other half. + headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + + ** 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.twitter_token = twitter_token + self.twitter_secret = twitter_secret + self.oauth_token = oauth_token + self.oauth_secret = oauth_token_secret + + # If there's headers, set them, otherwise be an embarassing parent for their own good. + self.headers = headers + if self.headers is None: + headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + + consumer = None + token = None + + if self.twitter_token is not None and self.twitter_secret is not None: + consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + + if self.oauth_token is not None and self.oauth_secret is not None: + token = oauth.Token(oauth_token, oauth_token_secret) + + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. + if consumer is not None and token is not None: + self.client = oauth.Client(consumer, token) + elif consumer is not None: + self.client = oauth.Client(consumer) + else: + # If they don't do authentication, but still want to request unprotected resources, we need an opener. + self.client = httplib2.Http() + + def __getattr__(self, api_call): + """ + The most magically awesome block of code you'll see in 2010. + + Rather than list out 9 million damn methods for this API, we just keep a table (see above) of + every API endpoint and their corresponding function id for this library. This pretty much gives + unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is + going to be your bottleneck... well, don't use Python. ;P + + For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). + It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, + we can take over and find the API method in our table. We then return a function that downloads and parses + what we're looking for, based on the keywords passed in. + + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". + """ + def get(self, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) + + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs)) + else: + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) + resp, content = self.client.request(url, fn['method']) + + return simplejson.loads(content) + + if api_call in api_table: + return get.__get__(self) + else: + raise AttributeError, api_call + + def get_authentication_tokens(self): + """ + get_auth_url(self) + + Returns an authorization URL for a user to hit. + """ + resp, content = self.client.request(self.request_token_url, "GET") + + if resp['status'] != '200': + raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + + request_tokens = dict(urlparse.parse_qsl(content)) + request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + return request_tokens + + def get_authorized_tokens(self): + """ + get_authorized_tokens + + Returns authorized tokens after they go through the auth_url phase. + """ + resp, content = self.client.request(self.access_token_url, "GET") + return dict(urlparse.parse_qsl(content)) + + + @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: + resp, content = self.client.request( + shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)}), + "GET" + ) + 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 = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + + 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. + + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + version = version or self.apiVersion + if self.authenticated is True: + apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += `id` + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + return simplejson.load(self.opener.open(apiURL)) + except HTTPError, e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) + else: + raise AuthError("bulkUserLookup() requires you to be authenticated.") + + def searchTwitter(self, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + return simplejson.load(self.opener.open(searchURL)) + except HTTPError, e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def searchTwitterGen(self, **kwargs): + """searchTwitterGen(search_query, **kwargs) + + Returns a generator of tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + data = simplejson.load(self.opener.open(searchURL)) + except HTTPError, e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) + + if not data['results']: + raise StopIteration + + for tweet in data['results']: + yield tweet + + if 'page' not in kwargs: + kwargs['page'] = 2 + else: + kwargs['page'] += 1 + + for tweet in self.searchTwitterGen(search_query, **kwargs): + yield tweet + + def isListMember(self, list_id, id, username, version = None): + """ isListMember(self, list_id, id, version) + + 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. + + 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. + """ + version = version or self.apiVersion + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`))) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def isListSubscriber(self, list_id, id, version = None): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + 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. + """ + version = version or self.apiVersion + try: + return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`))) + except HTTPError, e: + 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. + def updateProfileBackgroundImage(self, filename, tile="true", version = None): + """ updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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 = version or self.apiVersion + if self.authenticated is True: + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = self.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("You realize you need to be authenticated to change a background image, right?") + + def updateProfileImage(self, filename, version = None): + """ updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ + version = version or self.apiVersion + if self.authenticated is True: + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = Twython.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) + return self.opener.open(r).read() + except HTTPError, e: + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + else: + raise AuthError("You realize you need to be authenticated to change a profile image, right?") + + @staticmethod + def encode_multipart_formdata(fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % Twython.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + @staticmethod + def get_content_type(filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + @staticmethod + def unicode2utf8(text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text diff --git a/oauth_django_example/twitter/views.py b/oauth_django_example/twitter/views.py new file mode 100644 index 0000000..14d8e69 --- /dev/null +++ b/oauth_django_example/twitter/views.py @@ -0,0 +1,73 @@ +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.models import User +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response +from django.contrib.auth.decorators import login_required + +from twython import Twython +from twitter.models import Profile + +CONSUMER_KEY = "piKE9TwKoAhJoj7KEMlwGQ" +CONSUMER_SECRET = "RA9IzvvzoLAFGOOoOndm1Cvyh94pwPWLy4Grl4dt0o" + +def twitter_logout(request): + logout(request) + return HttpResponseRedirect('/') + +def twitter_begin_auth(request): + # Instantiate Twython with the first leg of our trip. + twitter = Twython( + twitter_token = CONSUMER_KEY, + twitter_secret = CONSUMER_SECRET + ) + + # Request an authorization url to send the user to... + auth_props = twitter.get_authentication_tokens() + + # Then send them over there, durh. + request.session['request_token'] = auth_props + return HttpResponseRedirect(auth_props['auth_url']) + +def twitter_thanks(request): + # Now that we've got the magic tokens back from Twitter, we need to exchange + # for permanent ones and store them... + twitter = Twython( + twitter_token = CONSUMER_KEY, + twitter_secret = CONSUMER_SECRET, + oauth_token = request.session['request_token']['oauth_token'], + oauth_token_secret = request.session['request_token']['oauth_token_secret'] + ) + + # Retrieve the tokens we want... + authorized_tokens = twitter.get_authorized_tokens() + + # If they already exist, grab them, login and redirect to a page displaying stuff. + try: + user = User.objects.get(username = authorized_tokens['screen_name']) + except User.DoesNotExist: + # We mock a creation here; no email, password is just the token, etc. + user = User.objects.create_user(authorized_tokens['screen_name'], "fjdsfn@jfndjfn.com", authorized_tokens['oauth_token_secret']) + profile = Profile() + profile.user = user + profile.oauth_token = authorized_tokens['oauth_token'] + profile.oauth_secret = authorized_tokens['oauth_token_secret'] + profile.save() + + user = authenticate( + username = authorized_tokens['screen_name'], + password = authorized_tokens['oauth_token_secret'] + ) + login(request, user) + return HttpResponseRedirect('/timeline') + +def twitter_timeline(request): + user = request.user.get_profile() + twitter = Twython( + twitter_token = CONSUMER_KEY, + twitter_secret = CONSUMER_SECRET, + oauth_token = user.oauth_token, + oauth_token_secret = user.oauth_secret + ) + my_tweets = twitter.getHomeTimeline() + print my_tweets + return render_to_response('tweets.html', {'tweets': my_tweets}) diff --git a/oauth_django_example/urls.py b/oauth_django_example/urls.py new file mode 100644 index 0000000..9344f4d --- /dev/null +++ b/oauth_django_example/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import * +from twitter.views import twitter_begin_auth, twitter_thanks, twitter_logout, twitter_timeline + +urlpatterns = patterns('', + (r'^login/?$', twitter_begin_auth), + (r'^/logout?$', twitter_logout), + (r'^thanks/?$', twitter_thanks), # Where they're redirect to after authorizing + (r'^timeline/?$', twitter_timeline), +) diff --git a/setup.py b/setup.py index 60ea710..505eea7 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,10 @@ import sys, os from setuptools import setup from setuptools import find_packages - __author__ = 'Ryan McGrath ' -__version__ = '1.2.1' - +__version__ = '1.3' setup( - # Basic package information. name = 'twython', version = __version__, @@ -20,7 +17,7 @@ setup( include_package_data = True, # Package dependencies. - install_requires = ['simplejson'], + install_requires = ['simplejson', 'oauth2'], # Metadata for PyPI. author = 'Ryan McGrath', @@ -38,5 +35,4 @@ setup( 'Topic :: Communications :: Chat', 'Topic :: Internet' ] - ) diff --git a/twython/__init__.py b/twython/__init__.py index 00deffe..59aac86 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1 +1 @@ -import core, twyauth, streaming +from twython import Twython diff --git a/twython/core.py b/twython/core.py deleted file mode 100644 index 1d15c85..0000000 --- a/twython/core.py +++ /dev/null @@ -1,1956 +0,0 @@ -#!/usr/bin/python - -""" - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - - -import httplib -import urllib -import urllib2 -import mimetypes -import mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - - -__author__ = "Ryan McGrath " -__version__ = "1.2.1" - - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - - -class TwythonError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - - -class APILimit(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - - -class AuthError(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - - -class setup: - - def __init__(self, username=None, password=None, headers=None, proxy=None, - version=1): - """setup(username = None, password = None, proxy = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - username - Your Twitter username, if you want Basic (HTTP) Authentication. - password - Password for your twitter account, if you want Basic (HTTP) Authentication. - headers - User agent header. - proxy - An object detailing information, in case proxy use/authentication is required. Object passed should be something like... - - proxyobj = { - "username": "fjnfsjdnfjd", - "password": "fjnfjsjdfnjd", - "host": "http://fjanfjasnfjjfnajsdfasd.com", - "port": 87 - } - - version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. - - ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. - """ - self.authenticated = False - self.username = username - self.apiVersion = version - self.proxy = proxy - self.headers = headers - if self.proxy is not None: - self.proxyobj = urllib2.ProxyHandler({'http': 'http://%s:%s@%s:%d' % (self.proxy["username"], self.proxy["password"], self.proxy["host"], self.proxy["port"])}) - # Check and set up authentication - if self.username is not None and password is not None: - # Assume Basic authentication ritual - self.auth_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) - self.handler = urllib2.HTTPBasicAuthHandler(self.auth_manager) - if self.proxy is not None: - self.opener = urllib2.build_opener(self.proxyobj, self.handler) - else: - self.opener = urllib2.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - self.authenticated = True # Play nice, people can force-check using verifyCredentials() - else: - # Build a non-auth opener so we can allow proxy-auth and/or header swapping - if self.proxy is not None: - self.opener = urllib2.build_opener(self.proxyobj) - else: - self.opener = urllib2.build_opener() - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - - # URL Shortening function huzzah - def shortenURL(self, 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: - return urllib2.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() - except HTTPError, e: - raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) - - def verifyCredentials(self, version = None): - """ verifyCredentials(self, version = None): - - Verifies the authenticity of the passed in credentials. Used to be a forced call, now made optional - (no need to waste network resources) - - Parameters: - None - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % version)) - except HTTPError, e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % `e.code`) - else: - raise AuthError("verifyCredentials() requires you to actually, y'know, pass in credentials.") - - def getRateLimitStatus(self, checkRequestingIP = True, version = None): - """getRateLimitStatus() - - Returns the remaining number of API requests available to the requesting user before the - API limit is reached for the current hour. Calls to rate_limit_status do not count against - the rate limit. If authentication credentials are provided, the rate limit status for the - authenticating user is returned. Otherwise, the rate limit status for the requesting - IP address is returned. - - Params: - checkRequestIP - Boolean, defaults to True. Set to False to check against the currently requesting IP, instead of the account level. - 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 = version or self.apiVersion - try: - if checkRequestingIP is True: - return simplejson.load(urllib2.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - raise TwythonError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError, e: - raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % `e.code`, e.code) - - def getPublicTimeline(self, version = None): - """getPublicTimeline() - - Returns the 20 most recent statuses from non-protected users who have set a custom user icon. - The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. - - Params: - 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 = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) - except HTTPError, e: - raise TwythonError("getPublicTimeline() failed with a %s error code." % `e.code`) - - def getHomeTimeline(self, version = None, **kwargs): - """getHomeTimeline(**kwargs) - - Returns the 20 most recent statuses, including retweets, posted by the authenticating user - and that user's friends. This is the equivalent of /timeline/home on the Web. - - Usage note: This home_timeline is identical to statuses/friends_timeline, except it also - contains retweets, which statuses/friends_timeline does not (for backwards compatibility - reasons). In a future version of the API, statuses/friends_timeline will go away and - be replaced by home_timeline. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(homeTimelineURL)) - except HTTPError, e: - raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % `e.code`) - else: - raise AuthError("getHomeTimeline() requires you to be authenticated.") - - def getFriendsTimeline(self, version = None, **kwargs): - """getFriendsTimeline(**kwargs) - - Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. - This is the equivalent of /timeline/home on the Web. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError, e: - raise TwythonError("getFriendsTimeline() failed with a %s error code." % `e.code`) - else: - raise AuthError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, version = None, **kwargs): - """getUserTimeline(id = None, **kwargs) - - Returns the 20 most recent statuses posted from the authenticating user. It's also - possible to request another user's timeline via the id parameter. This is the - equivalent of the Web / page for your own user, or the profile page for a third party. - - Parameters: - id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. - user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. - screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if id is not None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, `id`), kwargs) - elif id is None and kwargs.has_key("user_id") is False and kwargs.has_key("screen_name") is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) - else: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(self.opener.open(userTimelineURL)) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % `e.code`, e.code) - - def getUserMentions(self, version = None, **kwargs): - """getUserMentions(**kwargs) - - Returns the 20 most recent mentions (status containing @username) for the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError, e: - raise TwythonError("getUserMentions() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getUserMentions() requires you to be authenticated.") - - def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): - """reportSpam(self, id), user_id, screen_name): - - Report a user account to Twitter as a spam account. *One* of the following parameters is required, and - this requires that you be authenticated with a user account. - - Parameters: - id - Optional. The ID or screen_name of the user you want to report as a spammer. - user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. - if id is not None or user_id is not None or screen_name is not None: - try: - apiExtension = "" - if id is not None: - apiExtension = "id=%s" % id - if user_id is not None: - apiExtension = "user_id=%s" % `user_id` - if screen_name is not None: - apiExtension = "screen_name=%s" % screen_name - return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) - except HTTPError, e: - raise TwythonError("reportSpam() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") - else: - raise AuthError("reportSpam() requires you to be authenticated.") - - def reTweet(self, id, version = None): - """reTweet(id) - - Retweets a tweet. Requires the id parameter of the tweet you are retweeting. - - Parameters: - id - Required. The numerical ID of the tweet you are retweeting. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, `id`), "POST")) - except HTTPError, e: - raise TwythonError("reTweet() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - - def getRetweets(self, id, count = None, version = None): - """ getRetweets(self, id, count): - - Returns up to 100 of the first retweets of a given tweet. - - Parameters: - id - Required. The numerical ID of the tweet you want the retweets of. - count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, `id`) - if count is not None: - apiURL += "?count=%s" % `count` - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getRetweets failed with a %s eroror code." % `e.code`, e.code) - else: - raise AuthError("getRetweets() requires you to be authenticated.") - - def retweetedOfMe(self, version = None, **kwargs): - """retweetedOfMe(**kwargs) - - Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedOfMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedOfMe() requires you to be authenticated.") - - def retweetedByMe(self, version = None, **kwargs): - """retweetedByMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedByMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedByMe() requires you to be authenticated.") - - def retweetedToMe(self, version = None, **kwargs): - """retweetedToMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user's friends. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError, e: - raise TwythonError("retweetedToMe() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("retweetedToMe() requires you to be authenticated.") - - def searchUsers(self, q, per_page = 20, page = 1, version = None): - """ searchUsers(q, per_page = None, page = None): - - Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) - - Parameters: - q (string) - Required. The query you wanna search against; self explanatory. ;) - per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) - page (number) - Optional, defaults to 1. The page of users you want to pull 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) - except HTTPError, e: - raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("searchUsers(), oddly, requires you to be authenticated.") - - def showUser(self, id = None, user_id = None, screen_name = None, version = None): - """showUser(id = None, user_id = None, screen_name = None) - - Returns extended information of a given user. The author's most recent status will be returned inline. - - Parameters: - ** Note: One of the following must always be specified. - id - The ID or screen name of a user. - user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - 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. - - Usage Notes: - Requests for protected users without credentials from - 1) the user requested or - 2) a user that is following the protected user will omit the nested status element. - - ...will result in only publicly available data being returned. - """ - version = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) - if apiURL != "": - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("showUser() failed with a %s error code." % `e.code`, e.code) - - def bulkUserLookup(self, ids = None, screen_names = None, version = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - - 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. - - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - version = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += `id` + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("bulkUserLookup() requires you to be authenticated.") - - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") - - Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. - (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access - older friends. With no user specified, the request defaults to the authenticated users friends. - - It's also possible to request another user's friends list via the id, screen_name or user_id parameter. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, or screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of friends. - user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) - try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % `page`)) - else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) - except HTTPError, e: - raise TwythonError("getFriendsStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFriendsStatus() requires you to be authenticated.") - - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns the authenticating user's followers, each with current status inline. - They are ordered by the order in which they joined Twitter, 100 at a time. - (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) - - Use the page option to access earlier followers. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of followers. - user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) - try: - if apiURL.find("?") == -1: - apiURL += "?" - else: - apiURL += "&" - if page is not None: - return simplejson.load(self.opener.open(apiURL + "page=%s" % page)) - else: - return simplejson.load(self.opener.open(apiURL + "cursor=%s" % cursor)) - except HTTPError, e: - raise TwythonError("getFollowersStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFollowersStatus() requires you to be authenticated.") - - def showStatus(self, id, version = None): - """showStatus(id) - - Returns a single status, specified by the id parameter below. - The status's author will be returned inline. - - Parameters: - id - Required. The numerical ID of the status to retrieve. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - except HTTPError, e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % `e.code`, e.code) - - def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): - """updateStatus(status, in_reply_to_status_id = None) - - Updates the authenticating user's status. Requires the status parameter specified below. - A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. - - Parameters: - status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. - in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - latitude (string) - Optional. The location's latitude that this tweet refers to. - longitude (string) - Optional. The location's longitude that this tweet refers 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. - - ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references - is mentioned within the status text. Therefore, you must include @username, where username is - the author of the referenced tweet, within the update. - - ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. - This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. - """ - version = version or self.apiVersion - try: - postExt = urllib.urlencode({"status": self.unicode2utf8(status)}) - if latitude is not None and longitude is not None: - postExt += "&lat=%s&long=%s" % (latitude, longitude) - if in_reply_to_status_id is not None: - postExt += "&in_reply_to_status_id=%s" % `in_reply_to_status_id` - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, postExt)) - except HTTPError, e: - raise TwythonError("updateStatus() failed with a %s error code." % `e.code`, e.code) - - def destroyStatus(self, id, version = None): - """destroyStatus(id) - - Destroys the status specified by the required ID parameter. - The authenticating user must be the author of the specified status. - - Parameters: - id - Required. The ID of the status to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/destroy/%s.json?" % (version, id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("destroyStatus() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyStatus() requires you to be authenticated.") - - def endSession(self, version = None): - """endSession() - - Ends the session of the authenticating user, returning a null cookie. - Use this method to sign users out of client-facing applications (widgets, etc). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") - self.authenticated = False - except HTTPError, e: - raise TwythonError("endSession failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent to the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, `page`) - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getDirectMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getSentMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, `page`) - if since_id is not None: - apiURL += "&since_id=%s" % `since_id` - if max_id is not None: - apiURL += "&max_id=%s" % `max_id` - if count is not None: - apiURL += "&count=%s" % `count` - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getSentMessages() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text, version = None): - """sendDirectMessage(user, text) - - Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. - Returns the sent message in the requested format when successful. - - Parameters: - user - Required. The ID or screen name of the recipient user. - text - Required. The text of your direct message. Be sure to keep it under 140 characters. - 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 = version or self.apiVersion - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.urlencode({"user": user, "text": text})) - except HTTPError, e: - raise TwythonError("sendDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise TwythonError("Your message must not be longer than 140 characters") - else: - raise AuthError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id, version = None): - """destroyDirectMessage(id) - - Destroys the direct message specified in the required ID parameter. - The authenticating user must be the recipient of the specified direct message. - - Parameters: - id - Required. The ID of the direct message to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") - except HTTPError, e: - raise TwythonError("destroyDirectMessage() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): - """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") - - Allows the authenticating users to follow the user specified in the ID parameter. - Returns the befriended user in the requested format when successful. Returns a - string describing the failure condition when unsuccessful. If you are already - friends with the user an HTTP 403 will be returned. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to befriend. - user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. - follow - Optional. Enable notifications for the target user in addition to becoming friends. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "user_id=%s&follow=%s" %(`user_id`, follow) - if screen_name is not None: - apiURL = "screen_name=%s&follow=%s" %(screen_name, follow) - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) - except HTTPError, e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") - raise TwythonError("createFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): - """destroyFriendship(id = None, user_id = None, screen_name = None) - - Allows the authenticating users to unfollow the user specified in the ID parameter. - Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to unfollow. - user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "user_id=%s" % `user_id` - if screen_name is not None: - apiURL = "screen_name=%s" % screen_name - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, `id`), "lol=1")) # Random string hack for POST reasons ;P - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) - except HTTPError, e: - raise TwythonError("destroyFriendship() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b, version = None): - """checkIfFriendshipExists(user_a, user_b) - - Tests for the existence of friendship between two users. - Will return true if user_a follows user_b; otherwise, it'll return false. - - Parameters: - user_a - Required. The ID or screen_name of the subject user. - user_b - Required. The ID or screen_name of the user to test for following. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.urlencode({"user_a": user_a, "user_b": user_b})) - return simplejson.load(self.opener.open(friendshipURL)) - except HTTPError, e: - raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): - """showFriendship(source_id, source_screen_name, target_id, target_screen_name) - - Returns detailed information about the relationship between two users. - - Parameters: - ** Note: One of the following is required if the request is unauthenticated - source_id - The user_id of the subject user. - source_screen_name - The screen_name of the subject user. - - ** Note: One of the following is required at all times - target_id - The user_id of the target user. - target_screen_name - The screen_name of the target user. - - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D - if source_id is not None: - apiURL += "&source_id=%s" % `source_id` - if source_screen_name is not None: - apiURL += "&source_screen_name=%s" % source_screen_name - if target_id is not None: - apiURL += "&target_id=%s" % `target_id` - if target_screen_name is not None: - apiURL += "&target_screen_name=%s" % target_screen_name - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - # Catch this for now - if e.code == 403: - raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") - raise TwythonError("showFriendship() failed with a %s error code." % `e.code`, e.code) - - def updateDeliveryDevice(self, device_name = "none", version = None): - """updateDeliveryDevice(device_name = "none") - - Sets which device Twitter delivers updates to for the authenticating user. - Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) - - Parameters: - device - Required. Must be one of: sms, im, none. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.urlencode({"device": self.unicode2utf8(device_name)})) - except HTTPError, e: - raise TwythonError("updateDeliveryDevice() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, - profile_background_color = None, - profile_text_color = None, - profile_link_color = None, - profile_sidebar_fill_color = None, - profile_sidebar_border_color = None, - version = None): - """updateProfileColors() - - Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. - - Parameters: - ** Note: One or more of the following parameters must be present. Each parameter's value must - be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). - - profile_background_color - Optional. - profile_text_color - Optional. - profile_link_color - Optional. - profile_sidebar_fill_color - Optional. - profile_sidebar_border_color - Optional. - - 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. - """ - if self.authenticated is True: - updateProfileColorsQueryString = "?lol=2" - - def checkValidColor(str): - if len(str) != 6: - return False - for c in str: - if c not in "1234567890abcdefABCDEF": return False - - return True - - if profile_background_color is not None: - if checkValidColor(profile_background_color): - updateProfileColorsQueryString += "profile_background_color=" + profile_background_color - else: - raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") - if profile_text_color is not None: - if checkValidColor(profile_text_color): - updateProfileColorsQueryString += "profile_text_color=" + profile_text_color - else: - raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") - if profile_link_color is not None: - if checkValidColor(profile_link_color): - updateProfileColorsQueryString += "profile_link_color=" + profile_link_color - else: - raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") - if profile_sidebar_fill_color is not None: - if checkValidColor(profile_sidebar_fill_color): - updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color - else: - raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") - if profile_sidebar_border_color is not None: - if checkValidColor(profile_sidebar_border_color): - updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color - else: - raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") - - try: - return self.opener.open("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, updateProfileColorsQueryString) - except HTTPError, e: - raise TwythonError("updateProfileColors() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): - """updateProfile(name = None, email = None, url = None, location = None, description = None) - - Sets values that users are able to set under the "Account" tab of their settings page. - Only the parameters specified will be updated. - - Parameters: - One or more of the following parameters must be present. Each parameter's value - should be a string. See the individual parameter descriptions below for further constraints. - - name - Optional. Maximum of 20 characters. - email - Optional. Maximum of 40 characters. Must be a valid email address. - url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. - location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. - description - Optional. Maximum of 160 characters. - - 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 = version or self.apiVersion - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"url": self.unicode2utf8(url)}) - else: - updateProfileQueryString += urllib.urlencode({"url": self.unicode2utf8(url)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"location": self.unicode2utf8(location)}) - else: - updateProfileQueryString += urllib.urlencode({"location": self.unicode2utf8(location)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - updateProfileQueryString += urllib.urlencode({"description": self.unicode2utf8(description)}) - else: - raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) - except HTTPError, e: - raise TwythonError("updateProfile() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1", version = None): - """getFavorites(page = "1") - - Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. - - Parameters: - page - Optional. Specifies the page of favorites to retrieve. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, `page`))) - except HTTPError, e: - raise TwythonError("getFavorites() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id, version = None): - """createFavorite(id) - - Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. - - Parameters: - id - Required. The ID of the status to favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("createFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id, version = None): - """destroyFavorite(id) - - Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. - - Parameters: - id - Required. The ID of the status to un-favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroyFavorite() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): - """notificationFollow(id = None, user_id = None, screen_name = None) - - Enables device notifications for updates from the specified user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationFollow() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): - """notificationLeave(id = None, user_id = None, screen_name = None) - - Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError, e: - raise TwythonError("notificationLeave() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user the specified user is following. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getFriendsIDs() failed with a %s error code." % `e.code`, e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user following the specified user. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, `id`, breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, `user_id`, breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getFollowersIDs() failed with a %s error code." % `e.code`, e.code) - - def createBlock(self, id, version = None): - """createBlock(id) - - Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. - Returns the blocked user in the requested format when successful. - - Parameters: - id - The ID or screen name of a user to block. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("createBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id, version = None): - """destroyBlock(id) - - Un-blocks the user specified in the ID parameter for the authenticating user. - Returns the un-blocked user in the requested format when successful. - - Parameters: - id - Required. The ID or screen_name of the user to un-block - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroyBlock() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): - """checkIfBlockExists(id = None, user_id = None, screen_name = None) - - Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and - error with an HTTP 404 response code otherwise. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen_name of the potentially blocked user. - user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, `id`) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, `user_id`) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("checkIfBlockExists() failed with a %s error code." % `e.code`, e.code) - - def getBlocking(self, page = "1", version = None): - """getBlocking(page = "1") - - Returns an array of user objects that the authenticating user is blocking. - - Parameters: - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, `page`))) - except HTTPError, e: - raise TwythonError("getBlocking() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self, version = None): - """getBlockedIDs() - - Returns an array of numeric user ids the authenticating user is blocking. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) - except HTTPError, e: - raise TwythonError("getBlockedIDs() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - """searchTwitter(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. - lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. - locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. - rpp - Optional. The number of tweets to return per page, up to a max of 100. - page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) - since_id - Optional. Returns tweets with status ids greater than the given id. - geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. - show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. - - Usage Notes: - Queries are limited 140 URL encoded characters. - Some users may be absent from search results. - The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. - This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. - - Applications must have a meaningful and unique User Agent when using this method. - An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than - applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - return simplejson.load(self.opener.open(searchURL)) - except HTTPError, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def searchTwitterGen(self, search_query, **kwargs): - """searchTwitterGen(search_query, **kwargs) - - Returns a generator of tweets that match a specified query. - - Parameters: - callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. - lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. - locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. - rpp - Optional. The number of tweets to return per page, up to a max of 100. - page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) - since_id - Optional. Returns tweets with status ids greater than the given id. - geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. - show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. - - Usage Notes: - Queries are limited 140 URL encoded characters. - Some users may be absent from search results. - The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. - This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. - - Applications must have a meaningful and unique User Agent when using this method. - An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than - applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - data = simplejson.load(self.opener.open(searchURL)) - except HTTPError, e: - raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) - - if not data['results']: - raise StopIteration - - for tweet in data['results']: - yield tweet - - if 'page' not in kwargs: - kwargs['page'] = 2 - else: - kwargs['page'] += 1 - - for tweet in self.searchTwitterGen(search_query, **kwargs): - yield tweet - - def getCurrentTrends(self, excludeHashTags = False, version = None): - """getCurrentTrends(excludeHashTags = False, version = None) - - Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used - on Twitter Search results page for that topic. - - Parameters: - excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/current.json" % version - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getCurrentTrends() failed with a %s error code." % `e.code`, e.code) - - def getDailyTrends(self, date = None, exclude = False, version = None): - """getDailyTrends(date = None, exclude = False, version = None) - - Returns the top 20 trending topics for each hour in a given day. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/daily.json" % version - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getDailyTrends() failed with a %s error code." % `e.code`, e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - """getWeeklyTrends(date = None, exclude = False) - - Returns the top 30 trending topics for each day in a given week. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/daily.json" % version - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("getWeeklyTrends() failed with a %s error code." % `e.code`, e.code) - - def getSavedSearches(self, version = None): - """getSavedSearches() - - Returns the authenticated user's saved search queries. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) - except HTTPError, e: - raise TwythonError("getSavedSearches() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id, version = None): - """showSavedSearch(id) - - Retrieve the data for a saved search owned by the authenticating user specified by the given id. - - Parameters: - id - Required. The id of the saved search to be retrieved. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, `id`))) - except HTTPError, e: - raise TwythonError("showSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query, version = None): - """createSavedSearch(query) - - Creates a saved search for the authenticated user. - - Parameters: - query - Required. The query of the search the user would like to save. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) - except HTTPError, e: - raise TwythonError("createSavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id, version = None): - """ destroySavedSearch(id) - - Destroys a saved search for the authenticated user. - The search specified by id must be owned by the authenticating user. - - Parameters: - id - Required. The id of the saved search to be deleted. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, `id`), "")) - except HTTPError, e: - raise TwythonError("destroySavedSearch() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("destroySavedSearch() requires you to be authenticated.") - - def createList(self, name, mode = "public", description = "", version = None): - """ createList(self, name, mode, description, version) - - Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - name - Required. The name for the new list. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), - urllib.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError, e: - raise TwythonError("createList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("createList() requires you to be authenticated.") - - def updateList(self, list_id, name, mode = "public", description = "", version = None): - """ updateList(self, list_id, name, mode, description, version) - - Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look - at this... - - Parameters: - list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). - name - Required. The name of the list, possibly for renaming or such. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), - urllib.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError, e: - raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("updateList() requires you to be authenticated.") - - def showLists(self, version = None): - """ showLists(self, version) - - Show all the lists for the currently authenticated user (i.e, they own these lists). - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) - except HTTPError, e: - raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("showLists() requires you to be authenticated.") - - def getListMemberships(self, version = None): - """ getListMemberships(self, version) - - Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) - except HTTPError, e: - raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("getLists() requires you to be authenticated.") - - def deleteList(self, list_id, version = None): - """ deleteList(self, list_id, version) - - Deletes a list for the authenticating user. - - Parameters: - list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("deleteList() requires you to be authenticated.") - - def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): - """ getListTimeline(self, list_id, cursor, version, **kwargs) - - Retrieves a timeline representing everyone in the list specified. - - Parameters: - list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. - Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - try: - baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) - return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) - except HTTPError, e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) - - def getSpecificList(self, list_id, version = None): - """ getSpecificList(self, list_id, version) - - Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). - - Parameters: - list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - except HTTPError, e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) - - def addListMember(self, list_id, id, version = None): - """ addListMember(self, list_id, id, version) - - Adds a new Member (the passed in id) to the specified list. - - Parameters: - list_id - Required. The slug of the list to add the new member to. - id - Required. The ID or slug of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % (id))) - except HTTPError, e: - raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("addListMember requires you to be authenticated.") - - def getListMembers(self, list_id, version = None): - """ getListMembers(self, list_id, version = None) - - Show all members of a specified list. This method requires authentication if the list is private/protected. - - Parameters: - list_id - Required. The slug of the list to retrieve members for. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - except HTTPError, e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - - def removeListMember(self, list_id, id, version = None): - """ removeListMember(self, list_id, id, version) - - Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. - - Parameters: - list_id - Required. The slug of the list to remove the specified user from. - id - Required. The ID of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE&id=%s" % `id`)) - except HTTPError, e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("removeListMember() requires you to be authenticated.") - - def isListMember(self, list_id, id, version = None): - """ isListMember(self, list_id, id, version) - - 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, `id`))) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def subscribeToList(self, list_id, version): - """ subscribeToList(self, list_id, version) - - Subscribe the authenticated user to the list provided (must be public). - - Parameters: - list_id - Required. The list to subscribe 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) - except HTTPError, e: - raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("subscribeToList() requires you to be authenticated.") - - def unsubscribeFromList(self, list_id, version): - """ unsubscribeFromList(self, list_id, version) - - Unsubscribe the authenticated user from the list in question (must be public). - - Parameters: - list_id - Required. The list to unsubscribe from. - 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError, e: - raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("unsubscribeFromList() requires you to be authenticated.") - - def isListSubscriber(self, list_id, id, version = None): - """ isListSubscriber(self, list_id, id, version) - - Check if a specified user (id) is a subscriber 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, `id`))) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def availableTrends(self, latitude = None, longitude = None, version = None): - """ availableTrends(latitude, longitude, version): - - Gets all available trends, optionally filtering by geolocation based stuff. - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - latitude (string) - Optional. A latitude to sort by. - longitude (string) - Optional. A longitude to sort by. - 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 = version or self.apiVersion - try: - if latitude is not None and longitude is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json" % version)) - except HTTPError, e: - raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) - - def trendsByLocation(self, woeid, version = None): - """ trendsByLocation(woeid, version): - - Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - woeid (string) - Required. WoeID of the area you're searching in. - 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 = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) - except HTTPError, e: - raise TwythonError("trendsByLocation() 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. - def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """ updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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 = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename, version = None): - """ updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def unicode2utf8(self, text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text diff --git a/twython/oauth.py b/twython/oauth.py deleted file mode 100644 index 4bc47f5..0000000 --- a/twython/oauth.py +++ /dev/null @@ -1,524 +0,0 @@ -import cgi -import urllib -import time -import random -import urlparse -import hmac -import binascii - -VERSION = '1.0' # Hi Blaine! -HTTP_METHOD = 'GET' -SIGNATURE_METHOD = 'PLAINTEXT' - -# Generic exception class -class OAuthError(RuntimeError): - def __init__(self, message='OAuth error occured.'): - self.message = message - -# optional WWW-Authenticate header (401 error) -def build_authenticate_header(realm=''): - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - -# url escape -def escape(s): - # escape '/' too - return urllib.quote(s, safe='~') - -# util function: current timestamp -# seconds since epoch (UTC) -def generate_timestamp(): - return int(time.time()) - -# util function: nonce -# pseudorandom number -def generate_nonce(length=8): - return ''.join([str(random.randint(0, 9)) for i in range(length)]) - -# OAuthConsumer is a data type that represents the identity of the Consumer -# via its shared secret with the Service Provider. -class OAuthConsumer(object): - key = None - secret = None - - def __init__(self, key, secret): - self.key = key - self.secret = secret - -# OAuthToken is a data type that represents an End User via either an access -# or request token. -class OAuthToken(object): - # access tokens and request tokens - key = None - secret = None - - ''' - key = the token - secret = the token secret - ''' - def __init__(self, key, secret): - self.key = key - self.secret = secret - - def to_string(self): - return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) - - # return a token from something like: - # oauth_token_secret=digg&oauth_token=digg - def from_string(s): - params = cgi.parse_qs(s, keep_blank_values=False) - key = params['oauth_token'][0] - secret = params['oauth_token_secret'][0] - return OAuthToken(key, secret) - from_string = staticmethod(from_string) - - def __str__(self): - return self.to_string() - -# OAuthRequest represents the request and can be serialized -class OAuthRequest(object): - ''' - OAuth parameters: - - oauth_consumer_key - - oauth_token - - oauth_signature_method - - oauth_signature - - oauth_timestamp - - oauth_nonce - - oauth_version - ... any additional parameters, as defined by the Service Provider. - ''' - parameters = None # oauth parameters - http_method = HTTP_METHOD - http_url = None - version = VERSION - - def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): - self.http_method = http_method - self.http_url = http_url - self.parameters = parameters or {} - - def set_parameter(self, parameter, value): - self.parameters[parameter] = value - - def get_parameter(self, parameter): - try: - return self.parameters[parameter] - except: - raise OAuthError('Parameter not found: %s' % parameter) - - def _get_timestamp_nonce(self): - return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') - - # get any non-oauth parameters - def get_nonoauth_parameters(self): - parameters = {} - for k, v in self.parameters.iteritems(): - # ignore oauth parameters - if k.find('oauth_') < 0: - parameters[k] = v - return parameters - - # serialize as a header for an HTTPAuth request - def to_header(self, realm=''): - auth_header = 'OAuth realm="%s"' % realm - # add the oauth parameters - if self.parameters: - for k, v in self.parameters.iteritems(): - if k[:6] == 'oauth_': - auth_header += ', %s="%s"' % (k, escape(str(v))) - return {'Authorization': auth_header} - - # serialize as post data for a POST request - def to_postdata(self): - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) - - # serialize as a url for a GET request - def to_url(self): - return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) - - # return a string that consists of all the parameters that need to be signed - def get_normalized_parameters(self): - params = self.parameters - try: - # exclude the signature if it exists - del params['oauth_signature'] - except: - pass - key_values = params.items() - # sort lexicographically, first after key, then after value - key_values.sort() - # combine key value pairs in string and escape - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) - - # just uppercases the http method - def get_normalized_http_method(self): - return self.http_method.upper() - - # parses the url and rebuilds it to be scheme://host/path - def get_normalized_http_url(self): - parts = urlparse.urlparse(self.http_url) - url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path - return url_string - - # set the signature parameter to the result of build_signature - def sign_request(self, signature_method, consumer, token): - # set the signature method - self.set_parameter('oauth_signature_method', signature_method.get_name()) - # set the signature - self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) - - def build_signature(self, signature_method, consumer, token): - # call the build signature method within the signature method - return signature_method.build_signature(self, consumer, token) - - def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): - # combine multiple parameter sources - if parameters is None: - parameters = {} - - # headers - if headers and 'Authorization' in headers: - auth_header = headers['Authorization'] - # check that the authorization header is OAuth - if auth_header.index('OAuth') > -1: - try: - # get the parameters from the header - header_params = OAuthRequest._split_header(auth_header) - parameters.update(header_params) - except: - raise OAuthError('Unable to parse OAuth parameters from Authorization header.') - - # GET or POST query string - if query_string: - query_params = OAuthRequest._split_url_string(query_string) - parameters.update(query_params) - - # URL parameters - param_str = urlparse.urlparse(http_url)[4] # query - url_params = OAuthRequest._split_url_string(param_str) - parameters.update(url_params) - - if parameters: - return OAuthRequest(http_method, http_url, parameters) - - return None - from_request = staticmethod(from_request) - - def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): - if not parameters: - parameters = {} - - defaults = { - 'oauth_consumer_key': oauth_consumer.key, - 'oauth_timestamp': generate_timestamp(), - 'oauth_nonce': generate_nonce(), - 'oauth_version': OAuthRequest.version, - } - - defaults.update(parameters) - parameters = defaults - - if token: - parameters['oauth_token'] = token.key - - return OAuthRequest(http_method, http_url, parameters) - from_consumer_and_token = staticmethod(from_consumer_and_token) - - def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): - if not parameters: - parameters = {} - - parameters['oauth_token'] = token.key - - if callback: - parameters['oauth_callback'] = callback - - return OAuthRequest(http_method, http_url, parameters) - from_token_and_callback = staticmethod(from_token_and_callback) - - # util function: turn Authorization: header into parameters, has to do some unescaping - def _split_header(header): - params = {} - parts = header.split(',') - for param in parts: - # ignore realm parameter - if param.find('OAuth realm') > -1: - continue - # remove whitespace - param = param.strip() - # split key-value - param_parts = param.split('=', 1) - # remove quotes and unescape the value - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) - return params - _split_header = staticmethod(_split_header) - - # util function: turn url string into parameters, has to do some unescaping - def _split_url_string(param_str): - parameters = cgi.parse_qs(param_str, keep_blank_values=False) - for k, v in parameters.iteritems(): - parameters[k] = urllib.unquote(v[0]) - return parameters - _split_url_string = staticmethod(_split_url_string) - -# OAuthServer is a worker to check a requests validity against a data store -class OAuthServer(object): - timestamp_threshold = 300 # in seconds, five minutes - version = VERSION - signature_methods = None - data_store = None - - def __init__(self, data_store=None, signature_methods=None): - self.data_store = data_store - self.signature_methods = signature_methods or {} - - def set_data_store(self, oauth_data_store): - self.data_store = data_store - - def get_data_store(self): - return self.data_store - - def add_signature_method(self, signature_method): - self.signature_methods[signature_method.get_name()] = signature_method - return self.signature_methods - - # process a request_token request - # returns the request token on success - def fetch_request_token(self, oauth_request): - try: - # get the request token for authorization - token = self._get_token(oauth_request, 'request') - except OAuthError: - # no token required for the initial token request - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - self._check_signature(oauth_request, consumer, None) - # fetch a new token - token = self.data_store.fetch_request_token(consumer) - return token - - # process an access_token request - # returns the access token on success - def fetch_access_token(self, oauth_request): - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - # get the request token - token = self._get_token(oauth_request, 'request') - self._check_signature(oauth_request, consumer, token) - new_token = self.data_store.fetch_access_token(consumer, token) - return new_token - - # verify an api call, checks all the parameters - def verify_request(self, oauth_request): - # -> consumer and token - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - # get the access token - token = self._get_token(oauth_request, 'access') - self._check_signature(oauth_request, consumer, token) - parameters = oauth_request.get_nonoauth_parameters() - return consumer, token, parameters - - # authorize a request token - def authorize_token(self, token, user): - return self.data_store.authorize_request_token(token, user) - - # get the callback url - def get_callback(self, oauth_request): - return oauth_request.get_parameter('oauth_callback') - - # optional support for the authenticate header - def build_authenticate_header(self, realm=''): - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - - # verify the correct version request for this server - def _get_version(self, oauth_request): - try: - version = oauth_request.get_parameter('oauth_version') - except: - version = VERSION - if version and version != self.version: - raise OAuthError('OAuth version %s not supported.' % str(version)) - return version - - # figure out the signature with some defaults - def _get_signature_method(self, oauth_request): - try: - signature_method = oauth_request.get_parameter('oauth_signature_method') - except: - signature_method = SIGNATURE_METHOD - try: - # get the signature method object - signature_method = self.signature_methods[signature_method] - except: - signature_method_names = ', '.join(self.signature_methods.keys()) - raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) - - return signature_method - - def _get_consumer(self, oauth_request): - consumer_key = oauth_request.get_parameter('oauth_consumer_key') - if not consumer_key: - raise OAuthError('Invalid consumer key.') - consumer = self.data_store.lookup_consumer(consumer_key) - if not consumer: - raise OAuthError('Invalid consumer.') - return consumer - - # try to find the token for the provided request token key - def _get_token(self, oauth_request, token_type='access'): - token_field = oauth_request.get_parameter('oauth_token') - token = self.data_store.lookup_token(token_type, token_field) - if not token: - raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) - return token - - def _check_signature(self, oauth_request, consumer, token): - timestamp, nonce = oauth_request._get_timestamp_nonce() - self._check_timestamp(timestamp) - self._check_nonce(consumer, token, nonce) - signature_method = self._get_signature_method(oauth_request) - try: - signature = oauth_request.get_parameter('oauth_signature') - except: - raise OAuthError('Missing signature.') - # validate the signature - valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) - if not valid_sig: - key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) - raise OAuthError('Invalid signature. Expected signature base string: %s' % base) - built = signature_method.build_signature(oauth_request, consumer, token) - - def _check_timestamp(self, timestamp): - # verify that timestamp is recentish - timestamp = int(timestamp) - now = int(time.time()) - lapsed = now - timestamp - if lapsed > self.timestamp_threshold: - raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) - - def _check_nonce(self, consumer, token, nonce): - # verify that the nonce is uniqueish - nonce = self.data_store.lookup_nonce(consumer, token, nonce) - if nonce: - raise OAuthError('Nonce already used: %s' % str(nonce)) - -# OAuthClient is a worker to attempt to execute a request -class OAuthClient(object): - consumer = None - token = None - - def __init__(self, oauth_consumer, oauth_token): - self.consumer = oauth_consumer - self.token = oauth_token - - def get_consumer(self): - return self.consumer - - def get_token(self): - return self.token - - def fetch_request_token(self, oauth_request): - # -> OAuthToken - raise NotImplementedError - - def fetch_access_token(self, oauth_request): - # -> OAuthToken - raise NotImplementedError - - def access_resource(self, oauth_request): - # -> some protected resource - raise NotImplementedError - -# OAuthDataStore is a database abstraction used to lookup consumers and tokens -class OAuthDataStore(object): - - def lookup_consumer(self, key): - # -> OAuthConsumer - raise NotImplementedError - - def lookup_token(self, oauth_consumer, token_type, token_token): - # -> OAuthToken - raise NotImplementedError - - def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): - # -> OAuthToken - raise NotImplementedError - - def fetch_request_token(self, oauth_consumer): - # -> OAuthToken - raise NotImplementedError - - def fetch_access_token(self, oauth_consumer, oauth_token): - # -> OAuthToken - raise NotImplementedError - - def authorize_request_token(self, oauth_token, user): - # -> OAuthToken - raise NotImplementedError - -# OAuthSignatureMethod is a strategy class that implements a signature method -class OAuthSignatureMethod(object): - def get_name(self): - # -> str - raise NotImplementedError - - def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): - # -> str key, str raw - raise NotImplementedError - - def build_signature(self, oauth_request, oauth_consumer, oauth_token): - # -> str - raise NotImplementedError - - def check_signature(self, oauth_request, consumer, token, signature): - built = self.build_signature(oauth_request, consumer, token) - return built == signature - -class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): - - def get_name(self): - return 'HMAC-SHA1' - - def build_signature_base_string(self, oauth_request, consumer, token): - sig = ( - escape(oauth_request.get_normalized_http_method()), - escape(oauth_request.get_normalized_http_url()), - escape(oauth_request.get_normalized_parameters()), - ) - - key = '%s&' % escape(consumer.secret) - if token: - key += escape(token.secret) - raw = '&'.join(sig) - return key, raw - - def build_signature(self, oauth_request, consumer, token): - # build the base signature string - key, raw = self.build_signature_base_string(oauth_request, consumer, token) - - # hmac object - try: - import hashlib # 2.5 - hashed = hmac.new(key, raw, hashlib.sha1) - except: - import sha # deprecated - hashed = hmac.new(key, raw, sha) - - # calculate the digest base 64 - return binascii.b2a_base64(hashed.digest())[:-1] - -class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): - - def get_name(self): - return 'PLAINTEXT' - - def build_signature_base_string(self, oauth_request, consumer, token): - # concatenate the consumer key and secret - sig = escape(consumer.secret) + '&' - if token: - sig = sig + escape(token.secret) - return sig - - def build_signature(self, oauth_request, consumer, token): - return self.build_signature_base_string(oauth_request, consumer, token) \ No newline at end of file diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py new file mode 100644 index 0000000..42bd5a1 --- /dev/null +++ b/twython/twitter_endpoints.py @@ -0,0 +1,299 @@ +""" + 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: + + {{version}}, etc + + 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. + + 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). +""" + +# Base Twitter API url, no need to repeat this junk... +base_url = 'http://api.twitter.com/{{version}}' + +api_table = { + 'getRateLimitStatus': { + 'url': '/account/rate_limit_status.json', + 'method': 'GET', + }, + + # Timeline methods + 'getPublicTimeline': { + 'url': '/statuses/public_timeline.json', + 'method': 'GET', + }, + 'getHomeTimeline': { + 'url': '/statuses/home_timeline.json', + 'method': 'GET', + }, + 'getUserTimeline': { + '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', + }, + 'destroyFriendship': { + 'url': '/friendships/destroy.json', + 'method': 'POST', + }, + 'getFriendsIDs': { + 'url': '/friends/ids.json', + 'method': 'GET', + }, + 'getFollowersIDs': { + 'url': '/followers/ids.json', + 'method': 'GET', + }, + + # Retweets + 'reTweet': { + 'url': '/statuses/retweet/{{id}}.json', + 'method': 'POST', + }, + 'getRetweets': { + 'url': '/statuses/retweets/{{id}}.json', + 'method': 'GET', + }, + 'retweetedOfMe': { + 'url': '/statuses/retweets_of_me.json', + 'method': 'GET', + }, + 'retweetedByMe': { + 'url': '/statuses/retweeted_by_me.json', + 'method': 'GET', + }, + 'retweetedToMe': { + 'url': '/statuses/retweeted_to_me.json', + 'method': 'GET', + }, + + # User methods + 'showUser': { + 'url': '/users/show.json', + 'method': 'GET', + }, + 'searchUsers': { + 'url': '/users/search.json', + 'method': 'GET', + }, + + # Status methods - showing, updating, destroying, etc. + 'showStatus': { + 'url': '/statuses/show/{{id}}.json', + 'method': 'GET', + }, + 'updateStatus': { + 'url': '/statuses/update.json', + 'method': 'POST', + }, + 'destroyStatus': { + 'url': '/statuses/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Direct Messages - getting, sending, effing, etc. + 'getDirectMessages': { + 'url': '/direct_messages.json', + 'method': 'GET', + }, + 'getSentMessages': { + 'url': '/direct_messages/sent.json', + 'method': 'GET', + }, + 'sendDirectMessage': { + 'url': '/direct_messages/new.json', + 'method': 'POST', + }, + 'destroyDirectMessage': { + 'url': '/direct_messages/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Friendship methods + 'checkIfFriendshipExists': { + 'url': '/friendships/exists.json', + 'method': 'GET', + }, + 'showFriendship': { + 'url': '/friendships/show.json', + 'method': 'GET', + }, + + # Profile methods + 'updateProfile': { + 'url': '/account/update_profile.json', + 'method': 'POST', + }, + 'updateProfileColors': { + 'url': '/account/update_profile_colors.json', + 'method': 'POST', + }, + + # Favorites methods + 'getFavorites': { + 'url': '/favorites.json', + 'method': 'GET', + }, + 'createFavorite': { + 'url': '/favorites/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyFavorite': { + 'url': '/favorites/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Blocking methods + 'createBlock': { + 'url': '/blocks/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyBlock': { + 'url': '/blocks/destroy/{{id}}.json', + 'method': 'POST', + }, + 'getBlocking': { + 'url': '/blocks/blocking.json', + 'method': 'GET', + }, + 'getBlockedIDs': { + 'url': '/blocks/blocking/ids.json', + 'method': 'GET', + }, + 'checkIfBlockExists': { + 'url': '/blocks/exists.json', + 'method': 'GET', + }, + + # Trending methods + 'getCurrentTrends': { + 'url': '/trends/current.json', + 'method': 'GET', + }, + 'getDailyTrends': { + 'url': '/trends/daily.json', + 'method': 'GET', + }, + 'getWeeklyTrends': { + 'url': '/trends/weekly.json', + 'method': 'GET', + }, + 'availableTrends': { + 'url': '/trends/available.json', + 'method': 'GET', + }, + 'trendsByLocation': { + 'url': '/trends/{{woeid}}.json', + 'method': 'GET', + }, + + # Saved Searches + 'getSavedSearches': { + 'url': '/saved_searches.json', + 'method': 'GET', + }, + 'showSavedSearch': { + 'url': '/saved_searches/show/{{id}}.json', + 'method': 'GET', + }, + 'createSavedSearch': { + 'url': '/saved_searches/create.json', + 'method': 'GET', + }, + 'destroySavedSearch': { + 'url': '/saved_searches/destroy/{{id}}.json', + 'method': 'GET', + }, + + # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P + 'createList': { + 'url': '/{{username}}/lists.json', + 'method': 'POST', + }, + 'updateList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'POST', + }, + 'showLists': { + 'url': '/{{username}}/lists.json', + 'method': 'GET', + }, + 'getListMemberships': { + 'url': '/{{username}}/lists/followers.json', + 'method': 'GET', + }, + 'deleteList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'DELETE', + }, + 'getListTimeline': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'getSpecificList': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'addListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'POST', + }, + 'getListMembers': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'GET', + }, + 'deleteListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'DELETE', + }, + 'subscribeToList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'POST', + }, + 'unsubscribeFromList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'DELETE', + }, + + # The one-offs + 'notificationFollow': { + 'url': '/notifications/follow/follow.json', + 'method': 'POST', + }, + 'notificationLeave': { + 'url': '/notifications/leave/leave.json', + 'method': 'POST', + }, + 'updateDeliveryService': { + 'url': '/account/update_delivery_device.json', + 'method': 'POST', + }, + 'reportSpam': { + 'url': '/report_spam.json', + 'method': 'POST', + }, +} diff --git a/twython/twyauth.py b/twython/twyauth.py deleted file mode 100644 index bf4183f..0000000 --- a/twython/twyauth.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/python - -""" - Twython-oauth (twyauth) is a separate library to handle OAuth routines with Twython. This currently doesn't work, as I never get the time to finish it. - Feel free to help out. - - Questions, comments? ryan@venodesigns.net -""" - -import httplib, urllib, urllib2, mimetypes, mimetools - -from urlparse import urlparse -from urllib2 import HTTPError - -try: - import oauth -except ImportError: - pass - -class oauth: - def __init__(self, username, consumer_key, consumer_secret, signature_method = None, headers = None, version = 1): - """oauth(username = None, consumer_secret = None, consumer_key = None, headers = None) - - Instantiates an instance of Twython with OAuth. Takes optional parameters for authentication and such (see below). - - Parameters: - username - Your Twitter username, if you want Basic (HTTP) Authentication. - consumer_secret - Consumer secret, given to you when you register your App with Twitter. - consumer_key - Consumer key (see situation with consumer_secret). - signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1() - headers - User agent header. - version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. - """ - # OAuth specific variables below - self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version - self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version - self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version - self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version - self.consumer_key = consumer_key - self.consumer_secret = consumer_secret - self.request_token = None - self.access_token = None - self.consumer = None - self.connection = None - self.signature_method = None - self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret) - self.connection = httplib.HTTPSConnection("http://api.twitter.com") - - def getOAuthResource(self, url, access_token, params, http_method="GET"): - """getOAuthResource(self, url, access_token, params, http_method="GET") - - Returns a signed OAuth object for use in requests. - """ - newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters) - oauth_request.sign_request(self.signature_method, consumer, access_token) - return oauth_request - - def getResponse(self, oauth_request, connection): - """getResponse(self, oauth_request, connection) - - Returns a JSON-ified list of results. - """ - url = oauth_request.to_url() - connection.request(oauth_request.http_method, url) - response = connection.getresponse() - return simplejson.load(response.read()) - - def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url) - oauth_request.sign_request(signature_method, consumer, None) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) - - def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url) - oauth_request.sign_request(signature_method, consumer, token) - return oauth_request.to_url() - - def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()): - # May not be needed... - self.request_token = request_token - oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url) - oauth_request.sign_request(signature_method, consumer, request_token) - resp = fetch_response(oauth_request, connection) - return oauth.OAuthToken.from_string(resp) diff --git a/twython/twython.py b/twython/twython.py new file mode 100644 index 0000000..4ae4daf --- /dev/null +++ b/twython/twython.py @@ -0,0 +1,417 @@ +#!/usr/bin/python + +""" + Twython is a library for Python that wraps the Twitter API. + It aims to abstract away all the API endpoints, so that additions to the library + and/or the Twitter API won't cause any overall problems. + + Questions, comments? ryan@venodesigns.net +""" + +__author__ = "Ryan McGrath " +__version__ = "1.3" + +import urllib +import urllib2 +import urlparse +import httplib +import httplib2 +import mimetypes +import mimetools +import re + +import oauth2 as oauth + +# 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 urllib2 import HTTPError + +# There are some special setups (like, oh, a Django application) where +# 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 +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 + 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. + from django.utils import simplejson + except: + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +class TwythonError(Exception): + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by APILimit and AuthError. + + Note: To use these, the syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: + + from twython import TwythonError, APILimit, AuthError + """ + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + + 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 + 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 __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 + your authentication. + """ + def __init__(self, msg): + self.msg = msg + + 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, headers=None): + """setup(self, oauth_token = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + twitter_token - Given to you when you register your application with Twitter. + twitter_secret - Given to you when you register your application with Twitter. + oauth_token - If you've gone through the authentication process and have a token for this user, + pass it in and it'll be used for all requests going forward. + oauth_token_secret - see oauth_token; it's the other half. + headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + + ** 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.twitter_token = twitter_token + self.twitter_secret = twitter_secret + self.oauth_token = oauth_token + self.oauth_secret = oauth_token_secret + + # If there's headers, set them, otherwise be an embarassing parent for their own good. + self.headers = headers + if self.headers is None: + headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + + consumer = None + token = None + + if self.twitter_token is not None and self.twitter_secret is not None: + consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + + if self.oauth_token is not None and self.oauth_secret is not None: + token = oauth.Token(oauth_token, oauth_token_secret) + + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. + if consumer is not None and token is not None: + self.client = oauth.Client(consumer, token) + elif consumer is not None: + self.client = oauth.Client(consumer) + else: + # If they don't do authentication, but still want to request unprotected resources, we need an opener. + self.client = httplib2.Http() + + def __getattr__(self, api_call): + """ + The most magically awesome block of code you'll see in 2010. + + Rather than list out 9 million damn methods for this API, we just keep a table (see above) of + every API endpoint and their corresponding function id for this library. This pretty much gives + unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is + going to be your bottleneck... well, don't use Python. ;P + + For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). + It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, + we can take over and find the API method in our table. We then return a function that downloads and parses + what we're looking for, based on the keywords passed in. + + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". + """ + def get(self, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) + + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs)) + else: + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) + resp, content = self.client.request(url, fn['method']) + + return simplejson.loads(content) + + if api_call in api_table: + return get.__get__(self) + else: + raise AttributeError, api_call + + def get_authentication_tokens(self): + """ + get_auth_url(self) + + Returns an authorization URL for a user to hit. + """ + resp, content = self.client.request(self.request_token_url, "GET") + + if resp['status'] != '200': + raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + + request_tokens = dict(urlparse.parse_qsl(content)) + request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + return request_tokens + + def get_authorized_tokens(self): + """ + get_authorized_tokens + + Returns authorized tokens after they go through the auth_url phase. + """ + resp, content = self.client.request(self.access_token_url, "GET") + return dict(urlparse.parse_qsl(content)) + + # ------------------------------------------------------------------------------------------------------------------------ + # The following methods are all different in some manner or require special attention with regards to the Twitter API. + # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, + # but it's not high on the priority list at the moment. + # ------------------------------------------------------------------------------------------------------------------------ + + @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: + resp, content = self.client.request( + shortener + "?" + urllib.urlencode({query: Twython.unicode2utf8(url_to_shorten)}), + "GET" + ) + 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 = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + + 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. + + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += `id` + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + resp, content = self.client.request(apiURL, "GET") + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) + + def searchTwitter(self, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + resp, content = self.client.request(searchURL, "GET") + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + + def searchTwitterGen(self, **kwargs): + """searchTwitterGen(search_query, **kwargs) + + Returns a generator of tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + try: + resp, content = self.client.request(searchURL, "GET") + data = simplejson.loads(content) + except HTTPError, e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) + + if not data['results']: + raise StopIteration + + for tweet in data['results']: + yield tweet + + if 'page' not in kwargs: + kwargs['page'] = 2 + else: + kwargs['page'] += 1 + + for tweet in self.searchTwitterGen(search_query, **kwargs): + yield tweet + + def isListMember(self, list_id, id, username, version = 1): + """ isListMember(self, list_id, id, version) + + 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. + + 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. + """ + try: + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`)) + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def isListSubscriber(self, list_id, id, version = 1): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + 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. + """ + try: + resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`) + return simplejson.loads(content) + except HTTPError, e: + 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. + def updateProfileBackgroundImage(self, filename, tile="true", version = 1): + """ updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = Twython.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) + return urllib2.urlopen(r).read() + except HTTPError, e: + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + + def updateProfileImage(self, filename, version = 1): + """ updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = Twython.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) + return urllib2.urlopen(r).read() + except HTTPError, e: + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + @staticmethod + def encode_multipart_formdata(fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % Twython.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + @staticmethod + def get_content_type(filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + @staticmethod + def unicode2utf8(text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text \ No newline at end of file diff --git a/twython3k/core.py b/twython3k/core.py deleted file mode 100644 index d86a17f..0000000 --- a/twython3k/core.py +++ /dev/null @@ -1,1896 +0,0 @@ -#!/usr/bin/python - -""" - Twython is an up-to-date library for Python that wraps the Twitter API. - Other Python Twitter libraries seem to have fallen a bit behind, and - Twitter's API has evolved a bit. Here's hoping this helps. - - TODO: OAuth, Streaming API? - - Questions, comments? ryan@venodesigns.net -""" - -import http.client, urllib, urllib.request, urllib.error, urllib.parse, mimetypes, mimetools - -from urllib.parse import urlparse -from urllib.error import HTTPError - -__author__ = "Ryan McGrath " -__version__ = "1.1" - -"""Twython - Easy Twitter utilities in Python""" - -try: - import simplejson -except ImportError: - try: - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -class TwythonError(Exception): - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - def __str__(self): - return repr(self.msg) - -class APILimit(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class AuthError(TwythonError): - def __init__(self, msg): - self.msg = msg - def __str__(self): - return repr(self.msg) - -class setup: - def __init__(self, username = None, password = None, headers = None, proxy = None, version = 1): - """setup(username = None, password = None, proxy = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - username - Your Twitter username, if you want Basic (HTTP) Authentication. - password - Password for your twitter account, if you want Basic (HTTP) Authentication. - headers - User agent header. - proxy - An object detailing information, in case proxy use/authentication is required. Object passed should be something like... - - proxyobj = { - "username": "fjnfsjdnfjd", - "password": "fjnfjsjdfnjd", - "host": "http://fjanfjasnfjjfnajsdfasd.com", - "port": 87 - } - - version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis. - - ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. - """ - self.authenticated = False - self.username = username - self.apiVersion = version - self.proxy = proxy - self.headers = headers - if self.proxy is not None: - self.proxyobj = urllib.request.ProxyHandler({'http': 'http://%s:%s@%s:%d' % (self.proxy["username"], self.proxy["password"], self.proxy["host"], self.proxy["port"])}) - # Check and set up authentication - if self.username is not None and password is not None: - # Assume Basic authentication ritual - self.auth_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() - self.auth_manager.add_password(None, "http://api.twitter.com", self.username, password) - self.handler = urllib.request.HTTPBasicAuthHandler(self.auth_manager) - if self.proxy is not None: - self.opener = urllib.request.build_opener(self.proxyobj, self.handler) - else: - self.opener = urllib.request.build_opener(self.handler) - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - self.authenticated = True # Play nice, people can force-check using verifyCredentials() - else: - # Build a non-auth opener so we can allow proxy-auth and/or header swapping - if self.proxy is not None: - self.opener = urllib.request.build_opener(self.proxyobj) - else: - self.opener = urllib.request.build_opener() - if self.headers is not None: - self.opener.addheaders = [('User-agent', self.headers)] - - # URL Shortening function huzzah - def shortenURL(self, 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: - return urllib.request.urlopen(shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)})).read() - except HTTPError, e: - raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) - - def verifyCredentials(self, version = None): - """ verifyCredentials(self, version = None): - - Verifies the authenticity of the passed in credentials. Used to be a forced call, now made optional - (no need to waste network resources) - - Parameters: - None - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - simplejson.load(self.opener.open("http://api.twitter.com/%d/account/verify_credentials.json" % version)) - except HTTPError as e: - raise AuthError("Authentication failed with your provided credentials. Try again? (%s failure)" % repr(e.code)) - else: - raise AuthError("verifyCredentials() requires you to actually, y'know, pass in credentials.") - - def getRateLimitStatus(self, checkRequestingIP = True, version = None): - """getRateLimitStatus() - - Returns the remaining number of API requests available to the requesting user before the - API limit is reached for the current hour. Calls to rate_limit_status do not count against - the rate limit. If authentication credentials are provided, the rate limit status for the - authenticating user is returned. Otherwise, the rate limit status for the requesting - IP address is returned. - - Params: - checkRequestIP - Boolean, defaults to True. Set to False to check against the currently requesting IP, instead of the account level. - 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 = version or self.apiVersion - try: - if checkRequestingIP is True: - return simplejson.load(urllib.request.urlopen("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/account/rate_limit_status.json" % version)) - else: - raise TwythonError("You need to be authenticated to check a rate limit status on an account.") - except HTTPError as e: - raise TwythonError("It seems that there's something wrong. Twitter gave you a %s error code; are you doing something you shouldn't be?" % repr(e.code), e.code) - - def getPublicTimeline(self, version = None): - """getPublicTimeline() - - Returns the 20 most recent statuses from non-protected users who have set a custom user icon. - The public timeline is cached for 60 seconds, so requesting it more often than that is a waste of resources. - - Params: - 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 = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/public_timeline.json" % version)) - except HTTPError as e: - raise TwythonError("getPublicTimeline() failed with a %s error code." % repr(e.code)) - - def getHomeTimeline(self, version = None, **kwargs): - """getHomeTimeline(**kwargs) - - Returns the 20 most recent statuses, including retweets, posted by the authenticating user - and that user's friends. This is the equivalent of /timeline/home on the Web. - - Usage note: This home_timeline is identical to statuses/friends_timeline, except it also - contains retweets, which statuses/friends_timeline does not (for backwards compatibility - reasons). In a future version of the API, statuses/friends_timeline will go away and - be replaced by home_timeline. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - homeTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/home_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(homeTimelineURL)) - except HTTPError as e: - raise TwythonError("getHomeTimeline() failed with a %s error code. (This is an upcoming feature in the Twitter API, and may not be implemented yet)" % repr(e.code)) - else: - raise AuthError("getHomeTimeline() requires you to be authenticated.") - - def getFriendsTimeline(self, version = None, **kwargs): - """getFriendsTimeline(**kwargs) - - Returns the 20 most recent statuses posted by the authenticating user, as well as that users friends. - This is the equivalent of /timeline/home on the Web. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendsTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/friends_timeline.json" % version, kwargs) - return simplejson.load(self.opener.open(friendsTimelineURL)) - except HTTPError as e: - raise TwythonError("getFriendsTimeline() failed with a %s error code." % repr(e.code)) - else: - raise AuthError("getFriendsTimeline() requires you to be authenticated.") - - def getUserTimeline(self, id = None, version = None, **kwargs): - """getUserTimeline(id = None, **kwargs) - - Returns the 20 most recent statuses posted from the authenticating user. It's also - possible to request another user's timeline via the id parameter. This is the - equivalent of the Web / page for your own user, or the profile page for a third party. - - Parameters: - id - Optional. Specifies the ID or screen name of the user for whom to return the user_timeline. - user_id - Optional. Specfies the ID of the user for whom to return the user_timeline. Helpful for disambiguating. - screen_name - Optional. Specfies the screen name of the user for whom to return the user_timeline. (Helpful for disambiguating when a valid screen name is also a user ID) - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if id is not None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, repr(id)), kwargs) - elif id is None and ("user_id" in kwargs) is False and ("screen_name" in kwargs) is False and self.authenticated is True: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline/%s.json" % (version, self.username), kwargs) - else: - userTimelineURL = self.constructApiURL("http://api.twitter.com/%d/statuses/user_timeline.json" % version, kwargs) - try: - # We do our custom opener if we're authenticated, as it helps avoid cases where it's a protected user - if self.authenticated is True: - return simplejson.load(self.opener.open(userTimelineURL)) - else: - return simplejson.load(self.opener.open(userTimelineURL)) - except HTTPError as e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? If so, you'll need to authenticate and be their friend to get their timeline." - % repr(e.code), e.code) - - def getUserMentions(self, version = None, **kwargs): - """getUserMentions(**kwargs) - - Returns the 20 most recent mentions (status containing @username) for the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - mentionsFeedURL = self.constructApiURL("http://api.twitter.com/%d/statuses/mentions.json" % version, kwargs) - return simplejson.load(self.opener.open(mentionsFeedURL)) - except HTTPError as e: - raise TwythonError("getUserMentions() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getUserMentions() requires you to be authenticated.") - - def reportSpam(self, id = None, user_id = None, screen_name = None, version = None): - """reportSpam(self, id), user_id, screen_name): - - Report a user account to Twitter as a spam account. *One* of the following parameters is required, and - this requires that you be authenticated with a user account. - - Parameters: - id - Optional. The ID or screen_name of the user you want to report as a spammer. - user_id - Optional. The ID of the user you want to report as a spammer. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. The ID or screen_name of the user you want to report as a spammer. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - # This entire block of code is stupid, but I'm far too tired to think through it at the moment. Refactor it if you care. - if id is not None or user_id is not None or screen_name is not None: - try: - apiExtension = "" - if id is not None: - apiExtension = "id=%s" % id - if user_id is not None: - apiExtension = "user_id=%s" % repr(user_id) - if screen_name is not None: - apiExtension = "screen_name=%s" % screen_name - return simplejson.load(self.opener.open("http://api.twitter.com/%d/report_spam.json" % version, apiExtension)) - except HTTPError as e: - raise TwythonError("reportSpam() failed with a %s error code." % repr(e.code), e.code) - else: - raise TwythonError("reportSpam requires you to specify an id, user_id, or screen_name. Try again!") - else: - raise AuthError("reportSpam() requires you to be authenticated.") - - def reTweet(self, id, version = None): - """reTweet(id) - - Retweets a tweet. Requires the id parameter of the tweet you are retweeting. - - Parameters: - id - Required. The numerical ID of the tweet you are retweeting. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/retweet/%s.json" % (version, repr(id)), "POST")) - except HTTPError as e: - raise TwythonError("reTweet() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("reTweet() requires you to be authenticated.") - - def getRetweets(self, id, count = None, version = None): - """ getRetweets(self, id, count): - - Returns up to 100 of the first retweets of a given tweet. - - Parameters: - id - Required. The numerical ID of the tweet you want the retweets of. - count - Optional. Specifies the number of retweets to retrieve. May not be greater than 100. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/statuses/retweets/%s.json" % (version, repr(id)) - if count is not None: - apiURL += "?count=%s" % repr(count) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getRetweets failed with a %s eroror code." % repr(e.code), e.code) - else: - raise AuthError("getRetweets() requires you to be authenticated.") - - def retweetedOfMe(self, version = None, **kwargs): - """retweetedOfMe(**kwargs) - - Returns the 20 most recent tweets of the authenticated user that have been retweeted by others. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweets_of_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError as e: - raise TwythonError("retweetedOfMe() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("retweetedOfMe() requires you to be authenticated.") - - def retweetedByMe(self, version = None, **kwargs): - """retweetedByMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_by_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError as e: - raise TwythonError("retweetedByMe() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("retweetedByMe() requires you to be authenticated.") - - def retweetedToMe(self, version = None, **kwargs): - """retweetedToMe(**kwargs) - - Returns the 20 most recent retweets posted by the authenticating user's friends. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - retweetURL = self.constructApiURL("http://api.twitter.com/%d/statuses/retweeted_to_me.json" % version, kwargs) - return simplejson.load(self.opener.open(retweetURL)) - except HTTPError as e: - raise TwythonError("retweetedToMe() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("retweetedToMe() requires you to be authenticated.") - - def searchUsers(self, q, per_page = 20, page = 1, version = None): - """ searchUsers(q, per_page = None, page = None): - - Query Twitter to find a set of users who match the criteria we have. (Note: This, oddly, requires authentication - go figure) - - Parameters: - q (string) - Required. The query you wanna search against; self explanatory. ;) - per_page (number) - Optional, defaults to 20. Specify the number of users Twitter should return per page (no more than 20, just fyi) - page (number) - Optional, defaults to 1. The page of users you want to pull 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/users/search.json?q=%s&per_page=%d&page=%d" % (version, q, per_page, page))) - except HTTPError as e: - raise TwythonError("searchUsers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("searchUsers(), oddly, requires you to be authenticated.") - - def showUser(self, id = None, user_id = None, screen_name = None, version = None): - """showUser(id = None, user_id = None, screen_name = None) - - Returns extended information of a given user. The author's most recent status will be returned inline. - - Parameters: - ** Note: One of the following must always be specified. - id - The ID or screen name of a user. - user_id - Specfies the ID of the user to return. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Specfies the screen name of the user to return. Helpful for disambiguating when a valid screen name is also a user ID. - 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. - - Usage Notes: - Requests for protected users without credentials from - 1) the user requested or - 2) a user that is following the protected user will omit the nested status element. - - ...will result in only publicly available data being returned. - """ - version = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/users/show/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/users/show.json?screen_name=%s" % (version, screen_name) - if apiURL != "": - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("showUser() failed with a %s error code." % repr(e.code), e.code) - - def bulkUserLookup(self, ids = None, screen_names = None, version = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - - 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. - - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - version = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += repr(id) + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("bulkUserLookup() requires you to be authenticated.") - - def getFriendsStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor="-1", version = None): - """getFriendsStatus(id = None, user_id = None, screen_name = None, page = None, cursor="-1") - - Returns a user's friends, each with current status inline. They are ordered by the order in which they were added as friends, 100 at a time. - (Please note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) Use the page option to access - older friends. With no user specified, the request defaults to the authenticated users friends. - - It's also possible to request another user's friends list via the id, screen_name or user_id parameter. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, or screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of friends. - user_id - Optional. Specfies the ID of the user for whom to return the list of friends. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of friends. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page of friends to receive. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/friends.json?screen_name=%s" % (version, screen_name) - try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % repr(page))) - else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) - except HTTPError as e: - raise TwythonError("getFriendsStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getFriendsStatus() requires you to be authenticated.") - - def getFollowersStatus(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersStatus(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns the authenticating user's followers, each with current status inline. - They are ordered by the order in which they joined Twitter, 100 at a time. - (Note that the result set isn't guaranteed to be 100 every time, as suspended users will be filtered out.) - - Use the page option to access earlier followers. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen name of the user for whom to request a list of followers. - user_id - Optional. Specfies the ID of the user for whom to return the list of followers. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the user for whom to return the list of followers. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page to retrieve. - cursor - Optional. Breaks the results into pages. A single page contains 100 users. This is recommended for users who are following many users. Provide a value of -1 to begin paging. Provide values as returned to in the response body's next_cursor and previous_cursor attributes to page back and forth in the list. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/statuses/followers.json?screen_name=%s" % (version, screen_name) - try: - if page is not None: - return simplejson.load(self.opener.open(apiURL + "&page=%s" % page)) - else: - return simplejson.load(self.opener.open(apiURL + "&cursor=%s" % cursor)) - except HTTPError as e: - raise TwythonError("getFollowersStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getFollowersStatus() requires you to be authenticated.") - - def showStatus(self, id, version = None): - """showStatus(id) - - Returns a single status, specified by the id parameter below. - The status's author will be returned inline. - - Parameters: - id - Required. The numerical ID of the status to retrieve. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/show/%s.json" % (version, id))) - except HTTPError as e: - raise TwythonError("Failed with a %s error code. Does this user hide/protect their updates? You'll need to authenticate and be friends to get their timeline." - % repr(e.code), e.code) - - def updateStatus(self, status, in_reply_to_status_id = None, latitude = None, longitude = None, version = None): - """updateStatus(status, in_reply_to_status_id = None) - - Updates the authenticating user's status. Requires the status parameter specified below. - A status update with text identical to the authenticating users current status will be ignored to prevent duplicates. - - Parameters: - status - Required. The text of your status update. URL encode as necessary. Statuses over 140 characters will be forceably truncated. - in_reply_to_status_id - Optional. The ID of an existing status that the update is in reply to. - latitude (string) - Optional. The location's latitude that this tweet refers to. - longitude (string) - Optional. The location's longitude that this tweet refers 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. - - ** Note: in_reply_to_status_id will be ignored unless the author of the tweet this parameter references - is mentioned within the status text. Therefore, you must include @username, where username is - the author of the referenced tweet, within the update. - - ** Note: valid ranges for latitude/longitude are, for example, -180.0 to +180.0 (East is positive) inclusive. - This parameter will be ignored if outside that range, not a number, if geo_enabled is disabled, or if there not a corresponding latitude parameter with this tweet. - """ - version = version or self.apiVersion - try: - postExt = urllib.parse.urlencode({"status": self.unicode2utf8(status)}) - if latitude is not None and longitude is not None: - postExt += "&lat=%s&long=%s" % (latitude, longitude) - if in_reply_to_status_id is not None: - postExt += "&in_reply_to_status_id=%s" % repr(in_reply_to_status_id) - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/update.json?" % version, postExt)) - except HTTPError as e: - raise TwythonError("updateStatus() failed with a %s error code." % repr(e.code), e.code) - - def destroyStatus(self, id, version = None): - """destroyStatus(id) - - Destroys the status specified by the required ID parameter. - The authenticating user must be the author of the specified status. - - Parameters: - id - Required. The ID of the status to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/statuses/destroy/%s.json?" % (version, id), "_method=DELETE")) - except HTTPError as e: - raise TwythonError("destroyStatus() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("destroyStatus() requires you to be authenticated.") - - def endSession(self, version = None): - """endSession() - - Ends the session of the authenticating user, returning a null cookie. - Use this method to sign users out of client-facing applications (widgets, etc). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - self.opener.open("http://api.twitter.com/%d/account/end_session.json" % version, "") - self.authenticated = False - except HTTPError as e: - raise TwythonError("endSession failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("You can't end a session when you're not authenticated to begin with.") - - def getDirectMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getDirectMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent to the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages.json?page=%s" % (version, repr(page)) - if since_id is not None: - apiURL += "&since_id=%s" % repr(since_id) - if max_id is not None: - apiURL += "&max_id=%s" % repr(max_id) - if count is not None: - apiURL += "&count=%s" % repr(count) - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getDirectMessages() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getDirectMessages() requires you to be authenticated.") - - def getSentMessages(self, since_id = None, max_id = None, count = None, page = "1", version = None): - """getSentMessages(since_id = None, max_id = None, count = None, page = "1") - - Returns a list of the 20 most recent direct messages sent by the authenticating user. - - Parameters: - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - page - Optional. Specifies the page of results to retrieve. Note: there are pagination limits. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/direct_messages/sent.json?page=%s" % (version, repr(page)) - if since_id is not None: - apiURL += "&since_id=%s" % repr(since_id) - if max_id is not None: - apiURL += "&max_id=%s" % repr(max_id) - if count is not None: - apiURL += "&count=%s" % repr(count) - - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getSentMessages() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getSentMessages() requires you to be authenticated.") - - def sendDirectMessage(self, user, text, version = None): - """sendDirectMessage(user, text) - - Sends a new direct message to the specified user from the authenticating user. Requires both the user and text parameters. - Returns the sent message in the requested format when successful. - - Parameters: - user - Required. The ID or screen name of the recipient user. - text - Required. The text of your direct message. Be sure to keep it under 140 characters. - 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 = version or self.apiVersion - if self.authenticated is True: - if len(list(text)) < 140: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/new.json" % version, urllib.parse.urlencode({"user": user, "text": text})) - except HTTPError as e: - raise TwythonError("sendDirectMessage() failed with a %s error code." % repr(e.code), e.code) - else: - raise TwythonError("Your message must not be longer than 140 characters") - else: - raise AuthError("You must be authenticated to send a new direct message.") - - def destroyDirectMessage(self, id, version = None): - """destroyDirectMessage(id) - - Destroys the direct message specified in the required ID parameter. - The authenticating user must be the recipient of the specified direct message. - - Parameters: - id - Required. The ID of the direct message to destroy. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/direct_messages/destroy/%s.json" % (version, id), "") - except HTTPError as e: - raise TwythonError("destroyDirectMessage() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("You must be authenticated to destroy a direct message.") - - def createFriendship(self, id = None, user_id = None, screen_name = None, follow = "false", version = None): - """createFriendship(id = None, user_id = None, screen_name = None, follow = "false") - - Allows the authenticating users to follow the user specified in the ID parameter. - Returns the befriended user in the requested format when successful. Returns a - string describing the failure condition when unsuccessful. If you are already - friends with the user an HTTP 403 will be returned. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to befriend. - user_id - Required. Specfies the ID of the user to befriend. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to befriend. Helpful for disambiguating when a valid screen name is also a user ID. - follow - Optional. Enable notifications for the target user in addition to becoming friends. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "user_id=%s&follow=%s" %(repr(user_id), follow) - if screen_name is not None: - apiURL = "screen_name=%s&follow=%s" %(screen_name, follow) - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create/%s.json" % (version, id), "?follow=%s" % follow)) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/create.json" % version, apiURL)) - except HTTPError as e: - # Rate limiting is done differently here for API reasons... - if e.code == 403: - raise APILimit("You've hit the update limit for this method. Try again in 24 hours.") - raise TwythonError("createFriendship() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("createFriendship() requires you to be authenticated.") - - def destroyFriendship(self, id = None, user_id = None, screen_name = None, version = None): - """destroyFriendship(id = None, user_id = None, screen_name = None) - - Allows the authenticating users to unfollow the user specified in the ID parameter. - Returns the unfollowed user in the requested format when successful. Returns a string describing the failure condition when unsuccessful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to unfollow. - user_id - Required. Specfies the ID of the user to unfollow. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to unfollow. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if user_id is not None: - apiURL = "user_id=%s" % repr(user_id) - if screen_name is not None: - apiURL = "screen_name=%s" % screen_name - try: - if id is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy/%s.json" % (version, repr(id)), "lol=1")) # Random string hack for POST reasons ;P - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/friendships/destroy.json" % version, apiURL)) - except HTTPError as e: - raise TwythonError("destroyFriendship() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("destroyFriendship() requires you to be authenticated.") - - def checkIfFriendshipExists(self, user_a, user_b, version = None): - """checkIfFriendshipExists(user_a, user_b) - - Tests for the existence of friendship between two users. - Will return true if user_a follows user_b; otherwise, it'll return false. - - Parameters: - user_a - Required. The ID or screen_name of the subject user. - user_b - Required. The ID or screen_name of the user to test for following. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - friendshipURL = "http://api.twitter.com/%d/friendships/exists.json?%s" % (version, urllib.parse.urlencode({"user_a": user_a, "user_b": user_b})) - return simplejson.load(self.opener.open(friendshipURL)) - except HTTPError as e: - raise TwythonError("checkIfFriendshipExists() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("checkIfFriendshipExists(), oddly, requires that you be authenticated.") - - def showFriendship(self, source_id = None, source_screen_name = None, target_id = None, target_screen_name = None, version = None): - """showFriendship(source_id, source_screen_name, target_id, target_screen_name) - - Returns detailed information about the relationship between two users. - - Parameters: - ** Note: One of the following is required if the request is unauthenticated - source_id - The user_id of the subject user. - source_screen_name - The screen_name of the subject user. - - ** Note: One of the following is required at all times - target_id - The user_id of the target user. - target_screen_name - The screen_name of the target user. - - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/friendships/show.json?lol=1" % version # Another quick hack, look away if you want. :D - if source_id is not None: - apiURL += "&source_id=%s" % repr(source_id) - if source_screen_name is not None: - apiURL += "&source_screen_name=%s" % source_screen_name - if target_id is not None: - apiURL += "&target_id=%s" % repr(target_id) - if target_screen_name is not None: - apiURL += "&target_screen_name=%s" % target_screen_name - try: - if self.authenticated is True: - return simplejson.load(self.opener.open(apiURL)) - else: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - # Catch this for now - if e.code == 403: - raise AuthError("You're unauthenticated, and forgot to pass a source for this method. Try again!") - raise TwythonError("showFriendship() failed with a %s error code." % repr(e.code), e.code) - - def updateDeliveryDevice(self, device_name = "none", version = None): - """updateDeliveryDevice(device_name = "none") - - Sets which device Twitter delivers updates to for the authenticating user. - Sending "none" as the device parameter will disable IM or SMS updates. (Simply calling .updateDeliveryService() also accomplishes this) - - Parameters: - device - Required. Must be one of: sms, im, none. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return self.opener.open("http://api.twitter.com/%d/account/update_delivery_device.json?" % version, urllib.parse.urlencode({"device": self.unicode2utf8(device_name)})) - except HTTPError as e: - raise TwythonError("updateDeliveryDevice() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("updateDeliveryDevice() requires you to be authenticated.") - - def updateProfileColors(self, - profile_background_color = None, - profile_text_color = None, - profile_link_color = None, - profile_sidebar_fill_color = None, - profile_sidebar_border_color = None, - version = None): - """updateProfileColors() - - Sets one or more hex values that control the color scheme of the authenticating user's profile page on api.twitter.com. - - Parameters: - ** Note: One or more of the following parameters must be present. Each parameter's value must - be a valid hexidecimal value, and may be either three or six characters (ex: #fff or #ffffff). - - profile_background_color - Optional. - profile_text_color - Optional. - profile_link_color - Optional. - profile_sidebar_fill_color - Optional. - profile_sidebar_border_color - Optional. - - 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. - """ - if self.authenticated is True: - updateProfileColorsQueryString = "?lol=2" - - def checkValidColor(str): - if len(str) != 6: - return False - for c in str: - if c not in "1234567890abcdefABCDEF": return False - - return True - - if profile_background_color is not None: - if checkValidColor(profile_background_color): - updateProfileColorsQueryString += "profile_background_color=" + profile_background_color - else: - raise TwythonError("Invalid background color. Try an hexadecimal 6 digit number.") - if profile_text_color is not None: - if checkValidColor(profile_text_color): - updateProfileColorsQueryString += "profile_text_color=" + profile_text_color - else: - raise TwythonError("Invalid text color. Try an hexadecimal 6 digit number.") - if profile_link_color is not None: - if checkValidColor(profile_link_color): - updateProfileColorsQueryString += "profile_link_color=" + profile_link_color - else: - raise TwythonError("Invalid profile link color. Try an hexadecimal 6 digit number.") - if profile_sidebar_fill_color is not None: - if checkValidColor(profile_sidebar_fill_color): - updateProfileColorsQueryString += "profile_sidebar_fill_color=" + profile_sidebar_fill_color - else: - raise TwythonError("Invalid sidebar fill color. Try an hexadecimal 6 digit number.") - if profile_sidebar_border_color is not None: - if checkValidColor(profile_sidebar_border_color): - updateProfileColorsQueryString += "profile_sidebar_border_color=" + profile_sidebar_border_color - else: - raise TwythonError("Invalid sidebar border color. Try an hexadecimal 6 digit number.") - - try: - return self.opener.open("http://api.twitter.com/%d/account/update_profile_colors.json?" % version, updateProfileColorsQueryString) - except HTTPError as e: - raise TwythonError("updateProfileColors() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("updateProfileColors() requires you to be authenticated.") - - def updateProfile(self, name = None, email = None, url = None, location = None, description = None, version = None): - """updateProfile(name = None, email = None, url = None, location = None, description = None) - - Sets values that users are able to set under the "Account" tab of their settings page. - Only the parameters specified will be updated. - - Parameters: - One or more of the following parameters must be present. Each parameter's value - should be a string. See the individual parameter descriptions below for further constraints. - - name - Optional. Maximum of 20 characters. - email - Optional. Maximum of 40 characters. Must be a valid email address. - url - Optional. Maximum of 100 characters. Will be prepended with "http://" if not present. - location - Optional. Maximum of 30 characters. The contents are not normalized or geocoded in any way. - description - Optional. Maximum of 160 characters. - - 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 = version or self.apiVersion - if self.authenticated is True: - useAmpersands = False - updateProfileQueryString = "" - if name is not None: - if len(list(name)) < 20: - updateProfileQueryString += "name=" + name - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 20 for all usernames. Try again.") - if email is not None and "@" in email: - if len(list(email)) < 40: - if useAmpersands is True: - updateProfileQueryString += "&email=" + email - else: - updateProfileQueryString += "email=" + email - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 40 for all email addresses, and the email address must be valid. Try again.") - if url is not None: - if len(list(url)) < 100: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"url": self.unicode2utf8(url)}) - else: - updateProfileQueryString += urllib.parse.urlencode({"url": self.unicode2utf8(url)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 100 for all urls. Try again.") - if location is not None: - if len(list(location)) < 30: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"location": self.unicode2utf8(location)}) - else: - updateProfileQueryString += urllib.parse.urlencode({"location": self.unicode2utf8(location)}) - useAmpersands = True - else: - raise TwythonError("Twitter has a character limit of 30 for all locations. Try again.") - if description is not None: - if len(list(description)) < 160: - if useAmpersands is True: - updateProfileQueryString += "&" + urllib.parse.urlencode({"description": self.unicode2utf8(description)}) - else: - updateProfileQueryString += urllib.parse.urlencode({"description": self.unicode2utf8(description)}) - else: - raise TwythonError("Twitter has a character limit of 160 for all descriptions. Try again.") - - if updateProfileQueryString != "": - try: - return self.opener.open("http://api.twitter.com/%d/account/update_profile.json?" % version, updateProfileQueryString) - except HTTPError as e: - raise TwythonError("updateProfile() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("updateProfile() requires you to be authenticated.") - - def getFavorites(self, page = "1", version = None): - """getFavorites(page = "1") - - Returns the 20 most recent favorite statuses for the authenticating user or user specified by the ID parameter in the requested format. - - Parameters: - page - Optional. Specifies the page of favorites to retrieve. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites.json?page=%s" % (version, repr(page)))) - except HTTPError as e: - raise TwythonError("getFavorites() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getFavorites() requires you to be authenticated.") - - def createFavorite(self, id, version = None): - """createFavorite(id) - - Favorites the status specified in the ID parameter as the authenticating user. Returns the favorite status when successful. - - Parameters: - id - Required. The ID of the status to favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/create/%s.json" % (version, repr(id)), "")) - except HTTPError as e: - raise TwythonError("createFavorite() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("createFavorite() requires you to be authenticated.") - - def destroyFavorite(self, id, version = None): - """destroyFavorite(id) - - Un-favorites the status specified in the ID parameter as the authenticating user. Returns the un-favorited status in the requested format when successful. - - Parameters: - id - Required. The ID of the status to un-favorite. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/favorites/destroy/%s.json" % (version, repr(id)), "")) - except HTTPError as e: - raise TwythonError("destroyFavorite() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("destroyFavorite() requires you to be authenticated.") - - def notificationFollow(self, id = None, user_id = None, screen_name = None, version = None): - """notificationFollow(id = None, user_id = None, screen_name = None) - - Enables device notifications for updates from the specified user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/follow/follow.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - raise TwythonError("notificationFollow() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("notificationFollow() requires you to be authenticated.") - - def notificationLeave(self, id = None, user_id = None, screen_name = None, version = None): - """notificationLeave(id = None, user_id = None, screen_name = None) - - Disables notifications for updates from the specified user to the authenticating user. Returns the specified user when successful. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - if self.authenticated is True: - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/%s.json" % (version, id) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/notifications/leave/leave.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL, "")) - except HTTPError as e: - raise TwythonError("notificationLeave() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("notificationLeave() requires you to be authenticated.") - - def getFriendsIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFriendsIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user the specified user is following. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains up to 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids/%s.json?%s" %(version, id, breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?user_id=%s&%s" %(version, repr(user_id), breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/friends/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getFriendsIDs() failed with a %s error code." % repr(e.code), e.code) - - def getFollowersIDs(self, id = None, user_id = None, screen_name = None, page = None, cursor = "-1", version = None): - """getFollowersIDs(id = None, user_id = None, screen_name = None, page = None, cursor = "-1") - - Returns an array of numeric IDs for every user following the specified user. - - Note: The previously documented page-based pagination mechanism is still in production, but please migrate to cursor-based pagination for increase reliability and performance. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Required. The ID or screen name of the user to follow with device updates. - user_id - Required. Specfies the ID of the user to follow with device updates. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Required. Specfies the screen name of the user to follow with device updates. Helpful for disambiguating when a valid screen name is also a user ID. - page - (BEING DEPRECATED) Optional. Specifies the page number of the results beginning at 1. A single page contains 5000 ids. This is recommended for users with large ID lists. If not provided all ids are returned. (Please note that the result set isn't guaranteed to be 5000 every time as suspended users will be filtered out.) - cursor - Optional. Breaks the results into pages. A single page contains 5000 ids. This is recommended for users with large ID lists. Provide a value of -1 to begin paging. Provide values as returned to in the response body's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - apiURL = "" - breakResults = "cursor=%s" % cursor - if page is not None: - breakResults = "page=%s" % page - if id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids/%s.json?%s" % (version, repr(id), breakResults) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?user_id=%s&%s" %(version, repr(user_id), breakResults) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/followers/ids.json?screen_name=%s&%s" %(version, screen_name, breakResults) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getFollowersIDs() failed with a %s error code." % repr(e.code), e.code) - - def createBlock(self, id, version = None): - """createBlock(id) - - Blocks the user specified in the ID parameter as the authenticating user. Destroys a friendship to the blocked user if it exists. - Returns the blocked user in the requested format when successful. - - Parameters: - id - The ID or screen name of a user to block. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/create/%s.json" % (version, repr(id)), "")) - except HTTPError as e: - raise TwythonError("createBlock() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("createBlock() requires you to be authenticated.") - - def destroyBlock(self, id, version = None): - """destroyBlock(id) - - Un-blocks the user specified in the ID parameter for the authenticating user. - Returns the un-blocked user in the requested format when successful. - - Parameters: - id - Required. The ID or screen_name of the user to un-block - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/destroy/%s.json" % (version, repr(id)), "")) - except HTTPError as e: - raise TwythonError("destroyBlock() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("destroyBlock() requires you to be authenticated.") - - def checkIfBlockExists(self, id = None, user_id = None, screen_name = None, version = None): - """checkIfBlockExists(id = None, user_id = None, screen_name = None) - - Returns if the authenticating user is blocking a target user. Will return the blocked user's object if a block exists, and - error with an HTTP 404 response code otherwise. - - Parameters: - ** Note: One of the following is required. (id, user_id, screen_name) - id - Optional. The ID or screen_name of the potentially blocked user. - user_id - Optional. Specfies the ID of the potentially blocked user. Helpful for disambiguating when a valid user ID is also a valid screen name. - screen_name - Optional. Specfies the screen name of the potentially blocked user. Helpful for disambiguating when a valid screen name is also a user ID. - 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 = version or self.apiVersion - apiURL = "" - if id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists/%s.json" % (version, repr(id)) - if user_id is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?user_id=%s" % (version, repr(user_id)) - if screen_name is not None: - apiURL = "http://api.twitter.com/%d/blocks/exists.json?screen_name=%s" % (version, screen_name) - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("checkIfBlockExists() failed with a %s error code." % repr(e.code), e.code) - - def getBlocking(self, page = "1", version = None): - """getBlocking(page = "1") - - Returns an array of user objects that the authenticating user is blocking. - - Parameters: - page - Optional. Specifies the page number of the results beginning at 1. A single page contains 20 ids. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking.json?page=%s" % (version, repr(page)))) - except HTTPError as e: - raise TwythonError("getBlocking() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getBlocking() requires you to be authenticated") - - def getBlockedIDs(self, version = None): - """getBlockedIDs() - - Returns an array of numeric user ids the authenticating user is blocking. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/blocks/blocking/ids.json" % version)) - except HTTPError as e: - raise TwythonError("getBlockedIDs() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getBlockedIDs() requires you to be authenticated.") - - def searchTwitter(self, search_query, **kwargs): - """searchTwitter(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - callback - Optional. Only available for JSON format. If supplied, the response will use the JSONP format with a callback of the given name. - lang - Optional. Restricts tweets to the given language, given by an ISO 639-1 code. - locale - Optional. Language of the query you're sending (only ja is currently effective). Intended for language-specific clients; default should work in most cases. - rpp - Optional. The number of tweets to return per page, up to a max of 100. - page - Optional. The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page. Note: there are pagination limits.) - since_id - Optional. Returns tweets with status ids greater than the given id. - geocode - Optional. Returns tweets by users located within a given radius of the given latitude/longitude, where the user's location is taken from their Twitter profile. The parameter value is specified by "latitide,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly. - show_user - Optional. When true, prepends ":" to the beginning of the tweet. This is useful for readers that do not display Atom's author field. The default is false. - - Usage Notes: - Queries are limited 140 URL encoded characters. - Some users may be absent from search results. - The since_id parameter will be removed from the next_page element as it is not supported for pagination. If since_id is removed a warning will be added to alert you. - This method will return an HTTP 404 error if since_id is used and is too old to be in the search index. - - Applications must have a meaningful and unique User Agent when using this method. - An HTTP Referrer is expected but not required. Search traffic that does not include a User Agent will be rate limited to fewer API calls per hour than - applications including a User Agent string. You can set your custom UA headers by passing it as a respective argument to the setup() method. - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) - try: - return simplejson.load(self.opener.open(searchURL)) - except HTTPError as e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - - def getCurrentTrends(self, excludeHashTags = False, version = None): - """getCurrentTrends(excludeHashTags = False, version = None) - - Returns the current top 10 trending topics on Twitter. The response includes the time of the request, the name of each trending topic, and the query used - on Twitter Search results page for that topic. - - Parameters: - excludeHashTags - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/current.json" % version - if excludeHashTags is True: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getCurrentTrends() failed with a %s error code." % repr(e.code), e.code) - - def getDailyTrends(self, date = None, exclude = False, version = None): - """getDailyTrends(date = None, exclude = False, version = None) - - Returns the top 20 trending topics for each hour in a given day. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/daily.json" % version - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getDailyTrends() failed with a %s error code." % repr(e.code), e.code) - - def getWeeklyTrends(self, date = None, exclude = False): - """getWeeklyTrends(date = None, exclude = False) - - Returns the top 30 trending topics for each day in a given week. - - Parameters: - date - Optional. Permits specifying a start date for the report. The date should be formatted YYYY-MM-DD. - exclude - Optional. Setting this equal to hashtags will remove all hashtags from the trends list. - 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 = version or self.apiVersion - apiURL = "http://api.twitter.com/%d/trends/daily.json" % version - questionMarkUsed = False - if date is not None: - apiURL += "?date=%s" % date - questionMarkUsed = True - if exclude is True: - if questionMarkUsed is True: - apiURL += "&exclude=hashtags" - else: - apiURL += "?exclude=hashtags" - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError as e: - raise TwythonError("getWeeklyTrends() failed with a %s error code." % repr(e.code), e.code) - - def getSavedSearches(self, version = None): - """getSavedSearches() - - Returns the authenticated user's saved search queries. - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches.json" % version)) - except HTTPError as e: - raise TwythonError("getSavedSearches() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("getSavedSearches() requires you to be authenticated.") - - def showSavedSearch(self, id, version = None): - """showSavedSearch(id) - - Retrieve the data for a saved search owned by the authenticating user specified by the given id. - - Parameters: - id - Required. The id of the saved search to be retrieved. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/show/%s.json" % (version, repr(id)))) - except HTTPError as e: - raise TwythonError("showSavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("showSavedSearch() requires you to be authenticated.") - - def createSavedSearch(self, query, version = None): - """createSavedSearch(query) - - Creates a saved search for the authenticated user. - - Parameters: - query - Required. The query of the search the user would like to save. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/create.json?query=%s" % (version, query), "")) - except HTTPError as e: - raise TwythonError("createSavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("createSavedSearch() requires you to be authenticated.") - - def destroySavedSearch(self, id, version = None): - """ destroySavedSearch(id) - - Destroys a saved search for the authenticated user. - The search specified by id must be owned by the authenticating user. - - Parameters: - id - Required. The id of the saved search to be deleted. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/saved_searches/destroy/%s.json" % (version, repr(id)), "")) - except HTTPError as e: - raise TwythonError("destroySavedSearch() failed with a %s error code." % repr(e.code), e.code) - else: - raise AuthError("destroySavedSearch() requires you to be authenticated.") - - def createList(self, name, mode = "public", description = "", version = None): - """ createList(self, name, mode, description, version) - - Creates a new list for the currently authenticated user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - name - Required. The name for the new list. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username), - urllib.parse.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError as e: - raise TwythonError("createList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("createList() requires you to be authenticated.") - - def updateList(self, list_id, name, mode = "public", description = "", version = None): - """ updateList(self, list_id, name, mode, description, version) - - Updates an existing list for the authenticating user. (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - This method is a bit cumbersome for the time being; I'd personally avoid using it unless you're positive you know what you're doing. Twitter should really look - at this... - - Parameters: - list_id - Required. The name of the list (this gets turned into a slug - e.g, "Huck Hound" becomes "huck-hound"). - name - Required. The name of the list, possibly for renaming or such. - description - Optional, in the sense that you can leave it blank if you don't want one. ;) - mode - Optional. This is a string indicating "public" or "private", defaults to "public". - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), - urllib.parse.urlencode({"name": name, "mode": mode, "description": description}))) - except HTTPError as e: - raise TwythonError("updateList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("updateList() requires you to be authenticated.") - - def showLists(self, version = None): - """ showLists(self, version) - - Show all the lists for the currently authenticated user (i.e, they own these lists). - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists.json" % (version, self.username))) - except HTTPError as e: - raise TwythonError("showLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("showLists() requires you to be authenticated.") - - def getListMemberships(self, version = None): - """ getListMemberships(self, version) - - Get all the lists for the currently authenticated user (i.e, they're on all the lists that are returned, the lists belong to other people) - (Note: This may encounter issues if you authenticate with an email; try username (screen name) instead). - - Parameters: - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/followers.json" % (version, self.username))) - except HTTPError as e: - raise TwythonError("getLists() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("getLists() requires you to be authenticated.") - - def deleteList(self, list_id, version = None): - """ deleteList(self, list_id, version) - - Deletes a list for the authenticating user. - - Parameters: - list_id - Required. The name of the list to delete - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError as e: - raise TwythonError("deleteList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("deleteList() requires you to be authenticated.") - - def getListTimeline(self, list_id, cursor = "-1", version = None, **kwargs): - """ getListTimeline(self, list_id, cursor, version, **kwargs) - - Retrieves a timeline representing everyone in the list specified. - - Parameters: - list_id - Required. The name of the list to get a timeline for - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - since_id - Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified ID. - max_id - Optional. Returns only statuses with an ID less than (that is, older than) or equal to the specified ID. - count - Optional. Specifies the number of statuses to retrieve. May not be greater than 200. - cursor - Optional. Breaks the results into pages. Provide a value of -1 to begin paging. - Provide values returned in the response's "next_cursor" and "previous_cursor" attributes to page back and forth in the list. - 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 = version or self.apiVersion - try: - baseURL = self.constructApiURL("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id), kwargs) - return simplejson.load(self.opener.open(baseURL + "&cursor=%s" % cursor)) - except HTTPError as e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getListTimeline() failed with a %d error code." % e.code, e.code) - - def getSpecificList(self, list_id, version = None): - """ getSpecificList(self, list_id, version) - - Retrieve a specific list - this only requires authentication if the list you're requesting is protected/private (if it is, you need to have access as well). - - Parameters: - list_id - Required. The name of the list to get - this gets turned into a slug, so you can pass it as that, or hope the transformation works out alright. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/lists/%s/statuses.json" % (version, self.username, list_id))) - except HTTPError as e: - if e.code == 404: - raise AuthError("It seems the list you're trying to access is private/protected, and you don't have access. Are you authenticated and allowed?") - raise TwythonError("getSpecificList() failed with a %d error code." % e.code, e.code) - - def addListMember(self, list_id, version = None): - """ addListMember(self, list_id, id, version) - - Adds a new Member (the passed in id) to the specified list. - - Parameters: - list_id - Required. The slug of the list to add the new member to. - id - Required. The ID of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "id=%s" % repr(id))) - except HTTPError as e: - raise TwythonError("addListMember() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("addListMember requires you to be authenticated.") - - def getListMembers(self, list_id, version = None): - """ getListMembers(self, list_id, version = None) - - Show all members of a specified list. This method requires authentication if the list is private/protected. - - Parameters: - list_id - Required. The slug of the list to retrieve members for. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id))) - except HTTPError as e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - - def removeListMember(self, list_id, id, version = None): - """ removeListMember(self, list_id, id, version) - - Remove the specified user (id) from the specified list (list_id). Requires you to be authenticated and in control of the list in question. - - Parameters: - list_id - Required. The slug of the list to remove the specified user from. - id - Required. The ID of the user that's being added to the list. - 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 = version or self.apiVersion - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members.json" % (version, self.username, list_id), "_method=DELETE&id=%s" % repr(id))) - except HTTPError as e: - raise TwythonError("getListMembers() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("removeListMember() requires you to be authenticated.") - - def isListMember(self, list_id, id, version = None): - """ isListMember(self, list_id, id, version) - - 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, self.username, list_id, repr(id)))) - except HTTPError as e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def subscribeToList(self, list_id, version): - """ subscribeToList(self, list_id, version) - - Subscribe the authenticated user to the list provided (must be public). - - Parameters: - list_id - Required. The list to subscribe 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "")) - except HTTPError as e: - raise TwythonError("subscribeToList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("subscribeToList() requires you to be authenticated.") - - def unsubscribeFromList(self, list_id, version): - """ unsubscribeFromList(self, list_id, version) - - Unsubscribe the authenticated user from the list in question (must be public). - - Parameters: - list_id - Required. The list to unsubscribe from. - 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. - """ - if self.authenticated is True: - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following.json" % (version, self.username, list_id), "_method=DELETE")) - except HTTPError as e: - raise TwythonError("unsubscribeFromList() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("unsubscribeFromList() requires you to be authenticated.") - - def isListSubscriber(self, list_id, id, version = None): - """ isListSubscriber(self, list_id, id, version) - - Check if a specified user (id) is a subscriber 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. - - Parameters: - list_id - Required. The slug of the list to check against. - id - Required. The ID of the user being checked in the list. - 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 = version or self.apiVersion - try: - if self.authenticated is True: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) - else: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, self.username, list_id, repr(id)))) - except HTTPError as e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def availableTrends(self, latitude = None, longitude = None, version = None): - """ availableTrends(latitude, longitude, version): - - Gets all available trends, optionally filtering by geolocation based stuff. - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - latitude (string) - Optional. A latitude to sort by. - longitude (string) - Optional. A longitude to sort by. - 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 = version or self.apiVersion - try: - if latitude is not None and longitude is not None: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json?latitude=%s&longitude=%s" % (version, latitude, longitude))) - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/available.json" % version)) - except HTTPError as e: - raise TwythonError("availableTrends() failed with a %d error code." % e.code, e.code) - - def trendsByLocation(self, woeid, version = None): - """ trendsByLocation(woeid, version): - - Gets all available trends, filtering by geolocation (woeid - see http://developer.yahoo.com/geo/geoplanet/guide/concepts.html). - - Note: If you choose to pass a latitude/longitude, keep in mind that you have to pass both - one won't work by itself. ;P - - Parameters: - woeid (string) - Required. WoeID of the area you're searching in. - 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 = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/trends/%s.json" % (version, woeid))) - except HTTPError as e: - raise TwythonError("trendsByLocation() 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. - def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """ updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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 = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.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) - return self.opener.open(r).read() - except HTTPError as e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename, version = None): - """ updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.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) - return self.opener.open(r).read() - except HTTPError as e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a profile image, right?") - - def encode_multipart_formdata(self, fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % self.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def get_content_type(self, filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - def unicode2utf8(self, text): - try: - if isinstance(text, str): - text = text.encode('utf-8') - except: - pass - return text diff --git a/twython3k/twitter_endpoints.py b/twython3k/twitter_endpoints.py new file mode 100644 index 0000000..42bd5a1 --- /dev/null +++ b/twython3k/twitter_endpoints.py @@ -0,0 +1,299 @@ +""" + 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: + + {{version}}, etc + + 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. + + 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). +""" + +# Base Twitter API url, no need to repeat this junk... +base_url = 'http://api.twitter.com/{{version}}' + +api_table = { + 'getRateLimitStatus': { + 'url': '/account/rate_limit_status.json', + 'method': 'GET', + }, + + # Timeline methods + 'getPublicTimeline': { + 'url': '/statuses/public_timeline.json', + 'method': 'GET', + }, + 'getHomeTimeline': { + 'url': '/statuses/home_timeline.json', + 'method': 'GET', + }, + 'getUserTimeline': { + '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', + }, + 'destroyFriendship': { + 'url': '/friendships/destroy.json', + 'method': 'POST', + }, + 'getFriendsIDs': { + 'url': '/friends/ids.json', + 'method': 'GET', + }, + 'getFollowersIDs': { + 'url': '/followers/ids.json', + 'method': 'GET', + }, + + # Retweets + 'reTweet': { + 'url': '/statuses/retweet/{{id}}.json', + 'method': 'POST', + }, + 'getRetweets': { + 'url': '/statuses/retweets/{{id}}.json', + 'method': 'GET', + }, + 'retweetedOfMe': { + 'url': '/statuses/retweets_of_me.json', + 'method': 'GET', + }, + 'retweetedByMe': { + 'url': '/statuses/retweeted_by_me.json', + 'method': 'GET', + }, + 'retweetedToMe': { + 'url': '/statuses/retweeted_to_me.json', + 'method': 'GET', + }, + + # User methods + 'showUser': { + 'url': '/users/show.json', + 'method': 'GET', + }, + 'searchUsers': { + 'url': '/users/search.json', + 'method': 'GET', + }, + + # Status methods - showing, updating, destroying, etc. + 'showStatus': { + 'url': '/statuses/show/{{id}}.json', + 'method': 'GET', + }, + 'updateStatus': { + 'url': '/statuses/update.json', + 'method': 'POST', + }, + 'destroyStatus': { + 'url': '/statuses/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Direct Messages - getting, sending, effing, etc. + 'getDirectMessages': { + 'url': '/direct_messages.json', + 'method': 'GET', + }, + 'getSentMessages': { + 'url': '/direct_messages/sent.json', + 'method': 'GET', + }, + 'sendDirectMessage': { + 'url': '/direct_messages/new.json', + 'method': 'POST', + }, + 'destroyDirectMessage': { + 'url': '/direct_messages/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Friendship methods + 'checkIfFriendshipExists': { + 'url': '/friendships/exists.json', + 'method': 'GET', + }, + 'showFriendship': { + 'url': '/friendships/show.json', + 'method': 'GET', + }, + + # Profile methods + 'updateProfile': { + 'url': '/account/update_profile.json', + 'method': 'POST', + }, + 'updateProfileColors': { + 'url': '/account/update_profile_colors.json', + 'method': 'POST', + }, + + # Favorites methods + 'getFavorites': { + 'url': '/favorites.json', + 'method': 'GET', + }, + 'createFavorite': { + 'url': '/favorites/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyFavorite': { + 'url': '/favorites/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Blocking methods + 'createBlock': { + 'url': '/blocks/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyBlock': { + 'url': '/blocks/destroy/{{id}}.json', + 'method': 'POST', + }, + 'getBlocking': { + 'url': '/blocks/blocking.json', + 'method': 'GET', + }, + 'getBlockedIDs': { + 'url': '/blocks/blocking/ids.json', + 'method': 'GET', + }, + 'checkIfBlockExists': { + 'url': '/blocks/exists.json', + 'method': 'GET', + }, + + # Trending methods + 'getCurrentTrends': { + 'url': '/trends/current.json', + 'method': 'GET', + }, + 'getDailyTrends': { + 'url': '/trends/daily.json', + 'method': 'GET', + }, + 'getWeeklyTrends': { + 'url': '/trends/weekly.json', + 'method': 'GET', + }, + 'availableTrends': { + 'url': '/trends/available.json', + 'method': 'GET', + }, + 'trendsByLocation': { + 'url': '/trends/{{woeid}}.json', + 'method': 'GET', + }, + + # Saved Searches + 'getSavedSearches': { + 'url': '/saved_searches.json', + 'method': 'GET', + }, + 'showSavedSearch': { + 'url': '/saved_searches/show/{{id}}.json', + 'method': 'GET', + }, + 'createSavedSearch': { + 'url': '/saved_searches/create.json', + 'method': 'GET', + }, + 'destroySavedSearch': { + 'url': '/saved_searches/destroy/{{id}}.json', + 'method': 'GET', + }, + + # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P + 'createList': { + 'url': '/{{username}}/lists.json', + 'method': 'POST', + }, + 'updateList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'POST', + }, + 'showLists': { + 'url': '/{{username}}/lists.json', + 'method': 'GET', + }, + 'getListMemberships': { + 'url': '/{{username}}/lists/followers.json', + 'method': 'GET', + }, + 'deleteList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'DELETE', + }, + 'getListTimeline': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'getSpecificList': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'addListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'POST', + }, + 'getListMembers': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'GET', + }, + 'deleteListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'DELETE', + }, + 'subscribeToList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'POST', + }, + 'unsubscribeFromList': { + 'url': '/{{username}}/{{list_id}}/following.json', + 'method': 'DELETE', + }, + + # The one-offs + 'notificationFollow': { + 'url': '/notifications/follow/follow.json', + 'method': 'POST', + }, + 'notificationLeave': { + 'url': '/notifications/leave/leave.json', + 'method': 'POST', + }, + 'updateDeliveryService': { + 'url': '/account/update_delivery_device.json', + 'method': 'POST', + }, + 'reportSpam': { + 'url': '/report_spam.json', + 'method': 'POST', + }, +} diff --git a/twython3k/twython.py b/twython3k/twython.py new file mode 100644 index 0000000..5efa1e7 --- /dev/null +++ b/twython3k/twython.py @@ -0,0 +1,417 @@ +#!/usr/bin/python + +""" + Twython is a library for Python that wraps the Twitter API. + It aims to abstract away all the API endpoints, so that additions to the library + and/or the Twitter API won't cause any overall problems. + + Questions, comments? ryan@venodesigns.net +""" + +__author__ = "Ryan McGrath " +__version__ = "1.3" + +import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.error, urllib.parse +import urllib.parse +import http.client +import httplib2 +import mimetypes +import mimetools +import re + +import oauth2 as oauth + +# 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 urllib.error import HTTPError + +# There are some special setups (like, oh, a Django application) where +# 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 +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 + 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. + from django.utils import simplejson + except: + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +class TwythonError(Exception): + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by APILimit and AuthError. + + Note: To use these, the syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: + + from twython import TwythonError, APILimit, AuthError + """ + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) + + 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 + 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 __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 + your authentication. + """ + def __init__(self, msg): + self.msg = msg + + 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, headers=None): + """setup(self, oauth_token = None, headers = None) + + Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). + + Parameters: + twitter_token - Given to you when you register your application with Twitter. + twitter_secret - Given to you when you register your application with Twitter. + oauth_token - If you've gone through the authentication process and have a token for this user, + pass it in and it'll be used for all requests going forward. + oauth_token_secret - see oauth_token; it's the other half. + headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + + ** 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.twitter_token = twitter_token + self.twitter_secret = twitter_secret + self.oauth_token = oauth_token + self.oauth_secret = oauth_token_secret + + # If there's headers, set them, otherwise be an embarassing parent for their own good. + self.headers = headers + if self.headers is None: + headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + + consumer = None + token = None + + if self.twitter_token is not None and self.twitter_secret is not None: + consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + + if self.oauth_token is not None and self.oauth_secret is not None: + token = oauth.Token(oauth_token, oauth_token_secret) + + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. + if consumer is not None and token is not None: + self.client = oauth.Client(consumer, token) + elif consumer is not None: + self.client = oauth.Client(consumer) + else: + # If they don't do authentication, but still want to request unprotected resources, we need an opener. + self.client = httplib2.Http() + + def __getattr__(self, api_call): + """ + The most magically awesome block of code you'll see in 2010. + + Rather than list out 9 million damn methods for this API, we just keep a table (see above) of + every API endpoint and their corresponding function id for this library. This pretty much gives + unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is + going to be your bottleneck... well, don't use Python. ;P + + For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). + It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, + we can take over and find the API method in our table. We then return a function that downloads and parses + what we're looking for, based on the keywords passed in. + + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". + """ + def get(self, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) + + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(kwargs)) + else: + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.items()]) + resp, content = self.client.request(url, fn['method']) + + return simplejson.loads(content) + + if api_call in api_table: + return get.__get__(self) + else: + raise AttributeError(api_call) + + def get_authentication_tokens(self): + """ + get_auth_url(self) + + Returns an authorization URL for a user to hit. + """ + resp, content = self.client.request(self.request_token_url, "GET") + + if resp['status'] != '200': + raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + + request_tokens = dict(urllib.parse.parse_qsl(content)) + request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + return request_tokens + + def get_authorized_tokens(self): + """ + get_authorized_tokens + + Returns authorized tokens after they go through the auth_url phase. + """ + resp, content = self.client.request(self.access_token_url, "GET") + return dict(urllib.parse.parse_qsl(content)) + + # ------------------------------------------------------------------------------------------------------------------------ + # The following methods are all different in some manner or require special attention with regards to the Twitter API. + # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, + # but it's not high on the priority list at the moment. + # ------------------------------------------------------------------------------------------------------------------------ + + @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: + resp, content = self.client.request( + shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)}), + "GET" + ) + return content + except HTTPError as e: + raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) + + def bulkUserLookup(self, ids = None, screen_names = None, version = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + + 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. + + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += repr(id) + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + resp, content = self.client.request(apiURL, "GET") + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) + + def searchTwitter(self, **kwargs): + """searchTwitter(search_query, **kwargs) + + Returns tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) + try: + resp, content = self.client.request(searchURL, "GET") + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) + + def searchTwitterGen(self, **kwargs): + """searchTwitterGen(search_query, **kwargs) + + Returns a generator of tweets that match a specified query. + + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) + try: + resp, content = self.client.request(searchURL, "GET") + data = simplejson.loads(content) + except HTTPError as e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code) + + if not data['results']: + raise StopIteration + + for tweet in data['results']: + yield tweet + + if 'page' not in kwargs: + kwargs['page'] = 2 + else: + kwargs['page'] += 1 + + for tweet in self.searchTwitterGen(search_query, **kwargs): + yield tweet + + def isListMember(self, list_id, id, username, version = 1): + """ isListMember(self, list_id, id, version) + + 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. + + 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. + """ + try: + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id))) + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + + def isListSubscriber(self, list_id, id, version = 1): + """ isListSubscriber(self, list_id, id, version) + + Check if a specified user (id) is a subscriber 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. + + 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. + """ + try: + resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)) + return simplejson.loads(content) + except HTTPError as e: + 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. + def updateProfileBackgroundImage(self, filename, tile="true", version = 1): + """ updateProfileBackgroundImage(filename, tile="true") + + Updates the authenticating user's profile background image. + + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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: + files = [("image", filename, open(filename, 'rb').read())] + 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) + return urllib.request.urlopen(r).read() + except HTTPError as e: + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + + def updateProfileImage(self, filename, version = 1): + """ updateProfileImage(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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. + """ + try: + files = [("image", filename, open(filename, 'rb').read())] + 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) + return urllib.request.urlopen(r).read() + except HTTPError as e: + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + @staticmethod + def encode_multipart_formdata(fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % Twython.get_content_type(filename)) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + @staticmethod + def get_content_type(filename): + """ get_content_type(self, filename) + + Exactly what you think it does. :D + """ + return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + + @staticmethod + def unicode2utf8(text): + try: + if isinstance(text, str): + text = text.encode('utf-8') + except: + pass + return text \ No newline at end of file From 9c94da40317c1803a271eeacc8e30c3dce53b31d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 16 Oct 2010 23:40:24 -0400 Subject: [PATCH 174/687] Fix one final piece --- README.markdown | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 8357239..6eb928f 100644 --- a/README.markdown +++ b/README.markdown @@ -45,7 +45,11 @@ Example Use twitter = Twython() results = twitter.searchTwitter(q="bert") - + + # More function definitions can be found by reading over twython/twitter_endpoints.py, as well + # as skimming the source file. Both are kept human-readable, and are pretty well documented or + # very self documenting. + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored From 95fb03d80cb1e97e75b53dc01a6131f17150c59c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 16 Oct 2010 23:43:55 -0400 Subject: [PATCH 175/687] Removing useless files --- .../twitter/twitter_endpoints.py | 299 ------------- oauth_django_example/twitter/twython.py | 421 ------------------ 2 files changed, 720 deletions(-) delete mode 100644 oauth_django_example/twitter/twitter_endpoints.py delete mode 100644 oauth_django_example/twitter/twython.py diff --git a/oauth_django_example/twitter/twitter_endpoints.py b/oauth_django_example/twitter/twitter_endpoints.py deleted file mode 100644 index 42bd5a1..0000000 --- a/oauth_django_example/twitter/twitter_endpoints.py +++ /dev/null @@ -1,299 +0,0 @@ -""" - 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: - - {{version}}, etc - - 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. - - 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). -""" - -# Base Twitter API url, no need to repeat this junk... -base_url = 'http://api.twitter.com/{{version}}' - -api_table = { - 'getRateLimitStatus': { - 'url': '/account/rate_limit_status.json', - 'method': 'GET', - }, - - # Timeline methods - 'getPublicTimeline': { - 'url': '/statuses/public_timeline.json', - 'method': 'GET', - }, - 'getHomeTimeline': { - 'url': '/statuses/home_timeline.json', - 'method': 'GET', - }, - 'getUserTimeline': { - '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', - }, - 'destroyFriendship': { - 'url': '/friendships/destroy.json', - 'method': 'POST', - }, - 'getFriendsIDs': { - 'url': '/friends/ids.json', - 'method': 'GET', - }, - 'getFollowersIDs': { - 'url': '/followers/ids.json', - 'method': 'GET', - }, - - # Retweets - 'reTweet': { - 'url': '/statuses/retweet/{{id}}.json', - 'method': 'POST', - }, - 'getRetweets': { - 'url': '/statuses/retweets/{{id}}.json', - 'method': 'GET', - }, - 'retweetedOfMe': { - 'url': '/statuses/retweets_of_me.json', - 'method': 'GET', - }, - 'retweetedByMe': { - 'url': '/statuses/retweeted_by_me.json', - 'method': 'GET', - }, - 'retweetedToMe': { - 'url': '/statuses/retweeted_to_me.json', - 'method': 'GET', - }, - - # User methods - 'showUser': { - 'url': '/users/show.json', - 'method': 'GET', - }, - 'searchUsers': { - 'url': '/users/search.json', - 'method': 'GET', - }, - - # Status methods - showing, updating, destroying, etc. - 'showStatus': { - 'url': '/statuses/show/{{id}}.json', - 'method': 'GET', - }, - 'updateStatus': { - 'url': '/statuses/update.json', - 'method': 'POST', - }, - 'destroyStatus': { - 'url': '/statuses/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Direct Messages - getting, sending, effing, etc. - 'getDirectMessages': { - 'url': '/direct_messages.json', - 'method': 'GET', - }, - 'getSentMessages': { - 'url': '/direct_messages/sent.json', - 'method': 'GET', - }, - 'sendDirectMessage': { - 'url': '/direct_messages/new.json', - 'method': 'POST', - }, - 'destroyDirectMessage': { - 'url': '/direct_messages/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Friendship methods - 'checkIfFriendshipExists': { - 'url': '/friendships/exists.json', - 'method': 'GET', - }, - 'showFriendship': { - 'url': '/friendships/show.json', - 'method': 'GET', - }, - - # Profile methods - 'updateProfile': { - 'url': '/account/update_profile.json', - 'method': 'POST', - }, - 'updateProfileColors': { - 'url': '/account/update_profile_colors.json', - 'method': 'POST', - }, - - # Favorites methods - 'getFavorites': { - 'url': '/favorites.json', - 'method': 'GET', - }, - 'createFavorite': { - 'url': '/favorites/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyFavorite': { - 'url': '/favorites/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Blocking methods - 'createBlock': { - 'url': '/blocks/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyBlock': { - 'url': '/blocks/destroy/{{id}}.json', - 'method': 'POST', - }, - 'getBlocking': { - 'url': '/blocks/blocking.json', - 'method': 'GET', - }, - 'getBlockedIDs': { - 'url': '/blocks/blocking/ids.json', - 'method': 'GET', - }, - 'checkIfBlockExists': { - 'url': '/blocks/exists.json', - 'method': 'GET', - }, - - # Trending methods - 'getCurrentTrends': { - 'url': '/trends/current.json', - 'method': 'GET', - }, - 'getDailyTrends': { - 'url': '/trends/daily.json', - 'method': 'GET', - }, - 'getWeeklyTrends': { - 'url': '/trends/weekly.json', - 'method': 'GET', - }, - 'availableTrends': { - 'url': '/trends/available.json', - 'method': 'GET', - }, - 'trendsByLocation': { - 'url': '/trends/{{woeid}}.json', - 'method': 'GET', - }, - - # Saved Searches - 'getSavedSearches': { - 'url': '/saved_searches.json', - 'method': 'GET', - }, - 'showSavedSearch': { - 'url': '/saved_searches/show/{{id}}.json', - 'method': 'GET', - }, - 'createSavedSearch': { - 'url': '/saved_searches/create.json', - 'method': 'GET', - }, - 'destroySavedSearch': { - 'url': '/saved_searches/destroy/{{id}}.json', - 'method': 'GET', - }, - - # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P - 'createList': { - 'url': '/{{username}}/lists.json', - 'method': 'POST', - }, - 'updateList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'POST', - }, - 'showLists': { - 'url': '/{{username}}/lists.json', - 'method': 'GET', - }, - 'getListMemberships': { - 'url': '/{{username}}/lists/followers.json', - 'method': 'GET', - }, - 'deleteList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'DELETE', - }, - 'getListTimeline': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'getSpecificList': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'addListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'POST', - }, - 'getListMembers': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'GET', - }, - 'deleteListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'DELETE', - }, - 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/following.json', - 'method': 'POST', - }, - 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/following.json', - 'method': 'DELETE', - }, - - # The one-offs - 'notificationFollow': { - 'url': '/notifications/follow/follow.json', - 'method': 'POST', - }, - 'notificationLeave': { - 'url': '/notifications/leave/leave.json', - 'method': 'POST', - }, - 'updateDeliveryService': { - 'url': '/account/update_delivery_device.json', - 'method': 'POST', - }, - 'reportSpam': { - 'url': '/report_spam.json', - 'method': 'POST', - }, -} diff --git a/oauth_django_example/twitter/twython.py b/oauth_django_example/twitter/twython.py deleted file mode 100644 index eb96be7..0000000 --- a/oauth_django_example/twitter/twython.py +++ /dev/null @@ -1,421 +0,0 @@ -#!/usr/bin/python - -""" - Twython is a library for Python that wraps the Twitter API. - It aims to abstract away all the API endpoints, so that additions to the library - and/or the Twitter API won't cause any overall problems. - - Questions, comments? ryan@venodesigns.net -""" - -__author__ = "Ryan McGrath " -__version__ = "1.3" - -import urllib -import urllib2 -import urlparse -import httplib -import httplib2 -import mimetypes -import mimetools -import re - -import oauth2 as oauth - -# 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 urllib2 import HTTPError - -# There are some special setups (like, oh, a Django application) where -# 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 -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 - 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. - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -class TwythonError(Exception): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by APILimit and AuthError. - - Note: To use these, the syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: - - from twython import TwythonError, APILimit, AuthError - """ - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - - 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 - 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 __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 - your authentication. - """ - def __init__(self, msg): - self.msg = msg - - 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, headers=None): - """setup(self, oauth_token = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - twitter_token - Given to you when you register your application with Twitter. - twitter_secret - Given to you when you register your application with Twitter. - oauth_token - If you've gone through the authentication process and have a token for this user, - pass it in and it'll be used for all requests going forward. - oauth_token_secret - see oauth_token; it's the other half. - headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} - - ** 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.twitter_token = twitter_token - self.twitter_secret = twitter_secret - self.oauth_token = oauth_token - self.oauth_secret = oauth_token_secret - - # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers - if self.headers is None: - headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - - consumer = None - token = None - - if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - - if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) - - # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token) - elif consumer is not None: - self.client = oauth.Client(consumer) - else: - # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http() - - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. - - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P - - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. - - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs)) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) - resp, content = self.client.request(url, fn['method']) - - return simplejson.loads(content) - - if api_call in api_table: - return get.__get__(self) - else: - raise AttributeError, api_call - - def get_authentication_tokens(self): - """ - get_auth_url(self) - - Returns an authorization URL for a user to hit. - """ - resp, content = self.client.request(self.request_token_url, "GET") - - if resp['status'] != '200': - raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - - request_tokens = dict(urlparse.parse_qsl(content)) - request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) - return request_tokens - - def get_authorized_tokens(self): - """ - get_authorized_tokens - - Returns authorized tokens after they go through the auth_url phase. - """ - resp, content = self.client.request(self.access_token_url, "GET") - return dict(urlparse.parse_qsl(content)) - - - @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: - resp, content = self.client.request( - shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)}), - "GET" - ) - 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 = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - - 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. - - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - version = version or self.apiVersion - if self.authenticated is True: - apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += `id` + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," - try: - return simplejson.load(self.opener.open(apiURL)) - except HTTPError, e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) - else: - raise AuthError("bulkUserLookup() requires you to be authenticated.") - - def searchTwitter(self, **kwargs): - """searchTwitter(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - - e.g x.searchTwitter(q="jjndf", page="2") - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - return simplejson.load(self.opener.open(searchURL)) - except HTTPError, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - - def searchTwitterGen(self, **kwargs): - """searchTwitterGen(search_query, **kwargs) - - Returns a generator of tweets that match a specified query. - - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - - e.g x.searchTwitter(q="jjndf", page="2") - """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) - try: - data = simplejson.load(self.opener.open(searchURL)) - except HTTPError, e: - raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) - - if not data['results']: - raise StopIteration - - for tweet in data['results']: - yield tweet - - if 'page' not in kwargs: - kwargs['page'] = 2 - else: - kwargs['page'] += 1 - - for tweet in self.searchTwitterGen(search_query, **kwargs): - yield tweet - - def isListMember(self, list_id, id, username, version = None): - """ isListMember(self, list_id, id, version) - - 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. - - 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. - """ - version = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`))) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - - def isListSubscriber(self, list_id, id, version = None): - """ isListSubscriber(self, list_id, id, version) - - Check if a specified user (id) is a subscriber 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. - - 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. - """ - version = version or self.apiVersion - try: - return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`))) - except HTTPError, e: - 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. - def updateProfileBackgroundImage(self, filename, tile="true", version = None): - """ updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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 = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = self.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a background image, right?") - - def updateProfileImage(self, filename, version = None): - """ updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - version = version or self.apiVersion - if self.authenticated is True: - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return self.opener.open(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - else: - raise AuthError("You realize you need to be authenticated to change a profile image, right?") - - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % Twython.get_content_type(filename)) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - @staticmethod - def get_content_type(filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - @staticmethod - def unicode2utf8(text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text From 25eda807abbf3ccba0a2ea2abecd0c7e133e114c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 17 Oct 2010 01:44:49 -0400 Subject: [PATCH 176/687] Oh yeah, forgot to strip these... --- oauth_django_example/twitter/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth_django_example/twitter/views.py b/oauth_django_example/twitter/views.py index 14d8e69..cb3f182 100644 --- a/oauth_django_example/twitter/views.py +++ b/oauth_django_example/twitter/views.py @@ -7,8 +7,8 @@ from django.contrib.auth.decorators import login_required from twython import Twython from twitter.models import Profile -CONSUMER_KEY = "piKE9TwKoAhJoj7KEMlwGQ" -CONSUMER_SECRET = "RA9IzvvzoLAFGOOoOndm1Cvyh94pwPWLy4Grl4dt0o" +CONSUMER_KEY = "YOUR CONSUMER KEY HERE" +CONSUMER_SECRET = "YOUR CONSUMER SECRET HERE" def twitter_logout(request): logout(request) From 3cef1a463f3aa431c84c97935879405d3b9902a2 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 19 Oct 2010 16:31:09 -0400 Subject: [PATCH 177/687] Redid some examples, moved examples to core_examples to differentiate from oauth_django_example, version bump to 1.3.2 to fix distribution errors with Pip/etc (dumb binaries were being created earlier, just throwing out the source now and letting pip handle it) --- MANIFEST.in | 2 +- README.txt | 86 +++++++++++++++++++ {examples => core_examples}/current_trends.py | 4 +- {examples => core_examples}/daily_trends.py | 4 +- .../get_user_timeline.py | 4 +- .../public_timeline.py | 4 +- core_examples/search_results.py | 8 ++ core_examples/shorten_url.py | 6 ++ core_examples/update_profile_image.py | 9 ++ core_examples/update_status.py | 13 +++ {examples => core_examples}/weekly_trends.py | 4 +- examples/get_friends_timeline.py | 8 -- examples/get_user_mention.py | 6 -- examples/rate_limit.py | 10 --- examples/search_results.py | 8 -- examples/shorten_url.py | 7 -- examples/twython_setup.py | 7 -- examples/update_profile_image.py | 5 -- examples/update_status.py | 5 -- setup.py | 2 +- twython/twython.py | 4 +- twython3k/twython.py | 4 +- 22 files changed, 138 insertions(+), 72 deletions(-) create mode 100644 README.txt rename {examples => core_examples}/current_trends.py (64%) rename {examples => core_examples}/daily_trends.py (63%) rename {examples => core_examples}/get_user_timeline.py (72%) rename {examples => core_examples}/public_timeline.py (74%) create mode 100644 core_examples/search_results.py create mode 100644 core_examples/shorten_url.py create mode 100644 core_examples/update_profile_image.py create mode 100644 core_examples/update_status.py rename {examples => core_examples}/weekly_trends.py (63%) delete mode 100644 examples/get_friends_timeline.py delete mode 100644 examples/get_user_mention.py delete mode 100644 examples/rate_limit.py delete mode 100644 examples/search_results.py delete mode 100644 examples/shorten_url.py delete mode 100644 examples/twython_setup.py delete mode 100644 examples/update_profile_image.py delete mode 100644 examples/update_status.py diff --git a/MANIFEST.in b/MANIFEST.in index 0d9cce8..0878b48 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.markdown +include LICENSE README.markdown README.txt recursive-include examples * recursive-exclude examples *.pyc diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..6eb928f --- /dev/null +++ b/README.txt @@ -0,0 +1,86 @@ +Twython - Easy Twitter utilities in Python +========================================================================================= +Ah, Twitter, your API used to be so awesome, before you went and implemented the crap known +as OAuth 1.0. However, since you decided to force your entire development community over a barrel +about it, I suppose Twython has to support this. So, that said... + +If you used this library and it all stopped working, it's because of the Authentication method change. +========================================================================================================= +Twitter recently disabled the use of "Basic Authentication", which is why, if you used Twython previously, +you probably started getting a ton of 401 errors. To fix this, we should note one thing... + +You need to change how authentication works in your program/application. If you're using a command line +application or something, you'll probably languish in hell for a bit, because OAuth wasn't really designed +for those types of use cases. Twython cannot help you with that or fix the annoying parts of OAuth. + +If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. +Enjoy! + +Requirements +----------------------------------------------------------------------------------------------------- +Twython (for versions of Python before 2.6) requires a library called +"simplejson". Depending on your flavor of package manager, you can do the following... + + (pip install | easy_install) simplejson + +Twython also requires the (most excellent) OAuth2 library for handling OAuth tokens/signing/etc. Again... + + (pip install | easy_install) oauth2 + +Installation +----------------------------------------------------------------------------------------------------- +Installing Twython is fairly easy. You can... + + (pip install | easy_install) twython + +...or, you can clone the repo and install it the old fashioned way. + + git clone git://github.com/ryanmcgrath/twython.git + cd twython + sudo python setup.py install + +Example Use +----------------------------------------------------------------------------------------------------- + from twython import Twython + + twitter = Twython() + results = twitter.searchTwitter(q="bert") + + # More function definitions can be found by reading over twython/twitter_endpoints.py, as well + # as skimming the source file. Both are kept human-readable, and are pretty well documented or + # very self documenting. + +A note about the development of Twython (specifically, 1.3) +---------------------------------------------------------------------------------------------------- +As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored +in a separate Python file, and the class itself catches calls to methods that match up in said table. + +Certain functions require a bit more legwork, and get to stay in the main file, but for the most part +it's all abstracted out. + +As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main +Twython class to import and use. If you need to catch exceptions, import those from twython as well. + +Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that +whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped +as a named argument to any Twitter function. + +For example: the search API looks for arguments under the name "q", so you pass q="query_here" to searchTwitter(). + +Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up +from you using them by this library. + +Twython 3k +----------------------------------------------------------------------------------------------------- +There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed +to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. +If you choose to try it out, be aware of this. + + +Questions, Comments, etc? +----------------------------------------------------------------------------------------------------- +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if +you feel the need to contact me for this (or other) reasons, you can hit me up +at ryan@venodesigns.net. + +Twython is released under an MIT License - see the LICENSE file for more information. diff --git a/examples/current_trends.py b/core_examples/current_trends.py similarity index 64% rename from examples/current_trends.py rename to core_examples/current_trends.py index 8ee4d74..53f64f1 100644 --- a/examples/current_trends.py +++ b/core_examples/current_trends.py @@ -1,7 +1,7 @@ -import twython.core as twython +from twython import Twython """ Instantiate Twython with no Authentication """ -twitter = twython.setup() +twitter = Twython() trends = twitter.getCurrentTrends() print trends diff --git a/examples/daily_trends.py b/core_examples/daily_trends.py similarity index 63% rename from examples/daily_trends.py rename to core_examples/daily_trends.py index 38ca507..d4acc66 100644 --- a/examples/daily_trends.py +++ b/core_examples/daily_trends.py @@ -1,7 +1,7 @@ -import twython.core as twython +from twython import Twython """ Instantiate Twython with no Authentication """ -twitter = twython.setup() +twitter = Twython() trends = twitter.getDailyTrends() print trends diff --git a/examples/get_user_timeline.py b/core_examples/get_user_timeline.py similarity index 72% rename from examples/get_user_timeline.py rename to core_examples/get_user_timeline.py index bdb9200..9dd27e8 100644 --- a/examples/get_user_timeline.py +++ b/core_examples/get_user_timeline.py @@ -1,7 +1,7 @@ -import twython.core as twython +from twython import Twython # We won't authenticate for this, but sometimes it's necessary -twitter = twython.setup() +twitter = Twython() user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") print user_timeline diff --git a/examples/public_timeline.py b/core_examples/public_timeline.py similarity index 74% rename from examples/public_timeline.py rename to core_examples/public_timeline.py index 4670037..40d311d 100644 --- a/examples/public_timeline.py +++ b/core_examples/public_timeline.py @@ -1,7 +1,7 @@ -import twython.core as twython +from twython import Twython # Getting the public timeline requires no authentication, huzzah -twitter = twython.setup() +twitter = Twython() public_timeline = twitter.getPublicTimeline() for tweet in public_timeline: diff --git a/core_examples/search_results.py b/core_examples/search_results.py new file mode 100644 index 0000000..54c7dd0 --- /dev/null +++ b/core_examples/search_results.py @@ -0,0 +1,8 @@ +from twython import Twython + +""" Instantiate Twython with no Authentication """ +twitter = Twython() +search_results = twitter.searchTwitter(q="WebsDotCom", rpp="50") + +for tweet in search_results["results"]: + print tweet["text"] diff --git a/core_examples/shorten_url.py b/core_examples/shorten_url.py new file mode 100644 index 0000000..8ca57ba --- /dev/null +++ b/core_examples/shorten_url.py @@ -0,0 +1,6 @@ +from twython import Twython + +# Shortening URLs requires no authentication, huzzah +shortURL = Twython.shortenURL("http://www.webs.com/") + +print shortURL diff --git a/core_examples/update_profile_image.py b/core_examples/update_profile_image.py new file mode 100644 index 0000000..7f3b5ef --- /dev/null +++ b/core_examples/update_profile_image.py @@ -0,0 +1,9 @@ +from twython import Twython + +""" + You'll need to go through the OAuth ritual to be able to successfully + use this function. See the example oauth django application included in + this package for more information. +""" +twitter = Twython() +twitter.updateProfileImage("myImage.png") diff --git a/core_examples/update_status.py b/core_examples/update_status.py new file mode 100644 index 0000000..52113f1 --- /dev/null +++ b/core_examples/update_status.py @@ -0,0 +1,13 @@ +from twython import Twython + +""" + Note: for any method that'll require you to be authenticated (updating things, etc) + you'll need to go through the OAuth authentication ritual. See the example + Django application that's included with this package for more information. +""" +twitter = Twython() + +# OAuth ritual... + + +twitter.updateStatus("See how easy this was?") diff --git a/examples/weekly_trends.py b/core_examples/weekly_trends.py similarity index 63% rename from examples/weekly_trends.py rename to core_examples/weekly_trends.py index e48fb95..d457242 100644 --- a/examples/weekly_trends.py +++ b/core_examples/weekly_trends.py @@ -1,7 +1,7 @@ -import twython.core as twython +from twython import Twython """ Instantiate Twython with no Authentication """ -twitter = twython.setup() +twitter = Twython() trends = twitter.getWeeklyTrends() print trends diff --git a/examples/get_friends_timeline.py b/examples/get_friends_timeline.py deleted file mode 100644 index 9e67583..0000000 --- a/examples/get_friends_timeline.py +++ /dev/null @@ -1,8 +0,0 @@ -import twython.core as twython, pprint - -# Authenticate using Basic (HTTP) Authentication -twitter = twython.setup(username="example", password="example") -friends_timeline = twitter.getFriendsTimeline(count="150", page="3") - -for tweet in friends_timeline: - print tweet["text"] diff --git a/examples/get_user_mention.py b/examples/get_user_mention.py deleted file mode 100644 index 0db63a0..0000000 --- a/examples/get_user_mention.py +++ /dev/null @@ -1,6 +0,0 @@ -import twython.core as twython - -twitter = twython.setup(username="example", password="example") -mentions = twitter.getUserMentions(count="150") - -print mentions diff --git a/examples/rate_limit.py b/examples/rate_limit.py deleted file mode 100644 index 4b632b0..0000000 --- a/examples/rate_limit.py +++ /dev/null @@ -1,10 +0,0 @@ -import twython.core as twython - -# Instantiate with Basic (HTTP) Authentication -twitter = twython.setup(username="example", password="example") - -# This returns the rate limit for the requesting IP -rateLimit = twitter.getRateLimitStatus() - -# This returns the rate limit for the requesting authenticated user -rateLimit = twitter.getRateLimitStatus(rate_for="user") diff --git a/examples/search_results.py b/examples/search_results.py deleted file mode 100644 index e0df48b..0000000 --- a/examples/search_results.py +++ /dev/null @@ -1,8 +0,0 @@ -import twython.core as twython - -""" Instantiate Tango with no Authentication """ -twitter = twython.setup() -search_results = twitter.searchTwitter("WebsDotCom", rpp="50") - -for tweet in search_results["results"]: - print tweet["text"] diff --git a/examples/shorten_url.py b/examples/shorten_url.py deleted file mode 100644 index f0824bf..0000000 --- a/examples/shorten_url.py +++ /dev/null @@ -1,7 +0,0 @@ -import twython as twython - -# Shortening URLs requires no authentication, huzzah -twitter = twython.setup() -shortURL = twitter.shortenURL("http://www.webs.com/") - -print shortURL diff --git a/examples/twython_setup.py b/examples/twython_setup.py deleted file mode 100644 index 6eea228..0000000 --- a/examples/twython_setup.py +++ /dev/null @@ -1,7 +0,0 @@ -import twython.core as twython - -# Using no authentication -twitter = twython.setup() - -# Using Basic Authentication (core is all about basic auth, look to twython.oauth in the future for oauth) -twitter = twython.setup(username="example", password="example") diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py deleted file mode 100644 index 37ee00e..0000000 --- a/examples/update_profile_image.py +++ /dev/null @@ -1,5 +0,0 @@ -import twython.core as twython - -# Instantiate Twython with Basic (HTTP) Authentication -twitter = twython.setup(username="example", password="example") -twitter.updateProfileImage("myImage.png") diff --git a/examples/update_status.py b/examples/update_status.py deleted file mode 100644 index 0f7fe38..0000000 --- a/examples/update_status.py +++ /dev/null @@ -1,5 +0,0 @@ -import twython.core as twython - -# Create a Twython instance using Basic (HTTP) Authentication and update our Status -twitter = twython.setup(username="example", password="example") -twitter.updateStatus("See how easy this was?") diff --git a/setup.py b/setup.py index 505eea7..e4036e7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3' +__version__ = '1.3.2' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 4ae4daf..79bf641 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3" +__version__ = "1.3.2" import urllib import urllib2 @@ -414,4 +414,4 @@ class Twython(object): text = text.encode('utf-8') except: pass - return text \ No newline at end of file + return text diff --git a/twython3k/twython.py b/twython3k/twython.py index 5efa1e7..bdc84ba 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3" +__version__ = "1.3.2" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse @@ -414,4 +414,4 @@ class Twython(object): text = text.encode('utf-8') except: pass - return text \ No newline at end of file + return text From 3b9527ae698580fb5093434373f0663f423a5a83 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 20 Oct 2010 11:22:31 -0400 Subject: [PATCH 178/687] Fix constructApiURL method, version increment to fix function definitions in new versions --- setup.py | 2 +- twython/twython.py | 10 +++++++--- twython3k/twython.py | 10 +++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index e4036e7..f33dcc9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3.2' +__version__ = '1.3.3' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 79bf641..0890e95 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.2" +__version__ = "1.3.3" import urllib import urllib2 @@ -208,6 +208,10 @@ class Twython(object): # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ + @staticmethod + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, 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") @@ -260,7 +264,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": Twython.unicode2utf8(search_query)}) try: resp, content = self.client.request(searchURL, "GET") return simplejson.loads(content) @@ -277,7 +281,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": Twython.unicode2utf8(search_query)}) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) diff --git a/twython3k/twython.py b/twython3k/twython.py index bdc84ba..f78a962 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.2" +__version__ = "1.3.3" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse @@ -208,6 +208,10 @@ class Twython(object): # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ + @staticmethod + def constructApiURL(self, base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) + @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") @@ -260,7 +264,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": Twython.unicode2utf8(search_query)}) try: resp, content = self.client.request(searchURL, "GET") return simplejson.loads(content) @@ -277,7 +281,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": Twython.unicode2utf8(search_query)}) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) From f9fc3b4a7ce05a3d9a0de135a6fe1aa7151449e9 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 22 Oct 2010 12:43:50 -0400 Subject: [PATCH 179/687] Fix years --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 6fdab59..cd5b253 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2009 Ryan McGrath +Copyright (c) 2009 - 2010 Ryan McGrath Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +THE SOFTWARE. From 969a1b3578107092e05d585a237d5fe6d0e204db Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 29 Oct 2010 00:31:27 -0400 Subject: [PATCH 180/687] Fix for issue #22 (searchTwitter failing due to args specification); searchTwitter/searchTwitterGen now default to enforcing UTF-8 conversion, constructApiURL calls fixed, guess_mime_type() function consolidated/removed, bump to version 1.3.4 for small release with bug fixes --- twython/twython.py | 20 ++++++-------------- twython3k/twython.py | 20 ++++++-------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 0890e95..d1822ce 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.3" +__version__ = "1.3.4" import urllib import urllib2 @@ -209,8 +209,8 @@ class Twython(object): # ------------------------------------------------------------------------------------------------------------------------ @staticmethod - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.iteritems()]) + def constructApiURL(base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), Twython.unicode2utf8(value)) for (key, value) in params.iteritems()]) @staticmethod def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -264,7 +264,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": Twython.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET") return simplejson.loads(content) @@ -281,7 +281,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": Twython.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) @@ -394,7 +394,7 @@ class Twython(object): for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % Twython.get_content_type(filename)) + L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') L.append('') L.append(value) L.append('--' + BOUNDARY + '--') @@ -403,14 +403,6 @@ class Twython(object): content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - @staticmethod - def get_content_type(filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - @staticmethod def unicode2utf8(text): try: diff --git a/twython3k/twython.py b/twython3k/twython.py index f78a962..ee3f7ad 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.3" +__version__ = "1.3.4" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse @@ -209,8 +209,8 @@ class Twython(object): # ------------------------------------------------------------------------------------------------------------------------ @staticmethod - def constructApiURL(self, base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in params.items()]) + def constructApiURL(base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), Twython.unicode2utf8(value)) for (key, value) in params.items()]) @staticmethod def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -264,7 +264,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": Twython.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET") return simplejson.loads(content) @@ -281,7 +281,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": Twython.unicode2utf8(search_query)}) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) @@ -394,7 +394,7 @@ class Twython(object): for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % Twython.get_content_type(filename)) + L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') L.append('') L.append(value) L.append('--' + BOUNDARY + '--') @@ -403,14 +403,6 @@ class Twython(object): content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - @staticmethod - def get_content_type(filename): - """ get_content_type(self, filename) - - Exactly what you think it does. :D - """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' - @staticmethod def unicode2utf8(text): try: From ed14371d38079d8dc01609c351e07a44ccfdbc40 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 29 Oct 2010 00:32:19 -0400 Subject: [PATCH 181/687] Version bump for bug fixes release (1.3.3 => 1.3.4) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f33dcc9..69f1726 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3.3' +__version__ = '1.3.4' setup( # Basic package information. From 2122f6528d88da31d2e72a8a2bc6e1809c38950d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 7 Nov 2010 02:18:24 -0500 Subject: [PATCH 182/687] Minor Streaming work; not official, just hackery for fun. Fork and patch or look away. --- oauth_django_example/__init__.py | 0 oauth_django_example/manage.py | 11 --- oauth_django_example/settings.py | 94 --------------------- oauth_django_example/templates/tweets.html | 3 - oauth_django_example/test.db | Bin 65536 -> 0 bytes oauth_django_example/twitter/__init__.py | 0 oauth_django_example/twitter/models.py | 7 -- oauth_django_example/twitter/views.py | 73 ---------------- oauth_django_example/urls.py | 9 -- twython/streaming.py | 44 ++++++---- 10 files changed, 26 insertions(+), 215 deletions(-) delete mode 100644 oauth_django_example/__init__.py delete mode 100644 oauth_django_example/manage.py delete mode 100644 oauth_django_example/settings.py delete mode 100644 oauth_django_example/templates/tweets.html delete mode 100644 oauth_django_example/test.db delete mode 100644 oauth_django_example/twitter/__init__.py delete mode 100644 oauth_django_example/twitter/models.py delete mode 100644 oauth_django_example/twitter/views.py delete mode 100644 oauth_django_example/urls.py diff --git a/oauth_django_example/__init__.py b/oauth_django_example/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oauth_django_example/manage.py b/oauth_django_example/manage.py deleted file mode 100644 index 5e78ea9..0000000 --- a/oauth_django_example/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/oauth_django_example/settings.py b/oauth_django_example/settings.py deleted file mode 100644 index 8ceb7a5..0000000 --- a/oauth_django_example/settings.py +++ /dev/null @@ -1,94 +0,0 @@ -import os.path - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Ryan McGrath', 'ryan@venodesigns.net'), -) - -MANAGERS = ADMINS - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': os.path.join(os.path.dirname(__file__), 'test.db'), # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - - -AUTH_PROFILE_MODULE = 'twitter.Profile' - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/New_York' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com", "http://example.com/media/" -MEDIA_URL = '' - -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/media/' - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '&!_t1t^gmenaid9mkmkuw=4nthj7f)o+!@$#ipfp*s11380t*)' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'twython_testing.urls' - -TEMPLATE_DIRS = ( - os.path.join(os.path.dirname(__file__), 'templates'), -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'twitter', -) diff --git a/oauth_django_example/templates/tweets.html b/oauth_django_example/templates/tweets.html deleted file mode 100644 index a2ec453..0000000 --- a/oauth_django_example/templates/tweets.html +++ /dev/null @@ -1,3 +0,0 @@ -{% for tweet in tweets %} - {{ tweet.text }} -{% endfor %} diff --git a/oauth_django_example/test.db b/oauth_django_example/test.db deleted file mode 100644 index 3082a95365c31ec03e2a58e9352abe89008a2011..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65536 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 H|DXQ@eXup& diff --git a/oauth_django_example/twitter/__init__.py b/oauth_django_example/twitter/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/oauth_django_example/twitter/models.py b/oauth_django_example/twitter/models.py deleted file mode 100644 index 3a07664..0000000 --- a/oauth_django_example/twitter/models.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models -from django.contrib.auth.models import User - -class Profile(models.Model): - user = models.ForeignKey(User) - oauth_token = models.CharField(max_length = 200) - oauth_secret = models.CharField(max_length = 200) diff --git a/oauth_django_example/twitter/views.py b/oauth_django_example/twitter/views.py deleted file mode 100644 index cb3f182..0000000 --- a/oauth_django_example/twitter/views.py +++ /dev/null @@ -1,73 +0,0 @@ -from django.contrib.auth import authenticate, login, logout -from django.contrib.auth.models import User -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import render_to_response -from django.contrib.auth.decorators import login_required - -from twython import Twython -from twitter.models import Profile - -CONSUMER_KEY = "YOUR CONSUMER KEY HERE" -CONSUMER_SECRET = "YOUR CONSUMER SECRET HERE" - -def twitter_logout(request): - logout(request) - return HttpResponseRedirect('/') - -def twitter_begin_auth(request): - # Instantiate Twython with the first leg of our trip. - twitter = Twython( - twitter_token = CONSUMER_KEY, - twitter_secret = CONSUMER_SECRET - ) - - # Request an authorization url to send the user to... - auth_props = twitter.get_authentication_tokens() - - # Then send them over there, durh. - request.session['request_token'] = auth_props - return HttpResponseRedirect(auth_props['auth_url']) - -def twitter_thanks(request): - # Now that we've got the magic tokens back from Twitter, we need to exchange - # for permanent ones and store them... - twitter = Twython( - twitter_token = CONSUMER_KEY, - twitter_secret = CONSUMER_SECRET, - oauth_token = request.session['request_token']['oauth_token'], - oauth_token_secret = request.session['request_token']['oauth_token_secret'] - ) - - # Retrieve the tokens we want... - authorized_tokens = twitter.get_authorized_tokens() - - # If they already exist, grab them, login and redirect to a page displaying stuff. - try: - user = User.objects.get(username = authorized_tokens['screen_name']) - except User.DoesNotExist: - # We mock a creation here; no email, password is just the token, etc. - user = User.objects.create_user(authorized_tokens['screen_name'], "fjdsfn@jfndjfn.com", authorized_tokens['oauth_token_secret']) - profile = Profile() - profile.user = user - profile.oauth_token = authorized_tokens['oauth_token'] - profile.oauth_secret = authorized_tokens['oauth_token_secret'] - profile.save() - - user = authenticate( - username = authorized_tokens['screen_name'], - password = authorized_tokens['oauth_token_secret'] - ) - login(request, user) - return HttpResponseRedirect('/timeline') - -def twitter_timeline(request): - user = request.user.get_profile() - twitter = Twython( - twitter_token = CONSUMER_KEY, - twitter_secret = CONSUMER_SECRET, - oauth_token = user.oauth_token, - oauth_token_secret = user.oauth_secret - ) - my_tweets = twitter.getHomeTimeline() - print my_tweets - return render_to_response('tweets.html', {'tweets': my_tweets}) diff --git a/oauth_django_example/urls.py b/oauth_django_example/urls.py deleted file mode 100644 index 9344f4d..0000000 --- a/oauth_django_example/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.conf.urls.defaults import * -from twitter.views import twitter_begin_auth, twitter_thanks, twitter_logout, twitter_timeline - -urlpatterns = patterns('', - (r'^login/?$', twitter_begin_auth), - (r'^/logout?$', twitter_logout), - (r'^thanks/?$', twitter_thanks), # Where they're redirect to after authorizing - (r'^timeline/?$', twitter_timeline), -) diff --git a/twython/streaming.py b/twython/streaming.py index f6df1a3..8020116 100644 --- a/twython/streaming.py +++ b/twython/streaming.py @@ -1,37 +1,50 @@ #!/usr/bin/python """ - Yes, this is just in __init__ for now, hush. + TwythonStreamer is an implementation of the Streaming API for Twython. + Pretty self explanatory by reading the code below. It's worth noting + that the end user should, ideally, never import this library, but rather + this is exposed via a linking method in Twython's core. - The beginnings of Twitter Streaming API support in Twython. Don't expect this to work at all, - consider it a stub for now. -- Ryan - Questions, comments? ryan@venodesigns.net """ -import httplib, urllib, urllib2, mimetypes, mimetools, socket, time +__author__ = "Ryan McGrath " +__version__ = "1.0.0" + +import urllib +import urllib2 +import urlparse +import httplib +import httplib2 +import re from urllib2 import HTTPError +# There are some special setups (like, oh, a Django application) where +# simplejson exists behind the scenes anyway. Past Python 2.6, this should +# never really cause any problems to begin with. try: - import simplejson + # Python 2.6 and up + import json as simplejson except ImportError: try: - 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) + import 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. from django.utils import simplejson except: - raise Exception("Twython (Streaming) requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -__author__ = "Ryan McGrath " -__version__ = "0.1" + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") class TwythonStreamingError(Exception): def __init__(self, msg): self.msg = msg + def __str__(self): - return repr(self.msg) + return "%s" % str(self.msg) feeds = { "firehose": "http://stream.twitter.com/firehose.json", @@ -43,10 +56,5 @@ feeds = { "track": "http://stream.twitter.com/track.json", } -class stream: +class Stream(object): def __init__(self, username = None, password = None, feed = "spritzer", user_agent = "Twython Streaming"): - self.username = username - self.password = password - self.feed = feeds[feed] - self.user_agent = user_agent - self.connection_open = False From 0407742fd83f7c129f9f46fb687d377b6779ba8d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 7 Nov 2010 02:20:56 -0500 Subject: [PATCH 183/687] Moved twython-django to be a submodule, better references everything this way (no need to ship useless, bloated code with a release). Twython 1.3.5 will not ship with the extra Django code, as it's now in its own separate repository (this is better, email if you have questions, happy to answer). --- .gitmodules | 3 +++ twython-django | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 twython-django diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a3dc19e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "twython-django"] + path = twython-django + url = git@github.com:ryanmcgrath/twython-django.git diff --git a/twython-django b/twython-django new file mode 160000 index 0000000..fab3d7a --- /dev/null +++ b/twython-django @@ -0,0 +1 @@ +Subproject commit fab3d7a216de87bed8aef9830bb9d04a408859eb From a13539884d2a7a0bd97403351486ea9d6478fd34 Mon Sep 17 00:00:00 2001 From: Eugen Pyvovarov Date: Wed, 10 Nov 2010 07:33:10 -0800 Subject: [PATCH 184/687] in get method we use *kwargs, that's why we need to provide named arguments to the function --- core_examples/update_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_examples/update_status.py b/core_examples/update_status.py index 52113f1..1acc2b8 100644 --- a/core_examples/update_status.py +++ b/core_examples/update_status.py @@ -10,4 +10,4 @@ twitter = Twython() # OAuth ritual... -twitter.updateStatus("See how easy this was?") +twitter.updateStatus(status = "See how easy this was?") From 5f1d0d4e907550a312faeb4322962366c68c91a4 Mon Sep 17 00:00:00 2001 From: Eugen Pyvovarov Date: Wed, 10 Nov 2010 08:27:21 -0800 Subject: [PATCH 185/687] verify credentials and return userinfo --- twython/twitter_endpoints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 42bd5a1..4581c92 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -21,6 +21,11 @@ api_table = { 'method': 'GET', }, + 'verifyCredentials': { + 'url': '/account/verify_credentials.json', + 'method': 'GET', + }, + # Timeline methods 'getPublicTimeline': { 'url': '/statuses/public_timeline.json', From 931921be0195e8be4085838b1eedd69be6245a06 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 28 Nov 2010 17:56:11 +0900 Subject: [PATCH 186/687] Fix for issue #25, certain search queries not being properly encoded. Thanks to momander for pointing this out... --- setup.py | 2 +- twython/twython.py | 4 ++-- twython3k/twython.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 69f1726..ff542a4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3.4' +__version__ = '1.3.5' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index d1822ce..e5a3b3f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.4" +__version__ = "1.3.5" import urllib import urllib2 @@ -210,7 +210,7 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), 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"): diff --git a/twython3k/twython.py b/twython3k/twython.py index ee3f7ad..406dbc6 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.4" +__version__ = "1.3.5" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse @@ -210,7 +210,7 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), Twython.unicode2utf8(value)) for (key, value) in params.items()]) + return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.items()]) @staticmethod def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): From 9133d0f10ec39f7e774af7d5d8730db3a27150ca Mon Sep 17 00:00:00 2001 From: Eugen Pyvovarov Date: Wed, 8 Dec 2010 02:39:06 -0800 Subject: [PATCH 187/687] =?UTF-8?q?ability=20to=20send=20tweets=20containi?= =?UTF-8?q?ng=20unicode=20characters=20like=20cyrillic=20word=20'=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index d1822ce..f298867 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -166,7 +166,7 @@ class Twython(object): # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs)) + resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, v.encode('utf-8')] for k, v in kwargs.items()))) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) resp, content = self.client.request(url, fn['method']) From 8ecdaa5bfa4703b26933dbeba70e0292a5e33e2f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 12 Dec 2010 14:45:03 +0900 Subject: [PATCH 188/687] Fix a shortenURL reference bug pointed out by Jacob, incremental release of latest bugfixes because Pypi's been down recently --- setup.py | 2 +- twython/twython.py | 71 +++++++++++++++++++++--------------------- twython3k/twython.py | 73 +++++++++++++++++++++----------------------- 3 files changed, 70 insertions(+), 76 deletions(-) diff --git a/setup.py b/setup.py index ff542a4..da1bed7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3.5' +__version__ = '1.3.6' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 324fe1e..0c32838 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.5" +__version__ = "1.3.6" import urllib import urllib2 @@ -60,7 +60,7 @@ class TwythonError(Exception): self.msg = msg if error_code == 400: raise APILimit(msg) - + def __str__(self): return repr(self.msg) @@ -73,7 +73,7 @@ class APILimit(TwythonError): """ def __init__(self, msg): self.msg = msg - + def __str__(self): return repr(self.msg) @@ -85,7 +85,7 @@ class AuthError(TwythonError): """ def __init__(self, msg): self.msg = msg - + def __str__(self): return repr(self.msg) @@ -115,21 +115,21 @@ class Twython(object): self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret - + # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - + consumer = None token = None - + if self.twitter_token is not None and self.twitter_secret is not None: consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - + if self.oauth_token is not None and self.oauth_secret is not None: token = oauth.Token(oauth_token, oauth_token_secret) - + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if consumer is not None and token is not None: self.client = oauth.Client(consumer, token) @@ -138,7 +138,7 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http() - + def __getattr__(self, api_call): """ The most magically awesome block of code you'll see in 2010. @@ -152,32 +152,32 @@ class Twython(object): It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, we can take over and find the API method in our table. We then return a function that downloads and parses what we're looking for, based on the keywords passed in. - + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". """ def get(self, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] base = re.sub( - '\{\{(?P[a-zA-Z]+)\}\}', + '\{\{(?P[a-zA-Z]+)\}\}', lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. base_url + fn['url'] ) - + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, v.encode('utf-8')] for k, v in kwargs.items()))) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) resp, content = self.client.request(url, fn['method']) - + return simplejson.loads(content) - + if api_call in api_table: return get.__get__(self) else: raise AttributeError, api_call - + def get_authentication_tokens(self): """ get_auth_url(self) @@ -185,14 +185,14 @@ class Twython(object): Returns an authorization URL for a user to hit. """ resp, content = self.client.request(self.request_token_url, "GET") - + if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - + request_tokens = dict(urlparse.parse_qsl(content)) request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) return request_tokens - + def get_authorized_tokens(self): """ get_authorized_tokens @@ -201,17 +201,17 @@ class Twython(object): """ resp, content = self.client.request(self.access_token_url, "GET") return dict(urlparse.parse_qsl(content)) - + # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ - + @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()]) - + @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") @@ -223,17 +223,14 @@ class Twython(object): shortener - In case you want to use a url shortening service other than is.gd. """ try: - resp, content = self.client.request( - shortener + "?" + urllib.urlencode({query: Twython.unicode2utf8(url_to_shorten)}), - "GET" - ) + 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 = None): """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - + 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. @@ -270,13 +267,13 @@ class Twython(object): return simplejson.loads(content) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - + def searchTwitterGen(self, **kwargs): """searchTwitterGen(search_query, **kwargs) Returns a generator of tweets that match a specified query. - Parameters: + Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. e.g x.searchTwitter(q="jjndf", page="2") @@ -287,21 +284,21 @@ class Twython(object): data = simplejson.loads(content) except HTTPError, e: raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) - + if not data['results']: raise StopIteration for tweet in data['results']: yield tweet - + if 'page' not in kwargs: kwargs['page'] = 2 else: kwargs['page'] += 1 - + for tweet in self.searchTwitterGen(search_query, **kwargs): yield tweet - + def isListMember(self, list_id, id, username, version = 1): """ isListMember(self, list_id, id, version) @@ -348,7 +345,7 @@ class Twython(object): Parameters: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" 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. """ @@ -380,7 +377,7 @@ class Twython(object): return urllib2.urlopen(r).read() except HTTPError, e: raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - + @staticmethod def encode_multipart_formdata(fields, files): BOUNDARY = mimetools.choose_boundary() @@ -402,7 +399,7 @@ class Twython(object): body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - + @staticmethod def unicode2utf8(text): try: diff --git a/twython3k/twython.py b/twython3k/twython.py index 406dbc6..fc2412a 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.5" +__version__ = "1.3.6" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse @@ -60,7 +60,7 @@ class TwythonError(Exception): self.msg = msg if error_code == 400: raise APILimit(msg) - + def __str__(self): return repr(self.msg) @@ -73,7 +73,7 @@ class APILimit(TwythonError): """ def __init__(self, msg): self.msg = msg - + def __str__(self): return repr(self.msg) @@ -85,7 +85,7 @@ class AuthError(TwythonError): """ def __init__(self, msg): self.msg = msg - + def __str__(self): return repr(self.msg) @@ -115,21 +115,21 @@ class Twython(object): self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret - + # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - + consumer = None token = None - + if self.twitter_token is not None and self.twitter_secret is not None: consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - + if self.oauth_token is not None and self.oauth_secret is not None: token = oauth.Token(oauth_token, oauth_token_secret) - + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if consumer is not None and token is not None: self.client = oauth.Client(consumer, token) @@ -138,7 +138,7 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http() - + def __getattr__(self, api_call): """ The most magically awesome block of code you'll see in 2010. @@ -152,32 +152,32 @@ class Twython(object): It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, we can take over and find the API method in our table. We then return a function that downloads and parses what we're looking for, based on the keywords passed in. - + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". """ def get(self, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] base = re.sub( - '\{\{(?P[a-zA-Z]+)\}\}', + '\{\{(?P[a-zA-Z]+)\}\}', lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. base_url + fn['url'] ) - + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(kwargs)) + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, v.encode('utf-8')] for k, v in list(kwargs.items())))) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.items()]) resp, content = self.client.request(url, fn['method']) - + return simplejson.loads(content) - + if api_call in api_table: return get.__get__(self) else: raise AttributeError(api_call) - + def get_authentication_tokens(self): """ get_auth_url(self) @@ -185,14 +185,14 @@ class Twython(object): Returns an authorization URL for a user to hit. """ resp, content = self.client.request(self.request_token_url, "GET") - + if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - + request_tokens = dict(urllib.parse.parse_qsl(content)) request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) return request_tokens - + def get_authorized_tokens(self): """ get_authorized_tokens @@ -201,17 +201,17 @@ class Twython(object): """ resp, content = self.client.request(self.access_token_url, "GET") return dict(urllib.parse.parse_qsl(content)) - + # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ - + @staticmethod def constructApiURL(base_url, params): return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.items()]) - + @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") @@ -223,17 +223,14 @@ class Twython(object): shortener - In case you want to use a url shortening service other than is.gd. """ try: - resp, content = self.client.request( - shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)}), - "GET" - ) + content = urllib.request.urlopen(shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)})).read() return content except HTTPError as e: raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - + def bulkUserLookup(self, ids = None, screen_names = None, version = None): """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - + 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. @@ -270,13 +267,13 @@ class Twython(object): return simplejson.loads(content) except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - + def searchTwitterGen(self, **kwargs): """searchTwitterGen(search_query, **kwargs) Returns a generator of tweets that match a specified query. - Parameters: + Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. e.g x.searchTwitter(q="jjndf", page="2") @@ -287,21 +284,21 @@ class Twython(object): data = simplejson.loads(content) except HTTPError as e: raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code) - + if not data['results']: raise StopIteration for tweet in data['results']: yield tweet - + if 'page' not in kwargs: kwargs['page'] = 2 else: kwargs['page'] += 1 - + for tweet in self.searchTwitterGen(search_query, **kwargs): yield tweet - + def isListMember(self, list_id, id, username, version = 1): """ isListMember(self, list_id, id, version) @@ -348,7 +345,7 @@ class Twython(object): Parameters: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" 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. """ @@ -380,7 +377,7 @@ class Twython(object): return urllib.request.urlopen(r).read() except HTTPError as e: raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - + @staticmethod def encode_multipart_formdata(fields, files): BOUNDARY = mimetools.choose_boundary() @@ -402,7 +399,7 @@ class Twython(object): body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body - + @staticmethod def unicode2utf8(text): try: From 4b2a406b3545bba519d754959f2cca6857a1a3da Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 13 Dec 2010 14:26:49 +0900 Subject: [PATCH 189/687] Fix for searchTwitterGen search_query parameter, spotted (again) by Jacob Silterra --- twython/twython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 0c32838..53ff24a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -268,7 +268,7 @@ class Twython(object): except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - def searchTwitterGen(self, **kwargs): + def searchTwitterGen(self, search_query, **kwargs): """searchTwitterGen(search_query, **kwargs) Returns a generator of tweets that match a specified query. @@ -278,7 +278,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) From 435294e0045154d61f52259299823f40333bf52f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 13 Dec 2010 14:28:23 +0900 Subject: [PATCH 190/687] Fix username parameter bug --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 53ff24a..fbe697b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -318,7 +318,7 @@ class Twython(object): except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - def isListSubscriber(self, list_id, id, version = 1): + def isListSubscriber(self, username, list_id, id, version = 1): """ isListSubscriber(self, list_id, id, version) Check if a specified user (id) is a subscriber of the list in question (list_id). From 90cba31922883dc93d2ab7dd6c2029142675fe73 Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Sat, 8 Jan 2011 08:06:10 +0800 Subject: [PATCH 191/687] Some parameters that need substituting (e.g. addListMember) contain underscores. These weren't matched by the regex, so I've added an underscore to the character group. addListMember now works. --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index fbe697b..19b1f95 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -159,7 +159,7 @@ class Twython(object): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] base = re.sub( - '\{\{(?P[a-zA-Z]+)\}\}', + '\{\{(?P[a-zA-Z_]+)\}\}', lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. base_url + fn['url'] ) From d5c34779d9b72a78e269981a8d9d3a9d0b8a7060 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 17 Jan 2011 19:50:39 -0500 Subject: [PATCH 192/687] Woah, how the hell did we never notice this before? Set headers on the instance, not as a generic variable... --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index fbe697b..07d6c04 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -119,7 +119,7 @@ class Twython(object): # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: - headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} consumer = None token = None From b225918165bb72f9d01b6a7d6310ab5eb9396f32 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 18 Jan 2011 01:43:41 -0500 Subject: [PATCH 193/687] TwythonError should go off of AttributeError instead --- twython/twython.py | 4 ++-- twython3k/twython.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 49f2f22..10d4a37 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -46,7 +46,7 @@ except ImportError: # Seriously wtf is wrong with you if you get this Exception. raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -class TwythonError(Exception): +class TwythonError(AttributeError): """ Generic error class, catch-all for most Twython issues. Special cases are handled by APILimit and AuthError. @@ -176,7 +176,7 @@ class Twython(object): if api_call in api_table: return get.__get__(self) else: - raise AttributeError, api_call + raise TwythonError, api_call def get_authentication_tokens(self): """ diff --git a/twython3k/twython.py b/twython3k/twython.py index fc2412a..5c2a546 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -46,7 +46,7 @@ except ImportError: # Seriously wtf is wrong with you if you get this Exception. raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -class TwythonError(Exception): +class TwythonError(AttributeError): """ Generic error class, catch-all for most Twython issues. Special cases are handled by APILimit and AuthError. @@ -119,7 +119,7 @@ class Twython(object): # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: - headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} consumer = None token = None @@ -159,7 +159,7 @@ class Twython(object): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] base = re.sub( - '\{\{(?P[a-zA-Z]+)\}\}', + '\{\{(?P[a-zA-Z_]+)\}\}', lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. base_url + fn['url'] ) @@ -168,7 +168,7 @@ class Twython(object): if fn['method'] == 'POST': resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, v.encode('utf-8')] for k, v in list(kwargs.items())))) else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.items()]) + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) resp, content = self.client.request(url, fn['method']) return simplejson.loads(content) @@ -176,7 +176,7 @@ class Twython(object): if api_call in api_table: return get.__get__(self) else: - raise AttributeError(api_call) + raise TwythonError(api_call) def get_authentication_tokens(self): """ @@ -210,7 +210,7 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.items()]) + return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) @staticmethod def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): @@ -268,7 +268,7 @@ class Twython(object): except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - def searchTwitterGen(self, **kwargs): + def searchTwitterGen(self, search_query, **kwargs): """searchTwitterGen(search_query, **kwargs) Returns a generator of tweets that match a specified query. @@ -278,7 +278,7 @@ class Twython(object): e.g x.searchTwitter(q="jjndf", page="2") """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) try: resp, content = self.client.request(searchURL, "GET") data = simplejson.loads(content) @@ -318,7 +318,7 @@ class Twython(object): except HTTPError as e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - def isListSubscriber(self, list_id, id, version = 1): + def isListSubscriber(self, username, list_id, id, version = 1): """ isListSubscriber(self, list_id, id, version) Check if a specified user (id) is a subscriber of the list in question (list_id). From e5bef0f8e440c78cdf9f61313305dee39d8f5665 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 18 Jan 2011 03:01:50 -0500 Subject: [PATCH 194/687] Fix header passing, fix isListSubscriber call never firing a request --- twython/twython.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 10d4a37..f2c9f03 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -166,10 +166,10 @@ class Twython(object): # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, v.encode('utf-8')] for k, v in kwargs.items()))) + resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, v.encode('utf-8')] for k, v in kwargs.items())), headers = self.headers) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) - resp, content = self.client.request(url, fn['method']) + resp, content = self.client.request(url, fn['method'], headers = self.headers) return simplejson.loads(content) @@ -263,7 +263,7 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: - resp, content = self.client.request(searchURL, "GET") + resp, content = self.client.request(searchURL, "GET", headers = self.headers) return simplejson.loads(content) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) @@ -280,7 +280,7 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) try: - resp, content = self.client.request(searchURL, "GET") + resp, content = self.client.request(searchURL, "GET", headers = self.headers) data = simplejson.loads(content) except HTTPError, e: raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) @@ -313,7 +313,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, `id`)) + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`), headers = self.headers) return simplejson.loads(content) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -332,7 +332,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 = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`) + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`), headers = self.headers) return simplejson.loads(content) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) From 0737c124496a82131528b3f1f731cd73bc678733 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Tue, 18 Jan 2011 22:14:11 +0800 Subject: [PATCH 195/687] Added callback_url parameter to Twython to set the URL the remote site will use to return the request token. Supports both OAuth 1.0 and 1.0a methods of setting the oauth_callback parameter. For OAuth 1.0a support, a patched version of python-oauth2 is currently required (see https://github.com/simplegeo/python-oauth2/pull/43). Availability of OAuth 1.0a support in python-oauth2 is autodetected and a suitable warning is given if the remote site requires it but python-oauth2 doesn't support it. --- twython/twython.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index f2c9f03..2f9428a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -19,6 +19,7 @@ import httplib2 import mimetypes import mimetools import re +import inspect import oauth2 as oauth @@ -46,6 +47,9 @@ except ImportError: # Seriously wtf is wrong with you if you get this Exception. raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") +# Detect if oauth2 supports the callback_url argument to request +OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in inspect.getargspec(oauth.Client.request).args + class TwythonError(AttributeError): """ Generic error class, catch-all for most Twython issues. @@ -91,7 +95,7 @@ class AuthError(TwythonError): class Twython(object): - def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None): + def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None): """setup(self, oauth_token = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -115,6 +119,7 @@ class Twython(object): self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret + self.callback_url = callback_url # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers @@ -184,13 +189,37 @@ class Twython(object): Returns an authorization URL for a user to hit. """ - resp, content = self.client.request(self.request_token_url, "GET") + callback_url = self.callback_url or 'oob' + + request_args = {} + if OAUTH_LIB_SUPPORTS_CALLBACK: + request_args['callback_url'] = callback_url + + resp, content = self.client.request(self.request_token_url, "GET", **request_args) if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - + request_tokens = dict(urlparse.parse_qsl(content)) - request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' + + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url!='oob' and oauth_callback_confirmed: + import warnings + warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") + oauth_callback_confirmed = False + + auth_url_params = { + 'oauth_token' : request_tokens['oauth_token'], + } + + # Use old-style callback argument + if callback_url!='oob' and not oauth_callback_confirmed: + auth_url_params['oauth_callback'] = callback_url + + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) + + print request_tokens return request_tokens def get_authorized_tokens(self): From 6f38c4c27de122fab26c603a451ad176eb08735e Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Tue, 18 Jan 2011 22:20:54 +0800 Subject: [PATCH 196/687] Removed debugging print statement --- twython/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 2f9428a..28ab4af 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -219,7 +219,6 @@ class Twython(object): request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) - print request_tokens return request_tokens def get_authorized_tokens(self): From 7f93acb39e0636fd25f65448b67497132a243d09 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Tue, 25 Jan 2011 15:17:49 +0800 Subject: [PATCH 197/687] Modified blukUserLookup: * version argument was ignored * user_id and screen_name lists were not url-escaped, and always ended with a trailing comma * unknown parameter 'lol=1' removed * allow other arguments to users/lookup to be added (kwargs) * added http headers to request --- twython/twython.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 28ab4af..c312b76 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -256,25 +256,22 @@ class Twython(object): except HTTPError, e: raise TwythonError("shortenURL() failed with a %s error code." % `e.code`) - def bulkUserLookup(self, ids = None, screen_names = None, version = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + 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. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that contain their respective data sets. Statuses for the users in question will be returned inline if they exist. Requires authentication! """ - apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += `id` + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," + if ids: + kwargs['user_id'] = ','.join(map(str, ids)) + if screen_names: + kwargs['screen_names'] = ','.join(screen_names) + + lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: - resp, content = self.client.request(apiURL, "GET") + resp, content = self.client.request(lookupURL, "GET", headers = self.headers) return simplejson.loads(content) except HTTPError, e: raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) From 4d524fb654aec3d5ae186d49b962d681a4e7ea21 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Wed, 26 Jan 2011 23:31:20 +0800 Subject: [PATCH 198/687] Added getProfileImageUrl() to handle "users/profile_image" api call --- twython/twython.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index c312b76..b37c99f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -402,6 +402,22 @@ class Twython(object): return urllib2.urlopen(r).read() except HTTPError, e: raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + def getProfileImageUrl(self, username, size=None, version=1): + url = "http://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) + if size: + url = self.constructApiURL(url, {'size':size}) + + try: + client = httplib2.Http() + client.follow_redirects = False + resp = client.request(url, 'HEAD')[0] + if resp['status'] not in ('301', '302', '303', '307'): + raise TwythonError("getProfileImageUrl() failed to get redirect.") + return resp['location'] + + except HTTPError, e: + raise TwythonError("getProfileImageUrl() failed with a %d error code." % e.code, e.code) @staticmethod def encode_multipart_formdata(fields, files): From 66e7040df6fbe7fb6c039ba627222033f3293fbb Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Wed, 26 Jan 2011 23:46:57 +0800 Subject: [PATCH 199/687] Added documentation to getProfileImageUrl() --- twython/twython.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index b37c99f..bec10e2 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -404,6 +404,15 @@ class Twython(object): raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) def getProfileImageUrl(self, username, size=None, version=1): + """ getProfileImageUrl(username) + + Gets the URL for the user's profile image. + + Parameters: + username - Required. User name of the user you want the image url of. + 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}) From 1c12c9c7d5eef574804a9b59ed453c9b9e2258a3 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Thu, 3 Feb 2011 18:02:33 +0800 Subject: [PATCH 200/687] Rewrote getProfileImageUrl to handle responses with status 200 (happen when eg. a user does not exist) --- twython/twython.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index bec10e2..b8549fc 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -417,16 +417,16 @@ class Twython(object): if size: url = self.constructApiURL(url, {'size':size}) - try: - client = httplib2.Http() - client.follow_redirects = False - resp = client.request(url, 'HEAD')[0] - if resp['status'] not in ('301', '302', '303', '307'): - raise TwythonError("getProfileImageUrl() failed to get redirect.") + client = httplib2.Http() + client.follow_redirects = False + resp, content = client.request(url, 'GET') + + if resp.status in (301,302,303,307): return resp['location'] - - except HTTPError, e: - raise TwythonError("getProfileImageUrl() failed with a %d error code." % e.code, e.code) + elif resp.status == 200: + return simplejson.loads(content) + + raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) @staticmethod def encode_multipart_formdata(fields, files): From e13d371fefc9c6a67dfe7461cadf67909d210e97 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Thu, 3 Feb 2011 18:03:42 +0800 Subject: [PATCH 201/687] Added API calls "/friendships/incoming" and "/friendships/outgoing" --- twython/twitter_endpoints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 4581c92..c789446 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -73,6 +73,14 @@ api_table = { 'url': '/followers/ids.json', 'method': 'GET', }, + 'getIncomingFriendshipIDs': { + 'url': '/friendships/incoming.json', + 'method': 'GET', + }, + 'getOutgoingFriendshipIDs': { + 'url': '/friendships/outgoing.json', + 'method': 'GET', + }, # Retweets 'reTweet': { From ec6169292d2bff745ae467cd03c5f10a675ec7bd Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Fri, 18 Feb 2011 17:09:19 +0800 Subject: [PATCH 202/687] Added a few missing endpoints, fixed a few wrong endpoints --- twython/twitter_endpoints.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index c789446..6ad15bc 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -26,6 +26,11 @@ api_table = { 'method': 'GET', }, + 'endSession' : { + 'url': '/account/end_session.json', + 'method': 'POST', + }, + # Timeline methods 'getPublicTimeline': { 'url': '/statuses/public_timeline.json', @@ -256,7 +261,11 @@ api_table = { 'method': 'GET', }, 'getListMemberships': { - 'url': '/{{username}}/lists/followers.json', + 'url': '/{{username}}/lists/memberships.json', + 'method': 'GET', + }, + 'getListSubscriptions': { + 'url': '/{{username}}/lists/subscriptions.json', 'method': 'GET', }, 'deleteList': { @@ -283,12 +292,16 @@ api_table = { 'url': '/{{username}}/{{list_id}}/members.json', 'method': 'DELETE', }, + 'getListSubscribers': { + 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'method': 'GET', + }, 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/following.json', + 'url': '/{{username}}/{{list_id}}/subscribers.json', 'method': 'POST', }, 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/following.json', + 'url': '/{{username}}/{{list_id}}/subscribers.json', 'method': 'DELETE', }, From e97ec7ea0acc3587bb466a58a05cb734673994c3 Mon Sep 17 00:00:00 2001 From: Erik Scheffers Date: Fri, 25 Feb 2011 19:54:34 +0800 Subject: [PATCH 203/687] Added 'httplib2' as requirement --- setup.py | 2 +- twython-django | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 160000 twython-django diff --git a/setup.py b/setup.py index da1bed7..420e25b 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( include_package_data = True, # Package dependencies. - install_requires = ['simplejson', 'oauth2'], + install_requires = ['simplejson', 'oauth2', 'httplib2'], # Metadata for PyPI. author = 'Ryan McGrath', diff --git a/twython-django b/twython-django deleted file mode 160000 index fab3d7a..0000000 --- a/twython-django +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fab3d7a216de87bed8aef9830bb9d04a408859eb From f991f91cf861094851b6d7c2136865e4c48bb97b Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Thu, 24 Feb 2011 16:43:23 +0800 Subject: [PATCH 204/687] twython3: fixed __init__.py --- twython3k/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython3k/__init__.py b/twython3k/__init__.py index d93c168..710c742 100644 --- a/twython3k/__init__.py +++ b/twython3k/__init__.py @@ -1 +1 @@ -import core +from .twython import Twython From c32c855f5b8ddfaa7a9e8d342dc419c20fe69aa5 Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Thu, 24 Feb 2011 16:50:03 +0800 Subject: [PATCH 205/687] twython3k: fixed choose_boundary import --- twython3k/twython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index 5c2a546..dd1ebb0 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -17,7 +17,7 @@ import urllib.parse import http.client import httplib2 import mimetypes -import mimetools +from email.generator import _make_boundary import re import oauth2 as oauth @@ -380,7 +380,7 @@ class Twython(object): @staticmethod def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() + BOUNDARY = _make_boundary() CRLF = '\r\n' L = [] for (key, value) in fields: From af37b7f52d2b287786cd924c44937bbbdad06d29 Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Thu, 24 Feb 2011 16:50:23 +0800 Subject: [PATCH 206/687] twython3k: removed unnecessary json magic --- twython3k/twython.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index dd1ebb0..c198de4 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -28,23 +28,7 @@ from .twitter_endpoints import base_url, api_table from urllib.error import HTTPError -# There are some special setups (like, oh, a Django application) where -# 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 -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 - 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. - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") +import json as simplejson class TwythonError(AttributeError): """ From 22fef638ab3aeffbb5e03ae6af699ccac838de0a Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Fri, 25 Feb 2011 20:55:02 +0800 Subject: [PATCH 207/687] twython3k: simplejson wants string, not bytes --- twython3k/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index c198de4..2e686b9 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -155,7 +155,7 @@ class Twython(object): url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) resp, content = self.client.request(url, fn['method']) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) if api_call in api_table: return get.__get__(self) From 7cafb49fe8c6d67d7619c568a089d4675e500eb4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 26 Feb 2011 01:12:25 -0500 Subject: [PATCH 208/687] Updates about 3k library needs, etc --- README.markdown | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 6eb928f..e7090ea 100644 --- a/README.markdown +++ b/README.markdown @@ -16,7 +16,7 @@ for those types of use cases. Twython cannot help you with that or fix the annoy If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. Enjoy! -Requirements +Requirements (2.7 and below; for 3k, read section further down) ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called "simplejson". Depending on your flavor of package manager, you can do the following... @@ -72,10 +72,12 @@ from you using them by this library. Twython 3k ----------------------------------------------------------------------------------------------------- -There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed -to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. +There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to +work in all situations, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. +**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab +his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- From 7f243245073994a8fbc7d3a0d3d0716e29c304ad Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 26 Feb 2011 01:14:21 -0500 Subject: [PATCH 209/687] Updating to 1.4, pushing slew of changes up to Pypi (major thanks to Hades and eriks5 for their patches). --- setup.py | 2 +- twython/twython.py | 2 +- twython3k/twython.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 420e25b..8e3bf48 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.3.6' +__version__ = '1.4' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index b8549fc..a35e42b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.6" +__version__ = "1.4" import urllib import urllib2 diff --git a/twython3k/twython.py b/twython3k/twython.py index 2e686b9..7a86649 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.3.6" +__version__ = "1.4" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse From 8aee9932a5ed271aa48488bcc7c3f059d7b55eff Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 26 Feb 2011 01:37:22 -0500 Subject: [PATCH 210/687] Hmmm, lost submodule status for twython-django; not sure why, adding back. --- .gitmodules | 2 +- twython-django | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 twython-django diff --git a/.gitmodules b/.gitmodules index a3dc19e..7391969 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "twython-django"] path = twython-django - url = git@github.com:ryanmcgrath/twython-django.git + url = git://github.com/ryanmcgrath/twython-django.git diff --git a/twython-django b/twython-django new file mode 160000 index 0000000..e9b3190 --- /dev/null +++ b/twython-django @@ -0,0 +1 @@ +Subproject commit e9b31903727af8e38c4e2f047b8f9e6c9aa9a38f From 9f8c04b0f24a3a18781262e886be98655ae23700 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 26 Feb 2011 01:56:28 -0500 Subject: [PATCH 211/687] Patch for Python 2.5; inspect.getargspec() doesn't return a named tuple pre-2.6, so catch on AttributeError and just check in the entire tuple. Increment to 1.4.1 and pushed to Pypi for the sake of 2.5 users. --- setup.py | 2 +- twython/twython.py | 9 +++++++-- twython3k/twython.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 8e3bf48..46f494e 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4' +__version__ = '1.4.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index a35e42b..7ce8973 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4" +__version__ = "1.4.1" import urllib import urllib2 @@ -48,7 +48,12 @@ except ImportError: raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") # Detect if oauth2 supports the callback_url argument to request -OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in inspect.getargspec(oauth.Client.request).args +OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) +try: + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args +except AttributeError: + # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION class TwythonError(AttributeError): """ diff --git a/twython3k/twython.py b/twython3k/twython.py index 7a86649..4c64a98 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4" +__version__ = "1.4.1" import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse From 4e690f5568b450b27d0f54691e406219d5567832 Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Mon, 28 Feb 2011 19:10:07 +0800 Subject: [PATCH 212/687] Parameters now can be of any type. The patch by Eugene (9133d0f10ec) is nice, because you don't have to manually encode() the parameters yourself. But it sucks a little, because all your parameter values must be unicodes now. Proposed patch encodes unicodes to utf-8 and allows the olde strs and longs (I love to use longs in 'id' fields somewhy). --- twython/twython.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 7ce8973..7232c20 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -176,7 +176,7 @@ class Twython(object): # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, v.encode('utf-8')] for k, v in kwargs.items())), headers = self.headers) + resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())), headers = self.headers) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) resp, content = self.client.request(url, fn['method'], headers = self.headers) @@ -463,3 +463,9 @@ class Twython(object): except: pass return text + + @staticmethod + def encode(text): + if isinstance(text, (str,unicode)): + return Twython.unicode2utf8(text) + return str(text) From f184595ccf31ee235e5d0776cebc9d03e15d0d67 Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Mon, 28 Feb 2011 22:48:44 +0800 Subject: [PATCH 213/687] Allow passing arbitrary arguments to client backend. Useful for timeout, for example. --- twython/twython.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 7232c20..91e9942 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -100,7 +100,7 @@ class AuthError(TwythonError): class Twython(object): - def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None): + def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None, client_args={}): """setup(self, oauth_token = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -112,6 +112,7 @@ class Twython(object): pass it in and it'll be used for all requests going forward. oauth_token_secret - see oauth_token; it's the other half. headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ @@ -142,12 +143,12 @@ class Twython(object): # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token) + self.client = oauth.Client(consumer, token, **client_args) elif consumer is not None: - self.client = oauth.Client(consumer) + self.client = oauth.Client(consumer, **client_args) else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http() + self.client = httplib2.Http(**client_args) def __getattr__(self, api_call): """ From a6d524e79daca1073900376a4d9bb28e18e98a72 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 30 Mar 2011 22:38:07 +0900 Subject: [PATCH 214/687] Update to 1.4.2, catch for Python 2.5 where urlparse doesn't exist (specifically for Google App Engine, which is strangely still on 2.5 --- setup.py | 2 +- twython/twython.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 46f494e..d7c175c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4.1' +__version__ = '1.4.2' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 7ce8973..2c3d232 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,8 +9,9 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.1" +__version__ = "1.4.2" +import cgi import urllib import urllib2 import urlparse @@ -205,7 +206,10 @@ class Twython(object): if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - request_tokens = dict(urlparse.parse_qsl(content)) + try: + request_tokens = dict(urlparse.parse_qsl(content)) + except: + request_tokens = dict(cgi.parse_qsl(content)) oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' @@ -233,8 +237,11 @@ class Twython(object): Returns authorized tokens after they go through the auth_url phase. """ resp, content = self.client.request(self.access_token_url, "GET") - return dict(urlparse.parse_qsl(content)) - + try: + return dict(urlparse.parse_qsl(content)) + except: + return dict(cgi.parse_qsl(content)) + # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, From a3159bd6a7693761f413f69e56978a0e7ae45304 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 2 Apr 2011 18:15:08 +0900 Subject: [PATCH 215/687] Based on a heads up from hp-lw, this appears to have been swapped on Twitter's end. No real idea why, but this fixes the call. Thanks\! --- twython/twython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 203b495..712302a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -280,11 +280,11 @@ class Twython(object): if ids: kwargs['user_id'] = ','.join(map(str, ids)) if screen_names: - kwargs['screen_names'] = ','.join(screen_names) + kwargs['screen_name'] = ','.join(screen_names) lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: - resp, content = self.client.request(lookupURL, "GET", headers = self.headers) + resp, content = self.client.request(lookupURL, "POST", headers = self.headers) return simplejson.loads(content) except HTTPError, e: raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) From e262e80b23537e8addf2a72463d021dfe4196bd9 Mon Sep 17 00:00:00 2001 From: Jonathan Elsas Date: Wed, 11 May 2011 14:25:07 -0400 Subject: [PATCH 216/687] minor changes to ensure streaming.py doesn't throw an IndentationError --- twython/streaming.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/twython/streaming.py b/twython/streaming.py index 8020116..d145bf3 100644 --- a/twython/streaming.py +++ b/twython/streaming.py @@ -42,9 +42,9 @@ except ImportError: class TwythonStreamingError(Exception): def __init__(self, msg): self.msg = msg - + def __str__(self): - return "%s" % str(self.msg) + return str(self.msg) feeds = { "firehose": "http://stream.twitter.com/firehose.json", @@ -58,3 +58,4 @@ feeds = { class Stream(object): def __init__(self, username = None, password = None, feed = "spritzer", user_agent = "Twython Streaming"): + pass From d6d8823dc288a9d0efdde892c495c6a22417bc87 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 9 Aug 2011 18:01:17 -0400 Subject: [PATCH 217/687] Fixes an issue with incompatibility with newer versions of the SimpleGeo OAuth2 library; also fixes an issue with bulkUserLookup fixes not being up on Pypi (issue #37, thanks jmalina327) --- setup.py | 2 +- twython/twython.py | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index d7c175c..1f28737 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4.2' +__version__ = '1.4.3' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 712302a..fad12b7 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.2" +__version__ = "1.4.3" import cgi import urllib @@ -48,13 +48,19 @@ except ImportError: # Seriously wtf is wrong with you if you get this Exception. raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -# Detect if oauth2 supports the callback_url argument to request -OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) -try: - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args -except AttributeError: - # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION +# Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback +# url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P +OAUTH_CALLBACK_IN_URL = False +OAUTH_LIB_SUPPORTS_CALLBACK = False +if float(oauth._version.manual_verstr) <= 1.4: + OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) + try: + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args + except AttributeError: + # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION +else: + OAUTH_CALLBACK_IN_URL = True class TwythonError(AttributeError): """ @@ -214,7 +220,7 @@ class Twython(object): oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' - if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url!='oob' and oauth_callback_confirmed: + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: import warnings warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") oauth_callback_confirmed = False @@ -224,7 +230,7 @@ class Twython(object): } # Use old-style callback argument - if callback_url!='oob' and not oauth_callback_confirmed: + if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): auth_url_params['oauth_callback'] = callback_url request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) From 5a155d4b7c86905afadb7fc370cc9d17f9cf4fd9 Mon Sep 17 00:00:00 2001 From: Edward Hades Date: Sun, 4 Sep 2011 22:59:29 +0800 Subject: [PATCH 218/687] Fix AttributeError in OAuth version detection. Old versions of OAuth didn't have _version attribute, so Twython crashes on them with AttributeError. This version first checks if there is _version attribute. If no, obviously it's an old version. --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index fad12b7..16bddf0 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -52,7 +52,7 @@ except ImportError: # url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P OAUTH_CALLBACK_IN_URL = False OAUTH_LIB_SUPPORTS_CALLBACK = False -if float(oauth._version.manual_verstr) <= 1.4: +if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) try: OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args From 7c5787ce265d3553f5b8a2fec235ce72f1c0da14 Mon Sep 17 00:00:00 2001 From: kracekumar Date: Mon, 3 Oct 2011 23:39:29 +0530 Subject: [PATCH 219/687] changed searchTwitter() to search() --- README.markdown | 4 +- twython/twython.py | 745 ++++++++++++++++++++++--------------------- twython3k/twython.py | 6 +- 3 files changed, 378 insertions(+), 377 deletions(-) diff --git a/README.markdown b/README.markdown index e7090ea..bb8631f 100644 --- a/README.markdown +++ b/README.markdown @@ -44,7 +44,7 @@ Example Use from twython import Twython twitter = Twython() - results = twitter.searchTwitter(q="bert") + results = twitter.search(q = "bert") # More function definitions can be found by reading over twython/twitter_endpoints.py, as well # as skimming the source file. Both are kept human-readable, and are pretty well documented or @@ -65,7 +65,7 @@ Arguments to functions are now exact keyword matches for the Twitter API documen whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped as a named argument to any Twitter function. -For example: the search API looks for arguments under the name "q", so you pass q="query_here" to searchTwitter(). +For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. diff --git a/twython/twython.py b/twython/twython.py index 16bddf0..c687627 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,11 +1,11 @@ #!/usr/bin/python """ - Twython is a library for Python that wraps the Twitter API. - It aims to abstract away all the API endpoints, so that additions to the library - and/or the Twitter API won't cause any overall problems. + Twython is a library for Python that wraps the Twitter API. + It aims to abstract away all the API endpoints, so that additions to the library + and/or the Twitter API won't cause any overall problems. - Questions, comments? ryan@venodesigns.net + Questions, comments? ryan@venodesigns.net """ __author__ = "Ryan McGrath " @@ -34,452 +34,453 @@ from urllib2 import HTTPError # 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 up + import json as 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 - 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. - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + 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 + 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. + from django.utils import simplejson + except: + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") # Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback # url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P OAUTH_CALLBACK_IN_URL = False OAUTH_LIB_SUPPORTS_CALLBACK = False if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: - OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) - try: - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args - except AttributeError: - # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION + OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) + try: + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args + except AttributeError: + # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION else: - OAUTH_CALLBACK_IN_URL = True + OAUTH_CALLBACK_IN_URL = True class TwythonError(AttributeError): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by APILimit and AuthError. + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by APILimit and AuthError. - Note: To use these, the syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: + Note: To use these, the syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: - from twython import TwythonError, APILimit, AuthError - """ - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) + from twython import TwythonError, APILimit, AuthError + """ + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) - def __str__(self): - return repr(self.msg) + 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 - 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 + """ + 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 __str__(self): - return repr(self.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 - your authentication. - """ - def __init__(self, msg): - self.msg = msg + """ + 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) + 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, headers=None, callback_url=None, client_args={}): - """setup(self, oauth_token = None, headers = None) + def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None, client_args={}): + """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). - Parameters: - twitter_token - Given to you when you register your application with Twitter. - twitter_secret - Given to you when you register your application with Twitter. - oauth_token - If you've gone through the authentication process and have a token for this user, - pass it in and it'll be used for all requests going forward. - oauth_token_secret - see oauth_token; it's the other half. - headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} - client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} + Parameters: + twitter_token - Given to you when you register your application with Twitter. + twitter_secret - Given to you when you register your application with Twitter. + oauth_token - If you've gone through the authentication process and have a token for this user, + pass it in and it'll be used for all requests going forward. + oauth_token_secret - see oauth_token; it's the other half. + headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} - ** 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.twitter_token = twitter_token - self.twitter_secret = twitter_secret - self.oauth_token = oauth_token - self.oauth_secret = oauth_token_secret - self.callback_url = callback_url + ** 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.twitter_token = twitter_token + self.twitter_secret = twitter_secret + self.oauth_token = oauth_token + self.oauth_secret = oauth_token_secret + self.callback_url = callback_url - # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers - if self.headers is None: - self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + # If there's headers, set them, otherwise be an embarassing parent for their own good. + self.headers = headers + if self.headers is None: + self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - consumer = None - token = None + consumer = None + token = None - if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + if self.twitter_token is not None and self.twitter_secret is not None: + consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) + if self.oauth_token is not None and self.oauth_secret is not None: + token = oauth.Token(oauth_token, oauth_token_secret) - # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token, **client_args) - elif consumer is not None: - self.client = oauth.Client(consumer, **client_args) - else: - # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http(**client_args) + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. + if consumer is not None and token is not None: + self.client = oauth.Client(consumer, token, **client_args) + elif consumer is not None: + self.client = oauth.Client(consumer, **client_args) + else: + # If they don't do authentication, but still want to request unprotected resources, we need an opener. + self.client = httplib2.Http(**client_args) - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def __getattr__(self, api_call): + """ + The most magically awesome block of code you'll see in 2010. - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P + Rather than list out 9 million damn methods for this API, we just keep a table (see above) of + every API endpoint and their corresponding function id for this library. This pretty much gives + unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is + going to be your bottleneck... well, don't use Python. ;P - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. + For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). + It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, + we can take over and find the API method in our table. We then return a function that downloads and parses + what we're looking for, based on the keywords passed in. - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". + """ + def get(self, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())), headers = self.headers) + else: + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) + resp, content = self.client.request(url, fn['method'], headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content) - if api_call in api_table: - return get.__get__(self) - else: - raise TwythonError, api_call + if api_call in api_table: + return get.__get__(self) + else: + raise TwythonError, api_call - def get_authentication_tokens(self): - """ - get_auth_url(self) + def get_authentication_tokens(self): + """ + get_auth_url(self) - Returns an authorization URL for a user to hit. - """ - callback_url = self.callback_url or 'oob' - - request_args = {} - if OAUTH_LIB_SUPPORTS_CALLBACK: - request_args['callback_url'] = callback_url - - resp, content = self.client.request(self.request_token_url, "GET", **request_args) + Returns an authorization URL for a user to hit. + """ + callback_url = self.callback_url or 'oob' + + request_args = {} + if OAUTH_LIB_SUPPORTS_CALLBACK: + request_args['callback_url'] = callback_url + + resp, content = self.client.request(self.request_token_url, "GET", **request_args) - if resp['status'] != '200': - raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - - try: - request_tokens = dict(urlparse.parse_qsl(content)) - except: - request_tokens = dict(cgi.parse_qsl(content)) - - oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' - - if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: - import warnings - warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") - oauth_callback_confirmed = False + if resp['status'] != '200': + raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + + try: + request_tokens = dict(urlparse.parse_qsl(content)) + except: + request_tokens = dict(cgi.parse_qsl(content)) + + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' + + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: + import warnings + warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") + oauth_callback_confirmed = False - auth_url_params = { - 'oauth_token' : request_tokens['oauth_token'], - } - - # Use old-style callback argument - if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): - auth_url_params['oauth_callback'] = callback_url - - request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) - - return request_tokens + auth_url_params = { + 'oauth_token' : request_tokens['oauth_token'], + } + + # Use old-style callback argument + if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): + auth_url_params['oauth_callback'] = callback_url + + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) + + return request_tokens - def get_authorized_tokens(self): - """ - get_authorized_tokens + def get_authorized_tokens(self): + """ + get_authorized_tokens - Returns authorized tokens after they go through the auth_url phase. - """ - resp, content = self.client.request(self.access_token_url, "GET") - try: - return dict(urlparse.parse_qsl(content)) - except: - return dict(cgi.parse_qsl(content)) - - # ------------------------------------------------------------------------------------------------------------------------ - # The following methods are all different in some manner or require special attention with regards to the Twitter API. - # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, - # but it's not high on the priority list at the moment. - # ------------------------------------------------------------------------------------------------------------------------ + Returns authorized tokens after they go through the auth_url phase. + """ + resp, content = self.client.request(self.access_token_url, "GET") + try: + return dict(urlparse.parse_qsl(content)) + except: + return dict(cgi.parse_qsl(content)) + + # ------------------------------------------------------------------------------------------------------------------------ + # The following methods are all different in some manner or require special attention with regards to the Twitter API. + # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, + # but it's not high on the priority list at the moment. + # ------------------------------------------------------------------------------------------------------------------------ - @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()]) + @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()]) - @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") + @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. + 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`) + 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): - """ 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) - 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. + 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. - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - if ids: - kwargs['user_id'] = ','.join(map(str, ids)) - if screen_names: - kwargs['screen_name'] = ','.join(screen_names) - - lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) - try: - resp, content = self.client.request(lookupURL, "POST", headers = self.headers) - return simplejson.loads(content) - except HTTPError, e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + if ids: + kwargs['user_id'] = ','.join(map(str, ids)) + if screen_names: + kwargs['screen_name'] = ','.join(screen_names) + + lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) + try: + resp, content = self.client.request(lookupURL, "POST", headers = self.headers) + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) - def searchTwitter(self, **kwargs): - """searchTwitter(search_query, **kwargs) + def search(self, **kwargs): + """search(search_query, **kwargs) - Returns tweets that match a specified query. + Returns tweets that match a specified query. - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.searchTwitter(q="jjndf", page="2") - """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) - try: - resp, content = self.client.request(searchURL, "GET", headers = self.headers) - return simplejson.loads(content) - except HTTPError, e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) + e.g x.search(q="jjndf") + """ + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + try: + resp, content = self.client.request(searchURL, "GET", headers = self.headers) + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) - def searchTwitterGen(self, search_query, **kwargs): - """searchTwitterGen(search_query, **kwargs) - Returns a generator of tweets that match a specified query. + def searchTwitterGen(self, search_query, **kwargs): + """searchTwitterGen(search_query, **kwargs) - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + Returns a generator of tweets that match a specified query. - e.g x.searchTwitter(q="jjndf", page="2") - """ - searchURL = Twython.constructApiURL("http://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) - except HTTPError, e: - raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - if not data['results']: - raise StopIteration + e.g x.searchTwitter(q="jjndf", page="2") + """ + searchURL = Twython.constructApiURL("http://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) + except HTTPError, e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code) - for tweet in data['results']: - yield tweet + if not data['results']: + raise StopIteration - if 'page' not in kwargs: - kwargs['page'] = 2 - else: - kwargs['page'] += 1 + for tweet in data['results']: + yield tweet - for tweet in self.searchTwitterGen(search_query, **kwargs): - yield tweet + if 'page' not in kwargs: + kwargs['page'] = 2 + else: + kwargs['page'] += 1 - def isListMember(self, list_id, id, username, version = 1): - """ isListMember(self, list_id, id, version) + for tweet in self.searchTwitterGen(search_query, **kwargs): + yield tweet - Check if a specified user (id) 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) - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + Check if a specified user (id) is a member of the list in question (list_id). - 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. - """ - try: - resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`), headers = self.headers) - return simplejson.loads(content) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. - def isListSubscriber(self, username, list_id, id, version = 1): - """ isListSubscriber(self, list_id, id, version) + 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. + """ + try: + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`), headers = self.headers) + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - Check if a specified user (id) is a subscriber of the list in question (list_id). + def isListSubscriber(self, username, list_id, id, version = 1): + """ isListSubscriber(self, list_id, id, version) - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + Check if a specified user (id) is a subscriber of the list in question (list_id). - 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. - """ - try: - resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`), headers = self.headers) - return simplejson.loads(content) - except HTTPError, e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true", version = 1): - """ updateProfileBackgroundImage(filename, tile="true") + 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. + """ + try: + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`), headers = self.headers) + return simplejson.loads(content) + except HTTPError, e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - Updates the authenticating user's profile background image. + # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. + def updateProfileBackgroundImage(self, filename, tile="true", version = 1): + """ updateProfileBackgroundImage(filename, tile="true") - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + Updates the authenticating user's profile background image. - def updateProfileImage(self, filename, version = 1): - """ updateProfileImage(filename) + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = Twython.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) + return urllib2.urlopen(r).read() + except HTTPError, e: + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - Updates the authenticating user's profile image (avatar). + def updateProfileImage(self, filename, version = 1): + """ updateProfileImage(filename) - Parameters: - 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. - """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) - - def getProfileImageUrl(self, username, size=None, version=1): - """ getProfileImageUrl(username) - - Gets the URL for the user's profile image. - - Parameters: - username - Required. User name of the user you want the image url of. - 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}) - - client = httplib2.Http() - client.follow_redirects = False - resp, content = client.request(url, 'GET') - - if resp.status in (301,302,303,307): - return resp['location'] - elif resp.status == 200: - return simplejson.loads(content) - - raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) + Updates the authenticating user's profile image (avatar). - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body + Parameters: + 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. + """ + try: + files = [("image", filename, open(filename, 'rb').read())] + fields = [] + content_type, body = Twython.encode_multipart_formdata(fields, files) + headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} + r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) + return urllib2.urlopen(r).read() + except HTTPError, e: + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + def getProfileImageUrl(self, username, size=None, version=1): + """ getProfileImageUrl(username) + + Gets the URL for the user's profile image. + + Parameters: + username - Required. User name of the user you want the image url of. + 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}) + + client = httplib2.Http() + client.follow_redirects = False + resp, content = client.request(url, 'GET') + + if resp.status in (301,302,303,307): + return resp['location'] + elif resp.status == 200: + return simplejson.loads(content) + + raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) - @staticmethod - def unicode2utf8(text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text + @staticmethod + def encode_multipart_formdata(fields, files): + BOUNDARY = mimetools.choose_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body - @staticmethod - def encode(text): - if isinstance(text, (str,unicode)): - return Twython.unicode2utf8(text) - return str(text) + @staticmethod + def unicode2utf8(text): + try: + if isinstance(text, unicode): + text = text.encode('utf-8') + except: + pass + return text + + @staticmethod + def encode(text): + if isinstance(text, (str,unicode)): + return Twython.unicode2utf8(text) + return str(text) diff --git a/twython3k/twython.py b/twython3k/twython.py index 4c64a98..bf18869 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -235,15 +235,15 @@ class Twython(object): except HTTPError as e: raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) - def searchTwitter(self, **kwargs): - """searchTwitter(search_query, **kwargs) + def search(self, **kwargs): + """search(search_query, **kwargs) Returns tweets that match a specified query. Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.searchTwitter(q="jjndf", page="2") + e.g x.search(q="jjndf") """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: From d6ac7c18f4d75773159f847270beb11bac0803d1 Mon Sep 17 00:00:00 2001 From: kracekumar Date: Wed, 5 Oct 2011 07:52:10 +0530 Subject: [PATCH 220/687] changed searchTwitter() -> search() and searchTwitterGen() -> searchGen() --- twython/twython.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index c687627..7e7c1a9 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -303,7 +303,7 @@ class Twython(object): Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.search(q="jjndf") + e.g x.search(q = "jjndf", page = '2') """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: @@ -312,23 +312,28 @@ class Twython(object): except HTTPError, 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) - def searchTwitterGen(self, search_query, **kwargs): - """searchTwitterGen(search_query, **kwargs) + def searchGen(self, search_query, **kwargs): + """searchGen(search_query, **kwargs) Returns a generator of tweets that match a specified query. Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.searchTwitter(q="jjndf", page="2") + 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) try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) data = simplejson.loads(content) except HTTPError, e: - raise TwythonError("searchTwitterGen() 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']: raise StopIteration @@ -337,13 +342,26 @@ class Twython(object): yield tweet if 'page' not in kwargs: - kwargs['page'] = 2 + kwargs['page'] = '2' else: - kwargs['page'] += 1 + try: + kwargs['page'] = int(kwargs['page']) + kwargs['page'] += 1 + kwargs['page'] = str(kwargs['page']) + except TypeError: + raise TwythonError("searchGen() exited because page takes int") + except e: + raise TwythonError("searchGen() failed with %s error code" %\ + `e.code`, e.code) - for tweet in self.searchTwitterGen(search_query, **kwargs): + 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): """ isListMember(self, list_id, id, version) From 1d8f2a885156c8a2ef3466a0605937aec9f90a31 Mon Sep 17 00:00:00 2001 From: kracekumar Date: Wed, 5 Oct 2011 08:04:44 +0530 Subject: [PATCH 221/687] fixed typo --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 7e7c1a9..bc77526 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -349,7 +349,7 @@ class Twython(object): kwargs['page'] += 1 kwargs['page'] = str(kwargs['page']) except TypeError: - raise TwythonError("searchGen() exited because page takes int") + raise TwythonError("searchGen() exited because page takes str") except e: raise TwythonError("searchGen() failed with %s error code" %\ `e.code`, e.code) From 1d737b67d90228871b64ed34f524778a4e43d56f Mon Sep 17 00:00:00 2001 From: kracekumar Date: Wed, 5 Oct 2011 19:04:24 +0530 Subject: [PATCH 222/687] Modified searchTwitter() -> search(), searchTwitterGen() -> searchGen() in twython3k --- twython3k/twython.py | 602 ++++++++++++++++++++++--------------------- 1 file changed, 308 insertions(+), 294 deletions(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index bf18869..1cf3e34 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -1,11 +1,11 @@ #!/usr/bin/python """ - Twython is a library for Python that wraps the Twitter API. - It aims to abstract away all the API endpoints, so that additions to the library - and/or the Twitter API won't cause any overall problems. + Twython is a library for Python that wraps the Twitter API. + It aims to abstract away all the API endpoints, so that additions to the library + and/or the Twitter API won't cause any overall problems. - Questions, comments? ryan@venodesigns.net + Questions, comments? ryan@venodesigns.net """ __author__ = "Ryan McGrath " @@ -31,364 +31,378 @@ from urllib.error import HTTPError import json as simplejson class TwythonError(AttributeError): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by APILimit and AuthError. + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by APILimit and AuthError. - Note: To use these, the syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: + Note: To use these, the syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: - from twython import TwythonError, APILimit, AuthError - """ - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) + from twython import TwythonError, APILimit, AuthError + """ + def __init__(self, msg, error_code=None): + self.msg = msg + if error_code == 400: + raise APILimit(msg) - def __str__(self): - return repr(self.msg) + 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 - 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 + """ + 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 __str__(self): - return repr(self.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 - your authentication. - """ - def __init__(self, msg): - self.msg = msg + """ + 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) + 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, headers=None): - """setup(self, oauth_token = None, headers = None) + def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = 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). - Parameters: - twitter_token - Given to you when you register your application with Twitter. - twitter_secret - Given to you when you register your application with Twitter. - oauth_token - If you've gone through the authentication process and have a token for this user, - pass it in and it'll be used for all requests going forward. - oauth_token_secret - see oauth_token; it's the other half. - headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + Parameters: + twitter_token - Given to you when you register your application with Twitter. + twitter_secret - Given to you when you register your application with Twitter. + oauth_token - If you've gone through the authentication process and have a token for this user, + pass it in and it'll be used for all requests going forward. + oauth_token_secret - see oauth_token; it's the other half. + headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} - ** 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.twitter_token = twitter_token - self.twitter_secret = twitter_secret - self.oauth_token = oauth_token - self.oauth_secret = oauth_token_secret + ** 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.twitter_token = twitter_token + self.twitter_secret = twitter_secret + self.oauth_token = oauth_token + self.oauth_secret = oauth_token_secret - # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers - if self.headers is None: - self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} + # If there's headers, set them, otherwise be an embarassing parent for their own good. + self.headers = headers + if self.headers is None: + self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - consumer = None - token = None + consumer = None + token = None - if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + if self.twitter_token is not None and self.twitter_secret is not None: + consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) + if self.oauth_token is not None and self.oauth_secret is not None: + token = oauth.Token(oauth_token, oauth_token_secret) - # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token) - elif consumer is not None: - self.client = oauth.Client(consumer) - else: - # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http() + # Filter down through the possibilities here - if they have a token, if they're first stage, etc. + if consumer is not None and token is not None: + self.client = oauth.Client(consumer, token) + elif consumer is not None: + self.client = oauth.Client(consumer) + else: + # If they don't do authentication, but still want to request unprotected resources, we need an opener. + self.client = httplib2.Http() - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def __getattr__(self, api_call): + """ + The most magically awesome block of code you'll see in 2010. - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P + Rather than list out 9 million damn methods for this API, we just keep a table (see above) of + every API endpoint and their corresponding function id for this library. This pretty much gives + unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is + going to be your bottleneck... well, don't use Python. ;P - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. + For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). + It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, + we can take over and find the API method in our table. We then return a function that downloads and parses + what we're looking for, based on the keywords passed in. - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) + I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". + """ + def get(self, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, v.encode('utf-8')] for k, v in list(kwargs.items())))) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) - resp, content = self.client.request(url, fn['method']) + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, v.encode('utf-8')] for k, v in list(kwargs.items())))) + else: + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) + resp, content = self.client.request(url, fn['method']) - return simplejson.loads(content.decode('utf-8')) + return simplejson.loads(content.decode('utf-8')) - if api_call in api_table: - return get.__get__(self) - else: - raise TwythonError(api_call) + if api_call in api_table: + return get.__get__(self) + else: + raise TwythonError(api_call) - def get_authentication_tokens(self): - """ - get_auth_url(self) + def get_authentication_tokens(self): + """ + get_auth_url(self) - Returns an authorization URL for a user to hit. - """ - resp, content = self.client.request(self.request_token_url, "GET") + Returns an authorization URL for a user to hit. + """ + resp, content = self.client.request(self.request_token_url, "GET") - if resp['status'] != '200': - raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + if resp['status'] != '200': + raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - request_tokens = dict(urllib.parse.parse_qsl(content)) - request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) - return request_tokens + request_tokens = dict(urllib.parse.parse_qsl(content)) + request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + return request_tokens - def get_authorized_tokens(self): - """ - get_authorized_tokens + def get_authorized_tokens(self): + """ + get_authorized_tokens - Returns authorized tokens after they go through the auth_url phase. - """ - resp, content = self.client.request(self.access_token_url, "GET") - return dict(urllib.parse.parse_qsl(content)) + Returns authorized tokens after they go through the auth_url phase. + """ + resp, content = self.client.request(self.access_token_url, "GET") + return dict(urllib.parse.parse_qsl(content)) - # ------------------------------------------------------------------------------------------------------------------------ - # The following methods are all different in some manner or require special attention with regards to the Twitter API. - # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, - # but it's not high on the priority list at the moment. - # ------------------------------------------------------------------------------------------------------------------------ + # ------------------------------------------------------------------------------------------------------------------------ + # The following methods are all different in some manner or require special attention with regards to the Twitter API. + # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, + # but it's not high on the priority list at the moment. + # ------------------------------------------------------------------------------------------------------------------------ - @staticmethod - def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) + @staticmethod + def constructApiURL(base_url, params): + return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) - @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") + @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. + 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 = urllib.request.urlopen(shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)})).read() - return content - except HTTPError as e: - raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) + Parameters: + url_to_shorten - URL to shorten. + shortener - In case you want to use a url shortening service other than is.gd. + """ + try: + content = urllib.request.urlopen(shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)})).read() + return content + except HTTPError as e: + raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - def bulkUserLookup(self, ids = None, screen_names = None, version = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + def bulkUserLookup(self, ids = None, screen_names = None, version = None): + """ bulkUserLookup(self, ids = None, screen_names = None, version = None) - 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. + 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. - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += repr(id) + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," - try: - resp, content = self.client.request(apiURL, "GET") - return simplejson.loads(content) - except HTTPError as e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) + Statuses for the users in question will be returned inline if they exist. Requires authentication! + """ + apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" + if ids is not None: + apiURL += "&user_id=" + for id in ids: + apiURL += repr(id) + "," + if screen_names is not None: + apiURL += "&screen_name=" + for name in screen_names: + apiURL += name + "," + try: + resp, content = self.client.request(apiURL, "GET") + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) - def search(self, **kwargs): - """search(search_query, **kwargs) + def search(self, **kwargs): + """search(search_query, **kwargs) - Returns tweets that match a specified query. + Returns tweets that match a specified query. - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.search(q="jjndf") - """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) - try: - resp, content = self.client.request(searchURL, "GET") - return simplejson.loads(content) - except HTTPError as e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) + e.g x.search(q="jjndf") + """ + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) + try: + resp, content = self.client.request(searchURL, "GET") + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - def searchTwitterGen(self, search_query, **kwargs): - """searchTwitterGen(search_query, **kwargs) + def searchTwitter(self, **kwargs): + """use search(search_query, **kwargs) + searchTwitter("python", page = "2")""" + return search(self, **kwargs) - Returns a generator of tweets that match a specified query. + def searchGen(self, search_query, **kwargs): + """searchGen(search_query, **kwargs) - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. + Returns a generator of tweets that match a specified query. - e.g x.searchTwitter(q="jjndf", page="2") - """ - searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) - try: - resp, content = self.client.request(searchURL, "GET") - data = simplejson.loads(content) - except HTTPError as e: - raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code) + Parameters: + See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - if not data['results']: - raise StopIteration + e.g x.search(search_query="python", page="2") + """ + searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) + try: + resp, content = self.client.request(searchURL, "GET") + data = simplejson.loads(content) + except HTTPError as e: + raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code) - for tweet in data['results']: - yield tweet + if not data['results']: + raise StopIteration - if 'page' not in kwargs: - kwargs['page'] = 2 - else: - kwargs['page'] += 1 + for tweet in data['results']: + yield tweet - for tweet in self.searchTwitterGen(search_query, **kwargs): - yield tweet + if 'page' not in kwargs: + kwargs['page'] = '2' + else: + try: + kwargs['page'] = int(kwargs['page']) + kwargs['page'] += 1 + kwargs['page'] = str(kwargs['page']) + except TypeError: + raise TwythonError("searchGen() exited because page takes str") - def isListMember(self, list_id, id, username, version = 1): - """ isListMember(self, list_id, id, version) + except e: + raise TwythonError("searchGen() failed with %s error code" %\ + repr(e.code), e.code) - Check if a specified user (id) is a member of the list in question (list_id). + for tweet in self.searchGen(search_query, **kwargs): + yield tweet - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + def isListMember(self, list_id, id, username, version = 1): + """ isListMember(self, list_id, id, version) - 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. - """ - try: - resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id))) - return simplejson.loads(content) - except HTTPError as e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + Check if a specified user (id) is a member of the list in question (list_id). - def isListSubscriber(self, username, list_id, id, version = 1): - """ isListSubscriber(self, list_id, id, version) + **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. - Check if a specified user (id) is a subscriber of the list in question (list_id). + 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. + """ + try: + resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id))) + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. + def isListSubscriber(self, username, list_id, id, version = 1): + """ isListSubscriber(self, list_id, id, version) - 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. - """ - try: - resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)) - return simplejson.loads(content) - except HTTPError as e: - raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) + Check if a specified user (id) is a subscriber of the list in question (list_id). - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true", version = 1): - """ updateProfileBackgroundImage(filename, tile="true") + **Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists. - Updates the authenticating user's profile background image. + 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. + """ + try: + resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)) + return simplejson.loads(content) + except HTTPError as e: + raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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: - files = [("image", filename, open(filename, 'rb').read())] - 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) - return urllib.request.urlopen(r).read() - except HTTPError as e: - raise TwythonError("updateProfileBackgroundImage() 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. + def updateProfileBackgroundImage(self, filename, tile="true", version = 1): + """ updateProfileBackgroundImage(filename, tile="true") - def updateProfileImage(self, filename, version = 1): - """ updateProfileImage(filename) + Updates the authenticating user's profile background image. - Updates the authenticating user's profile image (avatar). + Parameters: + image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. + tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. + ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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: + files = [("image", filename, open(filename, 'rb').read())] + 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) + return urllib.request.urlopen(r).read() + except HTTPError as e: + raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) - Parameters: - 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. - """ - try: - files = [("image", filename, open(filename, 'rb').read())] - 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) - return urllib.request.urlopen(r).read() - except HTTPError as e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + def updateProfileImage(self, filename, version = 1): + """ updateProfileImage(filename) - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = _make_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body + Updates the authenticating user's profile image (avatar). - @staticmethod - def unicode2utf8(text): - try: - if isinstance(text, str): - text = text.encode('utf-8') - except: - pass - return text + Parameters: + 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. + """ + try: + files = [("image", filename, open(filename, 'rb').read())] + 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) + return urllib.request.urlopen(r).read() + except HTTPError as e: + raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + @staticmethod + def encode_multipart_formdata(fields, files): + BOUNDARY = _make_boundary() + CRLF = '\r\n' + L = [] + for (key, value) in fields: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) + L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + @staticmethod + def unicode2utf8(text): + try: + if isinstance(text, str): + text = text.encode('utf-8') + except: + pass + return text From 467f27a2a3eea8444c63c976aa6697f9feac817d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 7 Oct 2011 05:12:16 +0900 Subject: [PATCH 223/687] Ah, this was pretty out of date! Thanks to wescpy for the heads up. --- twython3k/twitter_endpoints.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/twython3k/twitter_endpoints.py b/twython3k/twitter_endpoints.py index 42bd5a1..6ad15bc 100644 --- a/twython3k/twitter_endpoints.py +++ b/twython3k/twitter_endpoints.py @@ -21,6 +21,16 @@ api_table = { 'method': 'GET', }, + 'verifyCredentials': { + 'url': '/account/verify_credentials.json', + 'method': 'GET', + }, + + 'endSession' : { + 'url': '/account/end_session.json', + 'method': 'POST', + }, + # Timeline methods 'getPublicTimeline': { 'url': '/statuses/public_timeline.json', @@ -68,6 +78,14 @@ api_table = { 'url': '/followers/ids.json', 'method': 'GET', }, + 'getIncomingFriendshipIDs': { + 'url': '/friendships/incoming.json', + 'method': 'GET', + }, + 'getOutgoingFriendshipIDs': { + 'url': '/friendships/outgoing.json', + 'method': 'GET', + }, # Retweets 'reTweet': { @@ -243,7 +261,11 @@ api_table = { 'method': 'GET', }, 'getListMemberships': { - 'url': '/{{username}}/lists/followers.json', + 'url': '/{{username}}/lists/memberships.json', + 'method': 'GET', + }, + 'getListSubscriptions': { + 'url': '/{{username}}/lists/subscriptions.json', 'method': 'GET', }, 'deleteList': { @@ -270,12 +292,16 @@ api_table = { 'url': '/{{username}}/{{list_id}}/members.json', 'method': 'DELETE', }, + 'getListSubscribers': { + 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'method': 'GET', + }, 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/following.json', + 'url': '/{{username}}/{{list_id}}/subscribers.json', 'method': 'POST', }, 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/following.json', + 'url': '/{{username}}/{{list_id}}/subscribers.json', 'method': 'DELETE', }, From d89b2735bbc16a98effae8acc36700fe71e18223 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 7 Oct 2011 05:20:33 +0900 Subject: [PATCH 224/687] Version bump, fixes to the 3k build (courtesy of wescpy), decode all responses before JSON parsing, 1.4.4 release --- setup.py | 2 +- twython/twython.py | 16 ++-- twython3k/twython.py | 192 ++++++++++++++++++++++++++++++++----------- 3 files changed, 153 insertions(+), 57 deletions(-) diff --git a/setup.py b/setup.py index 1f28737..106cbb6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4.3' +__version__ = '1.4.4' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index bc77526..7cee037 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.3" +__version__ = "1.4.4" import cgi import urllib @@ -189,7 +189,7 @@ class Twython(object): url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) resp, content = self.client.request(url, fn['method'], headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) if api_call in api_table: return get.__get__(self) @@ -291,7 +291,7 @@ class Twython(object): lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: resp, content = self.client.request(lookupURL, "POST", headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code) @@ -308,7 +308,7 @@ class Twython(object): searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) @@ -331,7 +331,7 @@ class Twython(object): searchURL = Twython.constructApiURL("http://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) + data = simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("searchGen() failed with a %s error code." % `e.code`, e.code) @@ -377,7 +377,7 @@ class Twython(object): """ try: resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`), headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -396,7 +396,7 @@ class Twython(object): """ try: resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`), headers = self.headers) - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -462,7 +462,7 @@ class Twython(object): if resp.status in (301,302,303,307): return resp['location'] elif resp.status == 200: - return simplejson.loads(content) + return simplejson.loads(content.decode('utf-8')) raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) diff --git a/twython3k/twython.py b/twython3k/twython.py index 1cf3e34..047941d 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,16 +9,18 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.1" +__version__ = "1.4.4" +import cgi import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.error, urllib.parse import urllib.parse import http.client import httplib2 import mimetypes -from email.generator import _make_boundary +import mimetools import re +import inspect import oauth2 as oauth @@ -28,7 +30,37 @@ from .twitter_endpoints import base_url, api_table from urllib.error import HTTPError -import json as simplejson +# There are some special setups (like, oh, a Django application) where +# 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 +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 + 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. + from django.utils import simplejson + except: + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + +# Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback +# url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P +OAUTH_CALLBACK_IN_URL = False +OAUTH_LIB_SUPPORTS_CALLBACK = False +if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: + OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) + try: + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args + except AttributeError: + # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION +else: + OAUTH_CALLBACK_IN_URL = True class TwythonError(AttributeError): """ @@ -75,7 +107,7 @@ class AuthError(TwythonError): class Twython(object): - def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None): + def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None, client_args={}): """setup(self, oauth_token = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -87,6 +119,7 @@ class Twython(object): pass it in and it'll be used for all requests going forward. oauth_token_secret - see oauth_token; it's the other half. headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} + client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. """ @@ -99,6 +132,7 @@ class Twython(object): self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret + self.callback_url = callback_url # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers @@ -116,12 +150,12 @@ class Twython(object): # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token) + self.client = oauth.Client(consumer, token, **client_args) elif consumer is not None: - self.client = oauth.Client(consumer) + self.client = oauth.Client(consumer, **client_args) else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http() + self.client = httplib2.Http(**client_args) def __getattr__(self, api_call): """ @@ -150,10 +184,10 @@ class Twython(object): # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, v.encode('utf-8')] for k, v in list(kwargs.items())))) + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) else: url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) - resp, content = self.client.request(url, fn['method']) + resp, content = self.client.request(url, fn['method'], headers = self.headers) return simplejson.loads(content.decode('utf-8')) @@ -168,13 +202,39 @@ class Twython(object): Returns an authorization URL for a user to hit. """ - resp, content = self.client.request(self.request_token_url, "GET") + callback_url = self.callback_url or 'oob' + + request_args = {} + if OAUTH_LIB_SUPPORTS_CALLBACK: + request_args['callback_url'] = callback_url + + resp, content = self.client.request(self.request_token_url, "GET", **request_args) if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + + try: + request_tokens = dict(urllib.parse.parse_qsl(content)) + except: + request_tokens = dict(cgi.parse_qsl(content)) + + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' + + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: + import warnings + warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") + oauth_callback_confirmed = False - request_tokens = dict(urllib.parse.parse_qsl(content)) - request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token']) + auth_url_params = { + 'oauth_token' : request_tokens['oauth_token'], + } + + # Use old-style callback argument + if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): + auth_url_params['oauth_callback'] = callback_url + + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.parse.urlencode(auth_url_params) + return request_tokens def get_authorized_tokens(self): @@ -184,8 +244,11 @@ class Twython(object): Returns authorized tokens after they go through the auth_url phase. """ resp, content = self.client.request(self.access_token_url, "GET") - return dict(urllib.parse.parse_qsl(content)) - + try: + return dict(urllib.parse.parse_qsl(content)) + except: + return dict(cgi.parse_qsl(content)) + # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, @@ -194,8 +257,8 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" %(Twython.unicode2utf8(key), urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) - + return base_url + "?" + "&"join(["%s=%s" % (key, urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) + @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") @@ -212,26 +275,23 @@ class Twython(object): except HTTPError as e: raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - def bulkUserLookup(self, ids = None, screen_names = None, version = None): - """ bulkUserLookup(self, ids = None, screen_names = None, version = None) + 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. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that contain their respective data sets. Statuses for the users in question will be returned inline if they exist. Requires authentication! """ - apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1" - if ids is not None: - apiURL += "&user_id=" - for id in ids: - apiURL += repr(id) + "," - if screen_names is not None: - apiURL += "&screen_name=" - for name in screen_names: - apiURL += name + "," + if ids: + kwargs['user_id'] = ','.join(map(str, ids)) + if screen_names: + kwargs['screen_name'] = ','.join(screen_names) + + lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: - resp, content = self.client.request(apiURL, "GET") - return simplejson.loads(content) + resp, content = self.client.request(lookupURL, "POST", headers = self.headers) + return simplejson.loads(content.decode('utf-8')) except HTTPError as e: raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) @@ -243,19 +303,19 @@ class Twython(object): Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.search(q="jjndf") + e.g x.search(q = "jjndf", page = '2') """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: - resp, content = self.client.request(searchURL, "GET") - return simplejson.loads(content) + resp, content = self.client.request(searchURL, "GET", headers = self.headers) + return simplejson.loads(content.decode('utf-8')) except HTTPError as e: raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) def searchTwitter(self, **kwargs): - """use search(search_query, **kwargs) - searchTwitter("python", page = "2")""" - return search(self, **kwargs) + """use search() ,this is a fall back method to support searchTwitter() + """ + return self.search(**kwargs) def searchGen(self, search_query, **kwargs): """searchGen(search_query, **kwargs) @@ -265,14 +325,15 @@ class Twython(object): Parameters: See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - e.g x.search(search_query="python", page="2") + 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) try: - resp, content = self.client.request(searchURL, "GET") - data = simplejson.loads(content) + resp, content = self.client.request(searchURL, "GET", headers = self.headers) + data = simplejson.loads(content.decode('utf-8')) except HTTPError as e: - raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code) + raise TwythonError("searchGen() failed with a %s error code." % repr(e.code), e.code) if not data['results']: raise StopIteration @@ -289,14 +350,18 @@ class Twython(object): 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" %\ - repr(e.code), e.code) + except e: + raise TwythonError("searchGen() failed with %s error code" %\ + repr(e.code), e.code) 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): """ isListMember(self, list_id, id, version) @@ -311,8 +376,8 @@ 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))) - return simplejson.loads(content) + resp, content = self.client.request("http://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) @@ -330,8 +395,8 @@ 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 = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id)) - return simplejson.loads(content) + resp, content = self.client.request("http://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) @@ -375,10 +440,35 @@ class Twython(object): return urllib.request.urlopen(r).read() except HTTPError as e: raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + + def getProfileImageUrl(self, username, size=None, version=1): + """ getProfileImageUrl(username) + + Gets the URL for the user's profile image. + + Parameters: + username - Required. User name of the user you want the image url of. + 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}) + + client = httplib2.Http() + client.follow_redirects = False + resp, content = client.request(url, 'GET') + + if resp.status in (301,302,303,307): + return resp['location'] + elif resp.status == 200: + return simplejson.loads(content.decode('utf-8')) + + raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) @staticmethod def encode_multipart_formdata(fields, files): - BOUNDARY = _make_boundary() + BOUNDARY = mimetools.choose_boundary() CRLF = '\r\n' L = [] for (key, value) in fields: @@ -406,3 +496,9 @@ class Twython(object): except: pass return text + + @staticmethod + def encode(text): + if isinstance(text, str): + return Twython.unicode2utf8(text) + return str(text) From e92f648b815bfc03778098c1b4f544e8502f64a0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 7 Oct 2011 06:27:24 +0900 Subject: [PATCH 225/687] Fix issue #44, typo --- twython3k/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index 047941d..1e09d59 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -257,7 +257,7 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&"join(["%s=%s" % (key, urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) + return base_url + "?" + "&".join(["%s=%s" % (key, urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) @staticmethod def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"): From 446d1e0c18a4b0eab407260a062d9836654789b1 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 7 Oct 2011 06:40:15 +0900 Subject: [PATCH 226/687] Fix issue #45, mimetools.choose_boundary => email.generator._make_boundary() --- twython3k/twython.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index 1e09d59..4705785 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -21,6 +21,7 @@ import mimetypes import mimetools import re import inspect +import email.generator import oauth2 as oauth @@ -468,7 +469,7 @@ class Twython(object): @staticmethod def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() + BOUNDARY = email.generator._make_boundary() CRLF = '\r\n' L = [] for (key, value) in fields: From d2d74b2b4d5e6e30b5a070b912ebb5067674f7c1 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 7 Oct 2011 06:41:29 +0900 Subject: [PATCH 227/687] ...and remove the import... --- twython3k/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index 4705785..f30cfcd 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -18,7 +18,6 @@ import urllib.parse import http.client import httplib2 import mimetypes -import mimetools import re import inspect import email.generator From ae9652a0913f4517b742e193c66131cb41a9caf6 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 11 Oct 2011 05:38:35 +0900 Subject: [PATCH 228/687] Syntax highlighted --- README.markdown | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.markdown b/README.markdown index bb8631f..ccfb1eb 100644 --- a/README.markdown +++ b/README.markdown @@ -35,21 +35,24 @@ Installing Twython is fairly easy. You can... ...or, you can clone the repo and install it the old fashioned way. + git clone git://github.com/ryanmcgrath/twython.git cd twython sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- - from twython import Twython - - twitter = Twython() - results = twitter.search(q = "bert") - - # More function definitions can be found by reading over twython/twitter_endpoints.py, as well - # as skimming the source file. Both are kept human-readable, and are pretty well documented or - # very self documenting. +``` python +from twython import Twython + +twitter = Twython() +results = twitter.search(q = "bert") +# More function definitions can be found by reading over twython/twitter_endpoints.py, as well +# as skimming the source file. Both are kept human-readable, and are pretty well documented or +# very self documenting. +``` + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored From 88474546e1134f2d350e54e046e38cdebceb9e82 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 26 Oct 2011 18:04:59 +0100 Subject: [PATCH 229/687] Adding in end point for userLookup to facilitate https://dev.twitter.com/docs/api/1/get/users/lookup --- twython/twitter_endpoints.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 6ad15bc..030d110 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -119,6 +119,11 @@ api_table = { 'method': 'GET', }, + 'lookupUser': { + 'url': '/users/lookup.json', + 'method': 'GET', + }, + # Status methods - showing, updating, destroying, etc. 'showStatus': { 'url': '/statuses/show/{{id}}.json', From 226f129edb443b2e7c348527986039e0ba108941 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 27 Oct 2011 02:32:28 +0900 Subject: [PATCH 230/687] Copy new endpoint to Twython3k, bump release --- setup.py | 2 +- twython/twython.py | 2 +- twython3k/twitter_endpoints.py | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 106cbb6..90f94a1 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4.4' +__version__ = '1.4.5' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 7cee037..14b5806 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.4" +__version__ = "1.4.5" import cgi import urllib diff --git a/twython3k/twitter_endpoints.py b/twython3k/twitter_endpoints.py index 6ad15bc..030d110 100644 --- a/twython3k/twitter_endpoints.py +++ b/twython3k/twitter_endpoints.py @@ -119,6 +119,11 @@ api_table = { 'method': 'GET', }, + 'lookupUser': { + 'url': '/users/lookup.json', + 'method': 'GET', + }, + # Status methods - showing, updating, destroying, etc. 'showStatus': { 'url': '/statuses/show/{{id}}.json', From 8852e21dc67e88af65670dd6c5ab05aed1a95e42 Mon Sep 17 00:00:00 2001 From: "Gun.io Whitespace Robot" Date: Wed, 26 Oct 2011 21:00:35 -0400 Subject: [PATCH 231/687] Remove whitespace [Gun.io WhitespaceBot] --- README.markdown | 34 +++++++++++++++++----------------- README.txt | 36 ++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/README.markdown b/README.markdown index ccfb1eb..60ad3f6 100644 --- a/README.markdown +++ b/README.markdown @@ -8,7 +8,7 @@ If you used this library and it all stopped working, it's because of the Authent ========================================================================================================= Twitter recently disabled the use of "Basic Authentication", which is why, if you used Twython previously, you probably started getting a ton of 401 errors. To fix this, we should note one thing... - + You need to change how authentication works in your program/application. If you're using a command line application or something, you'll probably languish in hell for a bit, because OAuth wasn't really designed for those types of use cases. Twython cannot help you with that or fix the annoying parts of OAuth. @@ -19,7 +19,7 @@ Enjoy! Requirements (2.7 and below; for 3k, read section further down) ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called -"simplejson". Depending on your flavor of package manager, you can do the following... +"simplejson". Depending on your flavor of package manager, you can do the following... (pip install | easy_install) simplejson @@ -31,26 +31,26 @@ Installation ----------------------------------------------------------------------------------------------------- Installing Twython is fairly easy. You can... - (pip install | easy_install) twython + (pip install | easy_install) twython ...or, you can clone the repo and install it the old fashioned way. - git clone git://github.com/ryanmcgrath/twython.git - cd twython - sudo python setup.py install + git clone git://github.com/ryanmcgrath/twython.git + cd twython + sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- ``` python -from twython import Twython - -twitter = Twython() -results = twitter.search(q = "bert") - -# More function definitions can be found by reading over twython/twitter_endpoints.py, as well -# as skimming the source file. Both are kept human-readable, and are pretty well documented or -# very self documenting. +from twython import Twython + +twitter = Twython() +results = twitter.search(q = "bert") + +# More function definitions can be found by reading over twython/twitter_endpoints.py, as well +# as skimming the source file. Both are kept human-readable, and are pretty well documented or +# very self documenting. ``` A note about the development of Twython (specifically, 1.3) @@ -59,7 +59,7 @@ As of version 1.3, Twython has been extensively overhauled. Most API endpoint de in a separate Python file, and the class itself catches calls to methods that match up in said table. Certain functions require a bit more legwork, and get to stay in the main file, but for the most part -it's all abstracted out. +it's all abstracted out. As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main Twython class to import and use. If you need to catch exceptions, import those from twython as well. @@ -76,7 +76,7 @@ from you using them by this library. Twython 3k ----------------------------------------------------------------------------------------------------- There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to -work in all situations, but it's provided so that others can grab it and hack on it. +work in all situations, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. **OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab @@ -85,7 +85,7 @@ his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/t Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- My hope is that Twython is so simple that you'd never *have* to ask any questions, but if -you feel the need to contact me for this (or other) reasons, you can hit me up +you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. Twython is released under an MIT License - see the LICENSE file for more information. diff --git a/README.txt b/README.txt index 6eb928f..622fa4f 100644 --- a/README.txt +++ b/README.txt @@ -8,7 +8,7 @@ If you used this library and it all stopped working, it's because of the Authent ========================================================================================================= Twitter recently disabled the use of "Basic Authentication", which is why, if you used Twython previously, you probably started getting a ton of 401 errors. To fix this, we should note one thing... - + You need to change how authentication works in your program/application. If you're using a command line application or something, you'll probably languish in hell for a bit, because OAuth wasn't really designed for those types of use cases. Twython cannot help you with that or fix the annoying parts of OAuth. @@ -19,7 +19,7 @@ Enjoy! Requirements ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called -"simplejson". Depending on your flavor of package manager, you can do the following... +"simplejson". Depending on your flavor of package manager, you can do the following... (pip install | easy_install) simplejson @@ -31,32 +31,32 @@ Installation ----------------------------------------------------------------------------------------------------- Installing Twython is fairly easy. You can... - (pip install | easy_install) twython + (pip install | easy_install) twython ...or, you can clone the repo and install it the old fashioned way. - git clone git://github.com/ryanmcgrath/twython.git - cd twython - sudo python setup.py install + git clone git://github.com/ryanmcgrath/twython.git + cd twython + sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- - from twython import Twython - - twitter = Twython() - results = twitter.searchTwitter(q="bert") - - # More function definitions can be found by reading over twython/twitter_endpoints.py, as well - # as skimming the source file. Both are kept human-readable, and are pretty well documented or - # very self documenting. - + from twython import Twython + + twitter = Twython() + results = twitter.searchTwitter(q="bert") + + # More function definitions can be found by reading over twython/twitter_endpoints.py, as well + # as skimming the source file. Both are kept human-readable, and are pretty well documented or + # very self documenting. + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored in a separate Python file, and the class itself catches calls to methods that match up in said table. Certain functions require a bit more legwork, and get to stay in the main file, but for the most part -it's all abstracted out. +it's all abstracted out. As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main Twython class to import and use. If you need to catch exceptions, import those from twython as well. @@ -73,14 +73,14 @@ from you using them by this library. Twython 3k ----------------------------------------------------------------------------------------------------- There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed -to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. +to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- My hope is that Twython is so simple that you'd never *have* to ask any questions, but if -you feel the need to contact me for this (or other) reasons, you can hit me up +you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. Twython is released under an MIT License - see the LICENSE file for more information. From f31446fa3183c5801c6da6294e2c9dde1d6bbc35 Mon Sep 17 00:00:00 2001 From: Kelly Slemko Date: Wed, 9 Nov 2011 14:42:10 -0800 Subject: [PATCH 232/687] Added handling for rate limiting error from search api. Now throws special exception which includes number of seconds to wait before trying again. --- twython/twython.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index 14b5806..329bfae 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -93,6 +93,18 @@ class APILimit(TwythonError): def __str__(self): return repr(self.msg) +class RateLimitError(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): + self.msg = msg + self.retry_wait_seconds = int(retry_wait_seconds) + + def __str__(self): + return repr(self.msg) + class AuthError(TwythonError): """ @@ -308,6 +320,11 @@ class Twython(object): searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) + + if resp.status == 420: + retry_wait_seconds = resp['retry-after'] + raise RateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." % retry_wait_seconds, retry_wait_seconds) + return simplejson.loads(content.decode('utf-8')) except HTTPError, e: raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code) From 4d4aa302d469cfe53227d64f1e4ce0bea2f9250d Mon Sep 17 00:00:00 2001 From: Kelly Slemko Date: Wed, 9 Nov 2011 15:39:02 -0800 Subject: [PATCH 233/687] Modifying how error is constructed to make sure it calls init on the super class --- twython/twython.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 329bfae..d8db763 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -98,9 +98,9 @@ class RateLimitError(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): - self.msg = msg + 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) @@ -321,9 +321,12 @@ class Twython(object): try: resp, content = self.client.request(searchURL, "GET", headers = self.headers) - if resp.status == 420: + if int(resp.status) == 420: retry_wait_seconds = resp['retry-after'] - raise RateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." % retry_wait_seconds, retry_wait_seconds) + raise RateLimitError("getSearchTimeline() is being rate limited. Retry after %s seconds." % + retry_wait_seconds, + retry_wait_seconds, + resp.status) return simplejson.loads(content.decode('utf-8')) except HTTPError, e: From 650a69ec17918bf37ec6d60168fc7debfa52e8ff Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Thu, 24 Nov 2011 13:15:23 +0000 Subject: [PATCH 234/687] Allow for easier debugging/development, by registering endpoints directly into Twython. --- twython/twython.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index d8db763..bb67474 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -168,6 +168,9 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) + # register available funcs to allow listing name when debugging. + for key in api_table.keys(): + self.__dict__[key] = self.__getattr__(key) def __getattr__(self, api_call): """ From 4710c49b2865b0d1fd8a253b4bdaa980c66a08fa Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Wed, 30 Nov 2011 14:29:51 +0000 Subject: [PATCH 235/687] Allow for easier debugging/development, by registering endpoints directly into Twython3k. --- twython3k/twython.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython3k/twython.py b/twython3k/twython.py index f30cfcd..53cab7c 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -156,6 +156,9 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) + # register available funcs to allow listing name when debugging. + for key in api_table.keys(): + self.__dict__[key] = self.__getattr__(key) def __getattr__(self, api_call): """ From 709c8453ea48a520d4a9583f5b8f37c53f3aa878 Mon Sep 17 00:00:00 2001 From: Remy D Date: Wed, 7 Dec 2011 14:22:15 -0500 Subject: [PATCH 236/687] PEP8 Edit: Removed Tab -> added 4 spaces --- core_examples/public_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_examples/public_timeline.py b/core_examples/public_timeline.py index 40d311d..da28e7e 100644 --- a/core_examples/public_timeline.py +++ b/core_examples/public_timeline.py @@ -5,4 +5,4 @@ twitter = Twython() public_timeline = twitter.getPublicTimeline() for tweet in public_timeline: - print tweet["text"] + print tweet["text"] From 9fcd14f3d11adca9d3e9dbb714d6603380bf0520 Mon Sep 17 00:00:00 2001 From: Remy D Date: Wed, 7 Dec 2011 14:23:22 -0500 Subject: [PATCH 237/687] PEP8 Edit: Removed Tab -> added 4 spaces --- core_examples/search_results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_examples/search_results.py b/core_examples/search_results.py index 54c7dd0..607a68f 100644 --- a/core_examples/search_results.py +++ b/core_examples/search_results.py @@ -5,4 +5,4 @@ twitter = Twython() search_results = twitter.searchTwitter(q="WebsDotCom", rpp="50") for tweet in search_results["results"]: - print tweet["text"] + print tweet["text"] From fb8cefd82373c3767ca8a40f27ccd42840e94ce7 Mon Sep 17 00:00:00 2001 From: Remy D Date: Wed, 7 Dec 2011 14:24:23 -0500 Subject: [PATCH 238/687] PEP8 Edit: Removed Tab -> added 4 spaces --- core_examples/update_profile_image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core_examples/update_profile_image.py b/core_examples/update_profile_image.py index 7f3b5ef..857140a 100644 --- a/core_examples/update_profile_image.py +++ b/core_examples/update_profile_image.py @@ -1,9 +1,9 @@ from twython import Twython """ - You'll need to go through the OAuth ritual to be able to successfully - use this function. See the example oauth django application included in - this package for more information. + You'll need to go through the OAuth ritual to be able to successfully + use this function. See the example oauth django application included in + this package for more information. """ twitter = Twython() twitter.updateProfileImage("myImage.png") From cf5b382d55090bdc654f371001320706475357b7 Mon Sep 17 00:00:00 2001 From: Remy D Date: Wed, 7 Dec 2011 14:26:02 -0500 Subject: [PATCH 239/687] PEP8 Edits: Removed Tabs, removed spaces around keywords, linebreaks in lines longer than 80 chars --- core_examples/update_status.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core_examples/update_status.py b/core_examples/update_status.py index 1acc2b8..9b7deca 100644 --- a/core_examples/update_status.py +++ b/core_examples/update_status.py @@ -1,13 +1,14 @@ from twython import Twython """ - Note: for any method that'll require you to be authenticated (updating things, etc) - you'll need to go through the OAuth authentication ritual. See the example - Django application that's included with this package for more information. + Note: for any method that'll require you to be authenticated (updating + things, etc) + you'll need to go through the OAuth authentication ritual. See the example + Django application that's included with this package for more information. """ twitter = Twython() # OAuth ritual... -twitter.updateStatus(status = "See how easy this was?") +twitter.updateStatus(status="See how easy this was?") From 262b7441d41510010dad0cbe590e3aeac110f616 Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Mon, 12 Dec 2011 07:32:40 +0000 Subject: [PATCH 240/687] Get rid of __getattr__ since the endpoints are directly registered into Twython by the constructor. --- twython/twython.py | 58 ++++++++++++++++---------------------------- twython3k/twython.py | 53 ++++++++++++---------------------------- 2 files changed, 37 insertions(+), 74 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index bb67474..4f8c3a4 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -170,46 +170,30 @@ class Twython(object): self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. for key in api_table.keys(): - self.__dict__[key] = self.__getattr__(key) + self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def _constructFunc(self, api_call, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + # The '1' here catches the API version. Slightly + # hilarious. + lambda m: "%s" % kwargs.get(m.group(1), '1'), + base_url + fn['url'] + ) - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P - - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. - - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) - - return simplejson.loads(content.decode('utf-8')) - - if api_call in api_table: - return get.__get__(self) + # Then open and load that shiiit, yo. TODO: check HTTP method + # and junk, handle errors/authentication + if fn['method'] == 'POST': + 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: - raise TwythonError, api_call + 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')) def get_authentication_tokens(self): """ diff --git a/twython3k/twython.py b/twython3k/twython.py index 53cab7c..fe7bcb8 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -158,46 +158,25 @@ class Twython(object): self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. for key in api_table.keys(): - self.__dict__[key] = self.__getattr__(key) + self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def _constructFunc(self, api_call, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P - - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. - - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) - - return simplejson.loads(content.decode('utf-8')) - - if api_call in api_table: - return get.__get__(self) + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) else: - raise TwythonError(api_call) + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) + resp, content = self.client.request(url, fn['method'], headers = self.headers) + + return simplejson.loads(content.decode('utf-8')) def get_authentication_tokens(self): """ From e54183df9cb2654a228fb30edcacd17de8949e85 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Jan 2012 22:37:50 -0500 Subject: [PATCH 241/687] Uploading profile image and profile background image, update setup required packages, removed some funcs. * You can now update user profile image or user profile background image thanks to the Python requests library. * Updated setup to include 'requests' as a required package * Changed to beginning hashbang to use the users environment python version * try/except for parse_qsl, removed try/excepts where it used cgi.parse_qsl/urlparse.parse_sql * Lines 161/162 (using self.consumer/token) <- this addition ended up not being needed, but it doesn't hurt. * updateProfileBackgroundImage() - param 'tile' is now True/False rather than a string "true" or string "false" * removed encode_multipart_formdata func, not needed any longer --- setup.py | 2 +- twython/twython.py | 126 ++++++++++++++++++++++----------------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/setup.py b/setup.py index 90f94a1..a75f04b 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( include_package_data = True, # Package dependencies. - install_requires = ['simplejson', 'oauth2', 'httplib2'], + install_requires = ['simplejson', 'oauth2', 'httplib2', 'requests'], # Metadata for PyPI. author = 'Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index d8db763..abcf0de 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env """ Twython is a library for Python that wraps the Twitter API. @@ -21,9 +21,16 @@ import mimetypes import mimetools import re import inspect +import time +import requests import oauth2 as oauth +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + # 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 @@ -151,20 +158,20 @@ class Twython(object): if self.headers is None: self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - consumer = None - token = None + self.consumer = None + self.token = None if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + self.consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) + self.token = oauth.Token(oauth_token, oauth_token_secret) # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token, **client_args) - elif consumer is not None: - self.client = oauth.Client(consumer, **client_args) + if self.consumer is not None and self.token is not 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. self.client = httplib2.Http(**client_args) @@ -225,10 +232,7 @@ class Twython(object): if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - try: - request_tokens = dict(urlparse.parse_qsl(content)) - except: - request_tokens = dict(cgi.parse_qsl(content)) + request_tokens = dict(parse_qsl(content)) oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' @@ -256,10 +260,7 @@ class Twython(object): Returns authorized tokens after they go through the auth_url phase. """ resp, content = self.client.request(self.access_token_url, "GET") - try: - return dict(urlparse.parse_qsl(content)) - except: - return dict(cgi.parse_qsl(content)) + return dict(parse_qsl(content)) # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. @@ -421,26 +422,36 @@ class Twython(object): 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. - def updateProfileBackgroundImage(self, filename, tile="true", version = 1): - """ updateProfileBackgroundImage(filename, tile="true") + def updateProfileBackgroundImage(self, filename, tile=True, version = 1): + """ updateProfileBackgroundImage(filename, tile=True) Updates the authenticating user's profile background image. Parameters: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + 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. """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + upload_url = 'http://api.twitter.com/%d/account/update_profile_background_image.json' % version + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(length=41), + 'oauth_timestamp': int(time.time()), + } + + #create a fake request with your upload url and parameters + faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) + + #sign the fake request. + signature_method = oauth.SignatureMethod_HMAC_SHA1() + faux_req.sign_request(signature_method, self.consumer, self.token) + + #create a dict out of the fake request signed params + params = dict(parse_qsl(faux_req.to_postdata())) + self.headers.update(faux_req.to_header()) + + req = requests.post(upload_url, data={'tile':tile}, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) + return req.content def updateProfileImage(self, filename, version = 1): """ updateProfileImage(filename) @@ -451,15 +462,26 @@ 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. """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + upload_url = 'http://api.twitter.com/%d/account/update_profile_image.json' % version + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(length=41), + 'oauth_timestamp': int(time.time()), + } + + #create a fake request with your upload url and parameters + faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) + + #sign the fake request. + signature_method = oauth.SignatureMethod_HMAC_SHA1() + faux_req.sign_request(signature_method, self.consumer, self.token) + + #create a dict out of the fake request signed params + params = dict(parse_qsl(faux_req.to_postdata())) + self.headers.update(faux_req.to_header()) + + req = requests.post(upload_url, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) + return req.content def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) @@ -485,29 +507,7 @@ class Twython(object): return simplejson.loads(content.decode('utf-8')) raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) - - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - + @staticmethod def unicode2utf8(text): try: @@ -521,4 +521,4 @@ class Twython(object): def encode(text): if isinstance(text, (str,unicode)): return Twython.unicode2utf8(text) - return str(text) + return str(text) \ No newline at end of file From f9d87b6fd38bb966d3c2561dc8205899eb81d744 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Jan 2012 23:16:36 -0500 Subject: [PATCH 242/687] Left out 'python' in hashbang, update setup to use env, too. Left out 'python' in hashbang, update setup to use env, too. --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a75f04b..c8c7c98 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import sys, os from setuptools import setup diff --git a/twython/twython.py b/twython/twython.py index abcf0de..8a400cf 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,4 +1,4 @@ -#!/usr/bin/env +#!/usr/bin/env python """ Twython is a library for Python that wraps the Twitter API. From 401f610be5b720ac9ba751fb497b9198c2e703a4 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 13 Jan 2012 16:25:52 -0500 Subject: [PATCH 243/687] Generic _media_update() func., added func. to update status with a photo. * Generic _media_update() func. for the 3 media api calls * Added func. to update status with a photo. updateStatusWithMedia() --- twython/twython.py | 51 +++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 90d6328..7cb67f4 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -409,7 +409,7 @@ class Twython(object): 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. - def updateProfileBackgroundImage(self, filename, tile=True, version = 1): + def updateProfileBackgroundImage(self, file_, tile=True, version=1): """ updateProfileBackgroundImage(filename, tile=True) Updates the authenticating user's profile background image. @@ -419,28 +419,9 @@ 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. """ - upload_url = 'http://api.twitter.com/%d/account/update_profile_background_image.json' % version - params = { - 'oauth_version': "1.0", - 'oauth_nonce': oauth.generate_nonce(length=41), - 'oauth_timestamp': int(time.time()), - } + return self._media_update('http://api.twitter.com/%d/account/update_profile_background_image.json' % version, {'image':(file_, open(file_, 'rb'))}, params={'tile':tile}) - #create a fake request with your upload url and parameters - faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) - - #sign the fake request. - signature_method = oauth.SignatureMethod_HMAC_SHA1() - faux_req.sign_request(signature_method, self.consumer, self.token) - - #create a dict out of the fake request signed params - params = dict(parse_qsl(faux_req.to_postdata())) - self.headers.update(faux_req.to_header()) - - req = requests.post(upload_url, data={'tile':tile}, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) - return req.content - - def updateProfileImage(self, filename, version = 1): + def updateProfileImage(self, file_, version=1): """ updateProfileImage(filename) Updates the authenticating user's profile image (avatar). @@ -449,26 +430,40 @@ 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. """ - upload_url = 'http://api.twitter.com/%d/account/update_profile_image.json' % version - params = { + return self._media_update('http://api.twitter.com/%d/account/update_profile_image.json' % version, {'image':(file_, open(file_, 'rb'))}) + + # statuses/update_with_media + def updateStatusWithMedia(self, file_, version=1, **params): + """ updateStatusWithMedia(filename) + + Updates the authenticating user's profile image (avatar). + + Parameters: + 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) + + def _media_update(self, url, file_, params={}): + oauth_params = { 'oauth_version': "1.0", 'oauth_nonce': oauth.generate_nonce(length=41), 'oauth_timestamp': int(time.time()), } #create a fake request with your upload url and parameters - faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) + faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params) #sign the fake request. signature_method = oauth.SignatureMethod_HMAC_SHA1() faux_req.sign_request(signature_method, self.consumer, self.token) #create a dict out of the fake request signed params - params = dict(parse_qsl(faux_req.to_postdata())) self.headers.update(faux_req.to_header()) - req = requests.post(upload_url, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) + req = requests.post(url, data=params, files=file_, headers=self.headers) return req.content + def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) @@ -508,4 +503,4 @@ class Twython(object): def encode(text): if isinstance(text, (str,unicode)): return Twython.unicode2utf8(text) - return str(text) + return str(text) \ No newline at end of file From fcbc702ae574a7a9852e251150ef64cd64c32a1c Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 15 Jan 2012 13:59:28 -0500 Subject: [PATCH 244/687] Fixes an issue in the 1.4.5 point release, first pointed out by @michaelhelmick, fixed thanks to "tatz_tsuchiya". --- twython/twython.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 7cb67f4..f390a62 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -176,8 +176,10 @@ class Twython(object): # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. + def setFunc(key): + return lambda **kwargs: self._constructFunc(key, **kwargs) for key in api_table.keys(): - self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) + self.__dict__[key] = setFunc(key) def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. @@ -503,4 +505,4 @@ class Twython(object): def encode(text): if isinstance(text, (str,unicode)): return Twython.unicode2utf8(text) - return str(text) \ No newline at end of file + return str(text) From a4334bb67d1139109f6e1c7d9eded0f1999aa9c4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 15 Jan 2012 14:06:37 -0500 Subject: [PATCH 245/687] Somewhat bring 3k up to par; currently way behind, will fix after finishing Requests merge --- twython3k/twython.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/twython3k/twython.py b/twython3k/twython.py index fe7bcb8..8f733a8 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.4" +__version__ = "1.4.6" import cgi import urllib.request, urllib.parse, urllib.error @@ -26,7 +26,7 @@ import oauth2 as oauth # 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 from urllib.error import HTTPError @@ -156,9 +156,11 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) + def setFunc(key): + return lambda **kwargs: self._constructFunc(key, **kwargs) # register available funcs to allow listing name when debugging. for key in api_table.keys(): - self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) + self.__dict__[key] = setFunc(key) def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. From eca965715e01db320a7b0495324bac897b9c4859 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 15 Jan 2012 14:07:04 -0500 Subject: [PATCH 246/687] 1.4.6 release for bug fixes - upgrade, please --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c8c7c98..aa9eb71 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '1.4.5' +__version__ = '1.4.6' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index f390a62..801792a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.5" +__version__ = "1.4.6" import cgi import urllib From bb99c90f1ab45ff239fb9a0496122eca44a82ef6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Feb 2012 21:13:08 +0100 Subject: [PATCH 247/687] Extended example --- core_examples/search_results.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core_examples/search_results.py b/core_examples/search_results.py index 607a68f..74cfd40 100644 --- a/core_examples/search_results.py +++ b/core_examples/search_results.py @@ -5,4 +5,5 @@ twitter = Twython() search_results = twitter.searchTwitter(q="WebsDotCom", rpp="50") for tweet in search_results["results"]: - print tweet["text"] + print "Tweet from @%s Date: %s" % (tweet['from_user'].encode('utf-8'),tweet['created_at']) + print tweet['text'].encode('utf-8'),"\n" \ No newline at end of file From 2f749183abf63052fbaf06730a3c6dd94a2c659b Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 28 Feb 2012 15:16:39 -0500 Subject: [PATCH 248/687] PEP8 Cleanup, More Verbosness * Rid of a lot of libs not being used * Changing Exceptions to prefix with "Twython", just safer in case other apps have "AuthError", etc. for some reason. --- twython/twython.py | 131 +++++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 65 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 801792a..6d38ac3 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -11,14 +11,9 @@ __author__ = "Ryan McGrath " __version__ = "1.4.6" -import cgi import urllib import urllib2 -import urlparse -import httplib import httplib2 -import mimetypes -import mimetools import re import inspect import time @@ -62,33 +57,34 @@ OAUTH_LIB_SUPPORTS_CALLBACK = False if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) try: - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args + OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args except AttributeError: # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION else: OAUTH_CALLBACK_IN_URL = True + class TwythonError(AttributeError): """ Generic error class, catch-all for most Twython issues. - Special cases are handled by APILimit and AuthError. + Special cases are handled by TwythonAPILimit and TwythonAuthError. Note: To use these, the syntax has changed as of Twython 1.3. To catch these, you need to explicitly import them into your code, e.g: - from twython import TwythonError, APILimit, AuthError + from twython import TwythonError, TwythonAPILimit, TwythonAuthError """ def __init__(self, msg, error_code=None): self.msg = msg if error_code == 400: - raise APILimit(msg) + raise TwythonAPILimit(msg) def __str__(self): return repr(self.msg) -class APILimit(TwythonError): +class TwythonAPILimit(TwythonError): """ 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 @@ -100,7 +96,8 @@ class APILimit(TwythonError): def __str__(self): return repr(self.msg) -class RateLimitError(TwythonError): + +class TwythonRateLimitError(TwythonError): """ Raised when you've hit a rate limit. retry_wait_seconds is the number of seconds to wait before trying again. @@ -113,7 +110,7 @@ class RateLimitError(TwythonError): return repr(self.msg) -class AuthError(TwythonError): +class TwythonAuthError(TwythonError): """ Raised when you try to access a protected resource and it fails due to some issue with your authentication. @@ -126,7 +123,8 @@ class AuthError(TwythonError): class Twython(object): - def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None, callback_url=None, client_args={}): + def __init__(self, twitter_token=None, twitter_secret=None, oauth_token=None, oauth_token_secret=None, \ + headers=None, callback_url=None, client_args=None): """setup(self, oauth_token = None, headers = None) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -140,7 +138,8 @@ class Twython(object): headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} - ** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported. + ** 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' @@ -161,6 +160,8 @@ class Twython(object): self.consumer = None self.token = None + client_args = client_args or {} + if self.twitter_token is not None and self.twitter_secret is not None: self.consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) @@ -176,6 +177,7 @@ class Twython(object): # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. + def setFunc(key): return lambda **kwargs: self._constructFunc(key, **kwargs) for key in api_table.keys(): @@ -196,10 +198,10 @@ class Twython(object): # and junk, handle errors/authentication if fn['method'] == 'POST': 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) + 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)) + 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')) @@ -211,35 +213,35 @@ class Twython(object): Returns an authorization URL for a user to hit. """ callback_url = self.callback_url or 'oob' - + request_args = {} if OAUTH_LIB_SUPPORTS_CALLBACK: request_args['callback_url'] = callback_url - + resp, content = self.client.request(self.request_token_url, "GET", **request_args) if resp['status'] != '200': - raise AuthError("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" % (resp['status'], content)) + request_tokens = dict(parse_qsl(content)) - - oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' - + + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: import warnings - warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") + warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") oauth_callback_confirmed = False auth_url_params = { - 'oauth_token' : request_tokens['oauth_token'], + 'oauth_token': request_tokens['oauth_token'], } - + # Use old-style callback argument - if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): + if OAUTH_CALLBACK_IN_URL or (callback_url != 'oob' and not oauth_callback_confirmed): auth_url_params['oauth_callback'] = callback_url - + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) - + return request_tokens def get_authorized_tokens(self): @@ -250,7 +252,7 @@ class Twython(object): """ resp, content = self.client.request(self.access_token_url, "GET") return dict(parse_qsl(content)) - + # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, @@ -259,10 +261,10 @@ class Twython(object): @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()]) + 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"): + 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. @@ -275,9 +277,9 @@ class Twython(object): 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`) + 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) A method to do bulk user lookups against the Twitter API. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that @@ -289,13 +291,13 @@ class Twython(object): kwargs['user_id'] = ','.join(map(str, ids)) if screen_names: kwargs['screen_name'] = ','.join(screen_names) - + lookupURL = Twython.constructApiURL("http://api.twitter.com/%d/users/lookup.json" % version, kwargs) try: - resp, content = self.client.request(lookupURL, "POST", headers = self.headers) + resp, content = self.client.request(lookupURL, "POST", headers=self.headers) return simplejson.loads(content.decode('utf-8')) except HTTPError, 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): """search(search_query, **kwargs) @@ -309,18 +311,18 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: - resp, content = self.client.request(searchURL, "GET", headers = self.headers) + resp, content = self.client.request(searchURL, "GET", headers=self.headers) if int(resp.status) == 420: retry_wait_seconds = resp['retry-after'] - raise RateLimitError("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, resp.status) return simplejson.loads(content.decode('utf-8')) except HTTPError, 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): """use search() ,this is a fall back method to support searchTwitter() @@ -340,10 +342,10 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json?q=%s" % Twython.unicode2utf8(search_query), kwargs) try: - resp, content = self.client.request(searchURL, "GET", headers = self.headers) + resp, content = self.client.request(searchURL, "GET", headers=self.headers) data = simplejson.loads(content.decode('utf-8')) except HTTPError, 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']: raise StopIteration @@ -360,9 +362,9 @@ class Twython(object): 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) + except e: + raise TwythonError("searchGen() failed with %s error code" % \ + e.code, e.code) for tweet in self.searchGen(search_query, **kwargs): yield tweet @@ -372,7 +374,7 @@ class Twython(object): searchTwitterGen()""" return self.searchGen(search_query, **kwargs) - def isListMember(self, list_id, id, username, version = 1): + def isListMember(self, list_id, id, username, version=1): """ isListMember(self, list_id, id, version) Check if a specified user (id) is a member of the list in question (list_id). @@ -386,12 +388,12 @@ 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, `id`), headers = self.headers) + resp, content = self.client.request("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')) except HTTPError, e: 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): """ isListSubscriber(self, list_id, id, version) Check if a specified user (id) is a subscriber of the list in question (list_id). @@ -405,7 +407,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, `id`), headers = self.headers) + resp, content = self.client.request("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')) except HTTPError, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) @@ -421,7 +423,7 @@ 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}) + return self._media_update('http://api.twitter.com/%d/account/update_profile_background_image.json' % version, {'image': (file_, open(file_, 'rb'))}, params={'tile': tile}) def updateProfileImage(self, file_, version=1): """ updateProfileImage(filename) @@ -432,7 +434,7 @@ 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'))}) + return self._media_update('http://api.twitter.com/%d/account/update_profile_image.json' % version, {'image': (file_, open(file_, 'rb'))}) # statuses/update_with_media def updateStatusWithMedia(self, file_, version=1, **params): @@ -444,7 +446,7 @@ 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) + 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={}): oauth_params = { @@ -455,23 +457,22 @@ class Twython(object): #create a fake request with your upload url and parameters faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params) - + #sign the fake request. signature_method = oauth.SignatureMethod_HMAC_SHA1() faux_req.sign_request(signature_method, self.consumer, self.token) - + #create a dict out of the fake request signed params self.headers.update(faux_req.to_header()) req = requests.post(url, data=params, files=file_, headers=self.headers) return req.content - def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) - + Gets the URL for the user's profile image. - + Parameters: username - Required. User name of the user you want the image url of. size - Optional. Image size. Valid options include 'normal', 'mini' and 'bigger'. Defaults to 'normal' if not given. @@ -479,19 +480,19 @@ class Twython(object): """ url = "http://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) if size: - url = self.constructApiURL(url, {'size':size}) - + url = self.constructApiURL(url, {'size': size}) + client = httplib2.Http() client.follow_redirects = False resp, content = client.request(url, 'GET') - - if resp.status in (301,302,303,307): + + if resp.status in (301, 302, 303, 307): return resp['location'] elif resp.status == 200: return simplejson.loads(content.decode('utf-8')) - + raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) - + @staticmethod def unicode2utf8(text): try: @@ -503,6 +504,6 @@ class Twython(object): @staticmethod def encode(text): - if isinstance(text, (str,unicode)): + if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) From e3d9ed656b008b147656a074c08f06d4d3ae0334 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 6 Mar 2012 16:58:27 -0500 Subject: [PATCH 249/687] PEP8 Cleanup on Twitter Endpoints --- twython/twitter_endpoints.py | 636 +++++++++++++++++------------------ 1 file changed, 318 insertions(+), 318 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 030d110..cf4690b 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -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 - will be replaced with the keyword that gets passed in to the function at call time. + 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. - 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). + 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). """ # Base Twitter API url, no need to repeat this junk... base_url = 'http://api.twitter.com/{{version}}' -api_table = { - 'getRateLimitStatus': { - 'url': '/account/rate_limit_status.json', - 'method': 'GET', - }, - - 'verifyCredentials': { - 'url': '/account/verify_credentials.json', - 'method': 'GET', - }, - - 'endSession' : { - 'url': '/account/end_session.json', - 'method': 'POST', - }, - - # Timeline methods - 'getPublicTimeline': { - 'url': '/statuses/public_timeline.json', - 'method': 'GET', - }, - 'getHomeTimeline': { - 'url': '/statuses/home_timeline.json', - 'method': 'GET', - }, - 'getUserTimeline': { - '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', - }, - 'destroyFriendship': { - 'url': '/friendships/destroy.json', - 'method': 'POST', - }, - 'getFriendsIDs': { - 'url': '/friends/ids.json', - 'method': 'GET', - }, - 'getFollowersIDs': { - 'url': '/followers/ids.json', - 'method': 'GET', - }, - 'getIncomingFriendshipIDs': { - 'url': '/friendships/incoming.json', - 'method': 'GET', - }, - 'getOutgoingFriendshipIDs': { - 'url': '/friendships/outgoing.json', - 'method': 'GET', - }, - - # Retweets - 'reTweet': { - 'url': '/statuses/retweet/{{id}}.json', - 'method': 'POST', - }, - 'getRetweets': { - 'url': '/statuses/retweets/{{id}}.json', - 'method': 'GET', - }, - 'retweetedOfMe': { - 'url': '/statuses/retweets_of_me.json', - 'method': 'GET', - }, - 'retweetedByMe': { - 'url': '/statuses/retweeted_by_me.json', - 'method': 'GET', - }, - 'retweetedToMe': { - 'url': '/statuses/retweeted_to_me.json', - 'method': 'GET', - }, - - # User methods - 'showUser': { - 'url': '/users/show.json', - 'method': 'GET', - }, - 'searchUsers': { - 'url': '/users/search.json', - 'method': 'GET', - }, - - 'lookupUser': { - 'url': '/users/lookup.json', - 'method': 'GET', - }, - - # Status methods - showing, updating, destroying, etc. - 'showStatus': { - 'url': '/statuses/show/{{id}}.json', - 'method': 'GET', - }, - 'updateStatus': { - 'url': '/statuses/update.json', - 'method': 'POST', - }, - 'destroyStatus': { - 'url': '/statuses/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Direct Messages - getting, sending, effing, etc. - 'getDirectMessages': { - 'url': '/direct_messages.json', - 'method': 'GET', - }, - 'getSentMessages': { - 'url': '/direct_messages/sent.json', - 'method': 'GET', - }, - 'sendDirectMessage': { - 'url': '/direct_messages/new.json', - 'method': 'POST', - }, - 'destroyDirectMessage': { - 'url': '/direct_messages/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Friendship methods - 'checkIfFriendshipExists': { - 'url': '/friendships/exists.json', - 'method': 'GET', - }, - 'showFriendship': { - 'url': '/friendships/show.json', - 'method': 'GET', - }, - - # Profile methods - 'updateProfile': { - 'url': '/account/update_profile.json', - 'method': 'POST', - }, - 'updateProfileColors': { - 'url': '/account/update_profile_colors.json', - 'method': 'POST', - }, - - # Favorites methods - 'getFavorites': { - 'url': '/favorites.json', - 'method': 'GET', - }, - 'createFavorite': { - 'url': '/favorites/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyFavorite': { - 'url': '/favorites/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Blocking methods - 'createBlock': { - 'url': '/blocks/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyBlock': { - 'url': '/blocks/destroy/{{id}}.json', - 'method': 'POST', - }, - 'getBlocking': { - 'url': '/blocks/blocking.json', - 'method': 'GET', - }, - 'getBlockedIDs': { - 'url': '/blocks/blocking/ids.json', - 'method': 'GET', - }, - 'checkIfBlockExists': { - 'url': '/blocks/exists.json', - 'method': 'GET', - }, - - # Trending methods - 'getCurrentTrends': { - 'url': '/trends/current.json', - 'method': 'GET', - }, - 'getDailyTrends': { - 'url': '/trends/daily.json', - 'method': 'GET', - }, - 'getWeeklyTrends': { - 'url': '/trends/weekly.json', - 'method': 'GET', - }, - 'availableTrends': { - 'url': '/trends/available.json', - 'method': 'GET', - }, - 'trendsByLocation': { - 'url': '/trends/{{woeid}}.json', - 'method': 'GET', - }, - - # Saved Searches - 'getSavedSearches': { - 'url': '/saved_searches.json', - 'method': 'GET', - }, - 'showSavedSearch': { - 'url': '/saved_searches/show/{{id}}.json', - 'method': 'GET', - }, - 'createSavedSearch': { - 'url': '/saved_searches/create.json', - 'method': 'GET', - }, - 'destroySavedSearch': { - 'url': '/saved_searches/destroy/{{id}}.json', - 'method': 'GET', - }, - - # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P - 'createList': { - 'url': '/{{username}}/lists.json', - 'method': 'POST', - }, - 'updateList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'POST', - }, - 'showLists': { - 'url': '/{{username}}/lists.json', - 'method': 'GET', - }, - 'getListMemberships': { - 'url': '/{{username}}/lists/memberships.json', - 'method': 'GET', - }, - 'getListSubscriptions': { - 'url': '/{{username}}/lists/subscriptions.json', - 'method': 'GET', - }, - 'deleteList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'DELETE', - }, - 'getListTimeline': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'getSpecificList': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'addListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'POST', - }, - 'getListMembers': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'GET', - }, - 'deleteListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'DELETE', - }, - 'getListSubscribers': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'GET', - }, - 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'POST', - }, - 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'DELETE', - }, +api_table = { + 'getRateLimitStatus': { + 'url': '/account/rate_limit_status.json', + 'method': 'GET', + }, - # The one-offs - 'notificationFollow': { - 'url': '/notifications/follow/follow.json', - 'method': 'POST', - }, - 'notificationLeave': { - 'url': '/notifications/leave/leave.json', - 'method': 'POST', - }, - 'updateDeliveryService': { - 'url': '/account/update_delivery_device.json', - 'method': 'POST', - }, - 'reportSpam': { - 'url': '/report_spam.json', - 'method': 'POST', - }, + 'verifyCredentials': { + 'url': '/account/verify_credentials.json', + 'method': 'GET', + }, + + 'endSession': { + 'url': '/account/end_session.json', + 'method': 'POST', + }, + + # Timeline methods + 'getPublicTimeline': { + 'url': '/statuses/public_timeline.json', + 'method': 'GET', + }, + 'getHomeTimeline': { + 'url': '/statuses/home_timeline.json', + 'method': 'GET', + }, + 'getUserTimeline': { + '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', + }, + 'destroyFriendship': { + 'url': '/friendships/destroy.json', + 'method': 'POST', + }, + 'getFriendsIDs': { + 'url': '/friends/ids.json', + 'method': 'GET', + }, + 'getFollowersIDs': { + 'url': '/followers/ids.json', + 'method': 'GET', + }, + 'getIncomingFriendshipIDs': { + 'url': '/friendships/incoming.json', + 'method': 'GET', + }, + 'getOutgoingFriendshipIDs': { + 'url': '/friendships/outgoing.json', + 'method': 'GET', + }, + + # Retweets + 'reTweet': { + 'url': '/statuses/retweet/{{id}}.json', + 'method': 'POST', + }, + 'getRetweets': { + 'url': '/statuses/retweets/{{id}}.json', + 'method': 'GET', + }, + 'retweetedOfMe': { + 'url': '/statuses/retweets_of_me.json', + 'method': 'GET', + }, + 'retweetedByMe': { + 'url': '/statuses/retweeted_by_me.json', + 'method': 'GET', + }, + 'retweetedToMe': { + 'url': '/statuses/retweeted_to_me.json', + 'method': 'GET', + }, + + # User methods + 'showUser': { + 'url': '/users/show.json', + 'method': 'GET', + }, + 'searchUsers': { + 'url': '/users/search.json', + 'method': 'GET', + }, + + 'lookupUser': { + 'url': '/users/lookup.json', + 'method': 'GET', + }, + + # Status methods - showing, updating, destroying, etc. + 'showStatus': { + 'url': '/statuses/show/{{id}}.json', + 'method': 'GET', + }, + 'updateStatus': { + 'url': '/statuses/update.json', + 'method': 'POST', + }, + 'destroyStatus': { + 'url': '/statuses/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Direct Messages - getting, sending, effing, etc. + 'getDirectMessages': { + 'url': '/direct_messages.json', + 'method': 'GET', + }, + 'getSentMessages': { + 'url': '/direct_messages/sent.json', + 'method': 'GET', + }, + 'sendDirectMessage': { + 'url': '/direct_messages/new.json', + 'method': 'POST', + }, + 'destroyDirectMessage': { + 'url': '/direct_messages/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Friendship methods + 'checkIfFriendshipExists': { + 'url': '/friendships/exists.json', + 'method': 'GET', + }, + 'showFriendship': { + 'url': '/friendships/show.json', + 'method': 'GET', + }, + + # Profile methods + 'updateProfile': { + 'url': '/account/update_profile.json', + 'method': 'POST', + }, + 'updateProfileColors': { + 'url': '/account/update_profile_colors.json', + 'method': 'POST', + }, + + # Favorites methods + 'getFavorites': { + 'url': '/favorites.json', + 'method': 'GET', + }, + 'createFavorite': { + 'url': '/favorites/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyFavorite': { + 'url': '/favorites/destroy/{{id}}.json', + 'method': 'POST', + }, + + # Blocking methods + 'createBlock': { + 'url': '/blocks/create/{{id}}.json', + 'method': 'POST', + }, + 'destroyBlock': { + 'url': '/blocks/destroy/{{id}}.json', + 'method': 'POST', + }, + 'getBlocking': { + 'url': '/blocks/blocking.json', + 'method': 'GET', + }, + 'getBlockedIDs': { + 'url': '/blocks/blocking/ids.json', + 'method': 'GET', + }, + 'checkIfBlockExists': { + 'url': '/blocks/exists.json', + 'method': 'GET', + }, + + # Trending methods + 'getCurrentTrends': { + 'url': '/trends/current.json', + 'method': 'GET', + }, + 'getDailyTrends': { + 'url': '/trends/daily.json', + 'method': 'GET', + }, + 'getWeeklyTrends': { + 'url': '/trends/weekly.json', + 'method': 'GET', + }, + 'availableTrends': { + 'url': '/trends/available.json', + 'method': 'GET', + }, + 'trendsByLocation': { + 'url': '/trends/{{woeid}}.json', + 'method': 'GET', + }, + + # Saved Searches + 'getSavedSearches': { + 'url': '/saved_searches.json', + 'method': 'GET', + }, + 'showSavedSearch': { + 'url': '/saved_searches/show/{{id}}.json', + 'method': 'GET', + }, + 'createSavedSearch': { + 'url': '/saved_searches/create.json', + 'method': 'GET', + }, + 'destroySavedSearch': { + 'url': '/saved_searches/destroy/{{id}}.json', + 'method': 'GET', + }, + + # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P + 'createList': { + 'url': '/{{username}}/lists.json', + 'method': 'POST', + }, + 'updateList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'POST', + }, + 'showLists': { + 'url': '/{{username}}/lists.json', + 'method': 'GET', + }, + 'getListMemberships': { + 'url': '/{{username}}/lists/memberships.json', + 'method': 'GET', + }, + 'getListSubscriptions': { + 'url': '/{{username}}/lists/subscriptions.json', + 'method': 'GET', + }, + 'deleteList': { + 'url': '/{{username}}/lists/{{list_id}}.json', + 'method': 'DELETE', + }, + 'getListTimeline': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'getSpecificList': { + 'url': '/{{username}}/lists/{{list_id}}/statuses.json', + 'method': 'GET', + }, + 'addListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'POST', + }, + 'getListMembers': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'GET', + }, + 'deleteListMember': { + 'url': '/{{username}}/{{list_id}}/members.json', + 'method': 'DELETE', + }, + 'getListSubscribers': { + 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'method': 'GET', + }, + 'subscribeToList': { + 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'method': 'POST', + }, + 'unsubscribeFromList': { + 'url': '/{{username}}/{{list_id}}/subscribers.json', + 'method': 'DELETE', + }, + + # The one-offs + 'notificationFollow': { + 'url': '/notifications/follow/follow.json', + 'method': 'POST', + }, + 'notificationLeave': { + 'url': '/notifications/leave/leave.json', + 'method': 'POST', + }, + 'updateDeliveryService': { + 'url': '/account/update_delivery_device.json', + 'method': 'POST', + }, + 'reportSpam': { + 'url': '/report_spam.json', + 'method': 'POST', + }, } From 8630dc3f03af5390eed1e0598f25f7d0e50146d0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 8 Mar 2012 12:20:04 -0500 Subject: [PATCH 250/687] Twython using requests/requests-oauth --- twython/twython.py | 177 +++++++++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 62 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 6d38ac3..a2a125e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -13,12 +13,13 @@ __version__ = "1.4.6" import urllib import urllib2 -import httplib2 import re import inspect import time import requests +from requests.exceptions import RequestException +from oauth_hook import OAuthHook import oauth2 as oauth try: @@ -124,7 +125,7 @@ class TwythonAuthError(TwythonError): class Twython(object): 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) Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -141,11 +142,16 @@ class Twython(object): ** Note: versioning is not currently used by search.twitter functions; 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. 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.twitter_token = twitter_token self.twitter_secret = twitter_secret self.oauth_token = oauth_token @@ -155,29 +161,23 @@ class Twython(object): # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers 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.token = None - - client_args = client_args or {} + self.client = 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: - 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. - if self.consumer is not None and self.token is not 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 self.client is None: # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http(**client_args) - # register available funcs to allow listing name when debugging. + self.client = requests.session() + # register available funcs to allow listing name when debugging. def setFunc(key): return lambda **kwargs: self._constructFunc(key, **kwargs) for key in api_table.keys(): @@ -194,17 +194,19 @@ class Twython(object): base_url + fn['url'] ) - # Then open and load that shiiit, yo. TODO: check HTTP method - # and junk, handle errors/authentication - if fn['method'] == 'POST': - 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) + method = fn['method'].lower() + if not method in ('get', 'post', 'delete'): + raise TwythonError('Method must be of GET, POST or DELETE') - 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): """ @@ -218,12 +220,14 @@ class Twython(object): if OAUTH_LIB_SUPPORTS_CALLBACK: 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': - raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) + if response.status_code != 200: + 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' @@ -250,8 +254,13 @@ class Twython(object): 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. @@ -294,9 +303,9 @@ class Twython(object): lookupURL = Twython.constructApiURL("http://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')) - except HTTPError, e: + 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) def search(self, **kwargs): @@ -311,17 +320,17 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://search.twitter.com/search.json", kwargs) try: - resp, content = self.client.request(searchURL, "GET", headers=self.headers) + response = self.client.get(searchURL, headers=self.headers) - if int(resp.status) == 420: - retry_wait_seconds = resp['retry-after'] + 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, - resp.status) + response.status_code) - return simplejson.loads(content.decode('utf-8')) - except HTTPError, e: + 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): @@ -342,9 +351,9 @@ class Twython(object): """ searchURL = Twython.constructApiURL("http://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')) - except HTTPError, e: + 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) if not data['results']: @@ -388,9 +397,9 @@ class Twython(object): version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. """ try: - resp, content = self.client.request("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')) - except HTTPError, e: + 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) def isListSubscriber(self, username, list_id, id, version=1): @@ -407,9 +416,9 @@ class Twython(object): version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. """ try: - resp, content = self.client.request("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')) - except HTTPError, e: + 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) # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. @@ -448,10 +457,35 @@ class Twython(object): """ return self._media_update('https://upload.twitter.com/%d/statuses/update_with_media.json' % version, {'media': (file_, open(file_, 'rb'))}, **params) - 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_version': "1.0", - 'oauth_nonce': oauth.generate_nonce(length=41), + 'oauth_consumer_key': self.oauth_hook.consumer_key, + 'oauth_token': self.oauth_token, 'oauth_timestamp': int(time.time()), } @@ -460,7 +494,28 @@ class Twython(object): #sign the fake request. 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 self.headers.update(faux_req.to_header()) @@ -482,16 +537,14 @@ class Twython(object): if size: url = self.constructApiURL(url, {'size': size}) - client = httplib2.Http() - client.follow_redirects = False - resp, content = client.request(url, 'GET') + #client.follow_redirects = False + response = self.client.get(url, allow_redirects=False) + image_url = response.headers.get('location') - if resp.status in (301, 302, 303, 307): - return resp['location'] - elif resp.status == 200: - return simplejson.loads(content.decode('utf-8')) + 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." % resp.status, resp.status) + raise TwythonError("getProfileImageUrl() failed with a %d error code." % response.status_code, response.status_code) @staticmethod def unicode2utf8(text): From 158bf77231f8ad09221205db53aa18ce21df95e3 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 8 Mar 2012 12:24:03 -0500 Subject: [PATCH 251/687] Remove httplib2 dependency, remove "shortenUrl" function, no need for urllib2 either * Removed shortenUrl since Twitter ALWAYS shortens the URL to a t.co, anyways. * Since removing shortenUrl, no need for urllib2 anymore * No need for httplib2 anymore, either --- setup.py | 49 +++++++++++++++++++++++----------------------- twython/twython.py | 18 ----------------- 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/setup.py b/setup.py index aa9eb71..aa4d164 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -import sys, os from setuptools import setup from setuptools import find_packages @@ -8,31 +7,31 @@ __author__ = 'Ryan McGrath ' __version__ = '1.4.6' setup( - # Basic package information. - name = 'twython', - version = __version__, - packages = find_packages(), + # Basic package information. + name='twython', + version=__version__, + packages=find_packages(), - # Packaging options. - include_package_data = True, + # Packaging options. + include_package_data=True, - # Package dependencies. - install_requires = ['simplejson', 'oauth2', 'httplib2', 'requests'], + # Package dependencies. + install_requires=['simplejson', 'oauth2', 'requests', 'requests-oauth'], - # Metadata for PyPI. - author = 'Ryan McGrath', - author_email = 'ryan@venodesigns.net', - license = 'MIT License', - url = 'http://github.com/ryanmcgrath/twython/tree/master', - keywords = 'twitter search api tweet twython', - description = 'An easy (and up to date) way to access Twitter data with Python.', - long_description = open('README.markdown').read(), - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Communications :: Chat', - 'Topic :: Internet' - ] + # Metadata for PyPI. + author='Ryan McGrath', + author_email='ryan@venodesigns.net', + license='MIT License', + url='http://github.com/ryanmcgrath/twython/tree/master', + keywords='twitter search api tweet twython', + description='An easy (and up to date) way to access Twitter data with Python.', + long_description=open('README.markdown').read(), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Communications :: Chat', + 'Topic :: Internet' + ] ) diff --git a/twython/twython.py b/twython/twython.py index a2a125e..80ed8ea 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -12,7 +12,6 @@ __author__ = "Ryan McGrath " __version__ = "1.4.6" import urllib -import urllib2 import re import inspect import time @@ -31,7 +30,6 @@ except ImportError: # table is a file with a dictionary of every API endpoint that Twython supports. from twitter_endpoints import base_url, api_table -from urllib2 import HTTPError # There are some special setups (like, oh, a Django application) where # simplejson exists behind the scenes anyway. Past Python 2.6, this should @@ -272,22 +270,6 @@ class Twython(object): 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()]) - @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): """ bulkUserLookup(self, ids = None, screen_names = None, version = 1, **kwargs) From 9e8bc0912152fdaf2736e2666729977c8c55e833 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 11:41:27 -0400 Subject: [PATCH 252/687] Fixes #67 Dynamic callback url --- twython/twython.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 80ed8ea..a7ad3be 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -215,10 +215,14 @@ class Twython(object): callback_url = self.callback_url or 'oob' request_args = {} - if OAUTH_LIB_SUPPORTS_CALLBACK: - request_args['callback_url'] = callback_url + request_args['oauth_callback'] = callback_url + method = 'get' - response = self.client.get(self.request_token_url, **request_args) + if not OAUTH_LIB_SUPPORTS_CALLBACK: + method = 'post' + + func = getattr(self.client, method) + response = func(self.request_token_url, data=request_args) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) From 9deced8f8b5b1d7b3e02018421574b8eee87732f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:21:34 +0100 Subject: [PATCH 253/687] v1.5.0 release - requests is now the default url/http library, thanks to Mike Helmick - Initial pass at a Streaming API is now included (Twython.stream()), due to how easy requests makes it. Would actually be sad if we *didn't* have this... thanks, Kenneth. >_>; - Return of shortenURL, for people who may have relied on it before. - Deleted streaming handler that existed before but never got implemented fully. - Exceptions now prefixed with Twython, but brought back originals with a more verbose error directing people to new ones, deprecate fully in future. - Twython3k now has an OAuth fix for callback_urls, though it still relies on httplib2. Thanks @jbouvier! - Added a list of contributors to the README files, something which I should have done long ago. Thank you all. --- README.markdown | 26 +++++ README.txt | 51 ++++++++-- setup.py | 2 +- twython/streaming.py | 61 ------------ twython/twython.py | 223 ++++++++++++++++++++++++++++++------------- twython3k/twython.py | 55 ++++++++--- 6 files changed, 266 insertions(+), 152 deletions(-) delete mode 100644 twython/streaming.py diff --git a/README.markdown b/README.markdown index 60ad3f6..be10bb9 100644 --- a/README.markdown +++ b/README.markdown @@ -88,4 +88,30 @@ My hope is that Twython is so simple that you'd never *have* to ask any question you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. +You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. + Twython is released under an MIT License - see the LICENSE file for more information. + +Special Thanks to... +----------------------------------------------------------------------------------------------------- +This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's +exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact +me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;)). + +- **[Mike Helmick (michaelhelmick)](https://github.com/michaelhelmick)**, multiple fixes and proper `requests` integration. +- **[kracekumar](https://github.com/kracekumar)**, early `requests` work and various fixes. +- **[Erik Scheffers (eriks5)](https://github.com/eriks5)**, various fixes regarding OAuth callback URLs. +- **[Jordan Bouvier (jbouvier)](https://github.com/jbouvier)**, various fixes regarding OAuth callback URLs. +- **[Dick Brouwer (dikbrouwer)](https://github.com/dikbrouwer)**, fixes for OAuth Verifier in `get_authorized_tokens`. +- **[hades](https://github.com/hades)**, Fixes to various initial OAuth issues and updates to `Twython3k` to stay current. +- **[Alex Sutton (alexdsutton)](https://github.com/alexsdutton/twython/)**, fix for parameter substitution regular expression (catch underscores!). +- **[Levgen Pyvovarov (bsn)](https://github.com/bsn)**, Various argument fixes, cyrillic text support. +- **[Mark Liu (mliu7)](https://github.com/mliu7)**, Missing parameter fix for `addListMember`. +- **[Randall Degges (rdegges)](https://github.com/rdegges)**, PEP-8 fixes, MANIFEST.in, installer fixes. +- **[Idris Mokhtarzada (idris)](https://github.com/idris)**, Fixes for various example code pieces. +- **[Jonathan Elsas (jelsas)](https://github.com/jelsas)**, Fix for original Streaming API stub causing import errors. +- **[LuqueDaniel](https://github.com/LuqueDaniel)**, Extended example code where necessary. +- **[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. diff --git a/README.txt b/README.txt index 622fa4f..be10bb9 100644 --- a/README.txt +++ b/README.txt @@ -16,7 +16,7 @@ for those types of use cases. Twython cannot help you with that or fix the annoy If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. Enjoy! -Requirements +Requirements (2.7 and below; for 3k, read section further down) ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called "simplejson". Depending on your flavor of package manager, you can do the following... @@ -35,20 +35,23 @@ Installing Twython is fairly easy. You can... ...or, you can clone the repo and install it the old fashioned way. + git clone git://github.com/ryanmcgrath/twython.git cd twython sudo python setup.py install Example Use ----------------------------------------------------------------------------------------------------- - from twython import Twython +``` python +from twython import Twython - twitter = Twython() - results = twitter.searchTwitter(q="bert") +twitter = Twython() +results = twitter.search(q = "bert") - # More function definitions can be found by reading over twython/twitter_endpoints.py, as well - # as skimming the source file. Both are kept human-readable, and are pretty well documented or - # very self documenting. +# More function definitions can be found by reading over twython/twitter_endpoints.py, as well +# as skimming the source file. Both are kept human-readable, and are pretty well documented or +# very self documenting. +``` A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- @@ -65,17 +68,19 @@ Arguments to functions are now exact keyword matches for the Twitter API documen whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped as a named argument to any Twitter function. -For example: the search API looks for arguments under the name "q", so you pass q="query_here" to searchTwitter(). +For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. Twython 3k ----------------------------------------------------------------------------------------------------- -There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed -to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it. +There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to +work in all situations, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. +**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab +his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** Questions, Comments, etc? ----------------------------------------------------------------------------------------------------- @@ -83,4 +88,30 @@ My hope is that Twython is so simple that you'd never *have* to ask any question you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. +You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. + Twython is released under an MIT License - see the LICENSE file for more information. + +Special Thanks to... +----------------------------------------------------------------------------------------------------- +This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's +exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact +me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;)). + +- **[Mike Helmick (michaelhelmick)](https://github.com/michaelhelmick)**, multiple fixes and proper `requests` integration. +- **[kracekumar](https://github.com/kracekumar)**, early `requests` work and various fixes. +- **[Erik Scheffers (eriks5)](https://github.com/eriks5)**, various fixes regarding OAuth callback URLs. +- **[Jordan Bouvier (jbouvier)](https://github.com/jbouvier)**, various fixes regarding OAuth callback URLs. +- **[Dick Brouwer (dikbrouwer)](https://github.com/dikbrouwer)**, fixes for OAuth Verifier in `get_authorized_tokens`. +- **[hades](https://github.com/hades)**, Fixes to various initial OAuth issues and updates to `Twython3k` to stay current. +- **[Alex Sutton (alexdsutton)](https://github.com/alexsdutton/twython/)**, fix for parameter substitution regular expression (catch underscores!). +- **[Levgen Pyvovarov (bsn)](https://github.com/bsn)**, Various argument fixes, cyrillic text support. +- **[Mark Liu (mliu7)](https://github.com/mliu7)**, Missing parameter fix for `addListMember`. +- **[Randall Degges (rdegges)](https://github.com/rdegges)**, PEP-8 fixes, MANIFEST.in, installer fixes. +- **[Idris Mokhtarzada (idris)](https://github.com/idris)**, Fixes for various example code pieces. +- **[Jonathan Elsas (jelsas)](https://github.com/jelsas)**, Fix for original Streaming API stub causing import errors. +- **[LuqueDaniel](https://github.com/LuqueDaniel)**, Extended example code where necessary. +- **[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. diff --git a/setup.py b/setup.py index aa4d164..e797caf 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.4.6' +__version__ = '1.5.0' setup( # Basic package information. diff --git a/twython/streaming.py b/twython/streaming.py deleted file mode 100644 index d145bf3..0000000 --- a/twython/streaming.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python - -""" - TwythonStreamer is an implementation of the Streaming API for Twython. - Pretty self explanatory by reading the code below. It's worth noting - that the end user should, ideally, never import this library, but rather - this is exposed via a linking method in Twython's core. - - Questions, comments? ryan@venodesigns.net -""" - -__author__ = "Ryan McGrath " -__version__ = "1.0.0" - -import urllib -import urllib2 -import urlparse -import httplib -import httplib2 -import re - -from urllib2 import HTTPError - -# There are some special setups (like, oh, a Django application) where -# 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 -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 - 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. - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") - -class TwythonStreamingError(Exception): - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return str(self.msg) - -feeds = { - "firehose": "http://stream.twitter.com/firehose.json", - "gardenhose": "http://stream.twitter.com/gardenhose.json", - "spritzer": "http://stream.twitter.com/spritzer.json", - "birddog": "http://stream.twitter.com/birddog.json", - "shadow": "http://stream.twitter.com/shadow.json", - "follow": "http://stream.twitter.com/follow.json", - "track": "http://stream.twitter.com/track.json", -} - -class Stream(object): - def __init__(self, username = None, password = None, feed = "spritzer", user_agent = "Twython Streaming"): - pass diff --git a/twython/twython.py b/twython/twython.py index a7ad3be..2e469ae 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.6" +__version__ = "1.5.0" import urllib import re @@ -95,6 +95,20 @@ class TwythonAPILimit(TwythonError): 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 + 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 __str__(self): + return repr(self.msg) + class TwythonRateLimitError(TwythonError): """ @@ -121,6 +135,18 @@ class TwythonAuthError(TwythonError): return repr(self.msg) +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): + self.msg = '%s\n Notice: AuthError is deprecated and soon to be removed, catch on TwythonAuthError instead!' % msg + + 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, \ headers=None, callback_url=None): @@ -140,72 +166,70 @@ class Twython(object): ** Note: versioning is not currently used by search.twitter functions; 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. 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.twitter_token = twitter_token self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret self.callback_url = callback_url - + # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: - self.headers = {'User-agent': 'Twython Python Twitter Library v1.4.6'} - + self.headers = {'User-agent': 'Twython Python Twitter Library v' + __version__} + self.client = None - + if self.twitter_token is not None and self.twitter_secret is not None: 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.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. if self.client is None: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = requests.session() - + # register available funcs to allow listing name when debugging. def setFunc(key): return lambda **kwargs: self._constructFunc(key, **kwargs) for key in api_table.keys(): self.__dict__[key] = setFunc(key) - + def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] base = re.sub( '\{\{(?P[a-zA-Z_]+)\}\}', - # The '1' here catches the API version. Slightly - # hilarious. + # The '1' here catches the API version. Slightly hilarious. lambda m: "%s" % kwargs.get(m.group(1), '1'), base_url + fn['url'] ) - + method = fn['method'].lower() if not method in ('get', 'post', 'delete'): raise TwythonError('Method must be of GET, POST or DELETE') - + 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): """ get_auth_url(self) @@ -213,41 +237,41 @@ class Twython(object): Returns an authorization URL for a user to hit. """ callback_url = self.callback_url or 'oob' - + request_args = {} request_args['oauth_callback'] = callback_url method = 'get' - + if not OAUTH_LIB_SUPPORTS_CALLBACK: method = 'post' - + func = getattr(self.client, method) response = func(self.request_token_url, data=request_args) - + if response.status_code != 200: 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(response.content)) if not request_tokens: raise TwythonError('Unable to decode request tokens.') - + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' - + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: import warnings warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") oauth_callback_confirmed = False - + auth_url_params = { 'oauth_token': request_tokens['oauth_token'], } - + # Use old-style callback argument if OAUTH_CALLBACK_IN_URL or (callback_url != 'oob' and not oauth_callback_confirmed): auth_url_params['oauth_callback'] = callback_url - + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) - + return request_tokens def get_authorized_tokens(self): @@ -256,20 +280,41 @@ class Twython(object): Returns authorized tokens after they go through the auth_url phase. """ - 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. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ + + @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. + Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, + but we keep this here for anyone who was previously using it for alternative purposes. ;) + + Parameters: + url_to_shorten - URL to shorten. + shortener = In case you want to use a url shortening service other than is.gd. + """ + request = requests.get('http://is.gd/api.php' , params = { + 'query': url_to_shorten + }) + + if r.status_code in [301, 201, 200]: + return request.text + else: + raise TwythonError('shortenURL() failed with a %s error code.' % r.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()]) @@ -286,14 +331,14 @@ class Twython(object): kwargs['user_id'] = ','.join(map(str, ids)) if screen_names: 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) - + def search(self, **kwargs): """search(search_query, **kwargs) @@ -314,16 +359,16 @@ class Twython(object): 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) - + def searchGen(self, search_query, **kwargs): """searchGen(search_query, **kwargs) @@ -341,13 +386,13 @@ class Twython(object): data = simplejson.loads(response.content.decode('utf-8')) except RequestException, e: raise TwythonError("searchGen() failed with a %s error code." % e.code, e.code) - + if not data['results']: raise StopIteration - + for tweet in data['results']: yield tweet - + if 'page' not in kwargs: kwargs['page'] = '2' else: @@ -360,15 +405,15 @@ class Twython(object): except e: raise TwythonError("searchGen() failed with %s error code" % \ e.code, e.code) - + 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): """ isListMember(self, list_id, id, version) @@ -387,7 +432,7 @@ class Twython(object): return simplejson.loads(response.content.decode('utf-8')) except RequestException, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - + def isListSubscriber(self, username, list_id, id, version=1): """ isListSubscriber(self, list_id, id, version) @@ -406,7 +451,7 @@ class Twython(object): return simplejson.loads(response.content.decode('utf-8')) except RequestException, e: 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. def updateProfileBackgroundImage(self, file_, tile=True, version=1): """ updateProfileBackgroundImage(filename, tile=True) @@ -418,8 +463,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}) - + return self._media_update('http://api.twitter.com/%d/account/update_profile_background_image.json' % version, { + 'image': (file_, open(file_, 'rb')) + }, params = {'tile': tile}) + def updateProfileImage(self, file_, version=1): """ updateProfileImage(filename) @@ -429,8 +476,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('http://api.twitter.com/%d/account/update_profile_image.json' % version, {'image': (file_, open(file_, 'rb'))}) - + return self._media_update('http://api.twitter.com/%d/account/update_profile_image.json' % version, { + 'image': (file_, open(file_, 'rb')) + }) + # statuses/update_with_media def updateStatusWithMedia(self, file_, version=1, **params): """ updateStatusWithMedia(filename) @@ -441,8 +490,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) - + 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=None): params = params or {} @@ -459,7 +510,7 @@ class Twython(object): header.. that MIGHT be why it's not working.. I haven't debugged enough. - - Mike Helmick + - Mike Helmick *** self.oauth_hook.header_auth = True @@ -474,24 +525,24 @@ class Twython(object): 'oauth_token': self.oauth_token, 'oauth_timestamp': int(time.time()), } - + #create a fake request with your upload url and parameters faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params) - + #sign the fake request. signature_method = oauth.SignatureMethod_HMAC_SHA1() - + 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 @@ -500,15 +551,15 @@ class Twython(object): '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 self.headers.update(faux_req.to_header()) - + req = requests.post(url, data=params, files=file_, headers=self.headers) return req.content - + def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) @@ -522,16 +573,58 @@ class Twython(object): url = "http://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) if size: url = self.constructApiURL(url, {'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) + + @staticmethod + def stream(data, callback): + """ + A Streaming API endpoint, because requests (by the lovely Kenneth Reitz) makes this not + stupidly annoying to implement. In reality, Twython does absolutely *nothing special* here, + but people new to programming expect this type of function to exist for this library, so we + provide it for convenience. + Seriously, this is nothing special. :) + + For the basic stream you're probably accessing, you'll want to pass the following as data dictionary + keys. If you need to use OAuth (newer streams), passing secrets/etc as keys SHOULD work... + + username - Required. User name, self explanatory. + password - Required. The Streaming API doesn't use OAuth, so we do this the old school way. It's all + done over SSL (https://), so you're not left totally vulnerable. + endpoint - Optional. Override the endpoint you're using with the Twitter Streaming API. This is defaulted to the one + that everyone has access to, but if Twitter <3's you feel free to set this to your wildest desires. + + Parameters: + data - Required. Dictionary of attributes to attach to the request (see: params https://dev.twitter.com/docs/streaming-api/methods) + callback - Required. Callback function to be fired when tweets come in (this is an event-based-ish API). + """ + endpoint = 'https://stream.twitter.com/1/statuses/filter.json' + if 'endpoint' in data: + endpoint = data.pop('endpoint') + + needs_basic_auth = False + if 'username' in data: + needs_basic_auth = True + username = data.pop('username') + password = data.pop('password') + + if needs_basic_auth: + stream = requests.post(endpoint, data = data, auth = (username, password)) + else: + stream = requests.post(endpoint, data = data) + + for line in stream.iter_lines(): + if line: + callback(json.loads(line)) + @staticmethod def unicode2utf8(text): try: @@ -540,7 +633,7 @@ class Twython(object): except: pass return text - + @staticmethod def encode(text): if isinstance(text, (str, unicode)): diff --git a/twython3k/twython.py b/twython3k/twython.py index 8f733a8..6296e82 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.4.6" +__version__ = "1.4.7" import cgi import urllib.request, urllib.parse, urllib.error @@ -37,16 +37,8 @@ try: # Python 2.6 and up import json as 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 - 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. - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + # Seriously wtf is wrong with you if you get this Exception. + raise Exception("Twython3k requires a json library to work. http://www.undefined.org/python/") # Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback # url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P @@ -81,7 +73,7 @@ class TwythonError(AttributeError): return repr(self.msg) -class APILimit(TwythonError): +class TwythonAPILimit(TwythonError): """ 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 @@ -94,7 +86,22 @@ class APILimit(TwythonError): return repr(self.msg) -class AuthError(TwythonError): +class APILimit(TwythonError): + """ + 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, you should be importing TwythonAPILimit instead. :) + """ + def __init__(self, msg): + self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch TwythonAPILimit instead!' % msg + + 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. @@ -105,6 +112,19 @@ class AuthError(TwythonError): 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 + your authentication. + + DEPRECATED, you should be importing TwythonAuthError instead. + """ + def __init__(self, msg): + self.msg = '%s\n Notice: AuthLimit is deprecated and soon to be removed, catch TwythonAPILimit instead!' % msg + + 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, headers=None, callback_url=None, client_args={}): @@ -189,10 +209,16 @@ class Twython(object): callback_url = self.callback_url or 'oob' request_args = {} + method = 'GET' if OAUTH_LIB_SUPPORTS_CALLBACK: request_args['callback_url'] = callback_url + else: + # This is a hack for versions of oauth that don't support the callback URL. This is also + # done differently than the Python2 version of Twython, which uses Requests internally (as opposed to httplib2). + request_args['body'] = urllib.urlencode({'oauth_callback': callback_url}) + method = 'POST' - resp, content = self.client.request(self.request_token_url, "GET", **request_args) + resp, content = self.client.request(self.request_token_url, method, **request_args) if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) @@ -213,7 +239,6 @@ class Twython(object): 'oauth_token' : request_tokens['oauth_token'], } - # Use old-style callback argument if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): auth_url_params['oauth_callback'] = callback_url From e0c76501bacb5420099a769f8f0f16a5ef3fd13f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:32:25 +0100 Subject: [PATCH 254/687] Note about new Streaming API stuff --- README.markdown | 28 +++++++++++++++++++++++++++- README.txt | 28 +++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index be10bb9..3498cfa 100644 --- a/README.markdown +++ b/README.markdown @@ -16,7 +16,7 @@ for those types of use cases. Twython cannot help you with that or fix the annoy If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. Enjoy! -Requirements (2.7 and below; for 3k, read section further down) +Requirements (2.6~ and below; for 3k, read section further down) ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called "simplejson". Depending on your flavor of package manager, you can do the following... @@ -53,6 +53,32 @@ results = twitter.search(q = "bert") # very self documenting. ``` +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 +Kenneth Reitz. + +**Example Usage:** +``` python +import json +from twython import Twython + +def on_results(results): + """ + A callback to handle passed results. Wheeee. + """ + print json.dumps(results) + +Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' +}, on_results) +``` + + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored diff --git a/README.txt b/README.txt index be10bb9..3498cfa 100644 --- a/README.txt +++ b/README.txt @@ -16,7 +16,7 @@ for those types of use cases. Twython cannot help you with that or fix the annoy If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started. Enjoy! -Requirements (2.7 and below; for 3k, read section further down) +Requirements (2.6~ and below; for 3k, read section further down) ----------------------------------------------------------------------------------------------------- Twython (for versions of Python before 2.6) requires a library called "simplejson". Depending on your flavor of package manager, you can do the following... @@ -53,6 +53,32 @@ results = twitter.search(q = "bert") # very self documenting. ``` +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 +Kenneth Reitz. + +**Example Usage:** +``` python +import json +from twython import Twython + +def on_results(results): + """ + A callback to handle passed results. Wheeee. + """ + print json.dumps(results) + +Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' +}, on_results) +``` + + A note about the development of Twython (specifically, 1.3) ---------------------------------------------------------------------------------------------------- As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored From 6d72b8aa33912a869f7dc2396e5b834a875ba18b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:34:32 +0100 Subject: [PATCH 255/687] Mmmm fix this...? --- README.markdown | 9 ++++++++- README.txt | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index 3498cfa..b776e36 100644 --- a/README.markdown +++ b/README.markdown @@ -60,7 +60,7 @@ Usage is as follows; it's designed to be open-ended enough that you can adapt it 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:** +**Example Usage:** ``` python import json from twython import Twython @@ -118,6 +118,13 @@ You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgr Twython is released under an MIT License - see the LICENSE file for more information. +Want to help? +----------------------------------------------------------------------------------------------------- +Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd +like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help +is always appreciated! + + Special Thanks to... ----------------------------------------------------------------------------------------------------- This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's diff --git a/README.txt b/README.txt index 3498cfa..b776e36 100644 --- a/README.txt +++ b/README.txt @@ -60,7 +60,7 @@ Usage is as follows; it's designed to be open-ended enough that you can adapt it 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:** +**Example Usage:** ``` python import json from twython import Twython @@ -118,6 +118,13 @@ You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgr Twython is released under an MIT License - see the LICENSE file for more information. +Want to help? +----------------------------------------------------------------------------------------------------- +Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd +like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help +is always appreciated! + + Special Thanks to... ----------------------------------------------------------------------------------------------------- This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's From 16a70d0240fd2ad938f386b01eca84cf3256e1ad Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:35:35 +0100 Subject: [PATCH 256/687] README formatting --- README.markdown | 28 ++++++++++++++-------------- README.txt | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.markdown b/README.markdown index b776e36..b281c3f 100644 --- a/README.markdown +++ b/README.markdown @@ -61,22 +61,22 @@ streams. This also exists in large part (read: pretty much in full) thanks to th Kenneth Reitz. **Example Usage:** -``` python -import json -from twython import Twython +``` python +import json +from twython import Twython -def on_results(results): - """ - A callback to handle passed results. Wheeee. - """ - print json.dumps(results) +def on_results(results): + """ + A callback to handle passed results. Wheeee. + """ + print json.dumps(results) -Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' -}, on_results) -``` +Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' +}, on_results) +``` A note about the development of Twython (specifically, 1.3) diff --git a/README.txt b/README.txt index b776e36..b281c3f 100644 --- a/README.txt +++ b/README.txt @@ -61,22 +61,22 @@ streams. This also exists in large part (read: pretty much in full) thanks to th Kenneth Reitz. **Example Usage:** -``` python -import json -from twython import Twython +``` python +import json +from twython import Twython -def on_results(results): - """ - A callback to handle passed results. Wheeee. - """ - print json.dumps(results) +def on_results(results): + """ + A callback to handle passed results. Wheeee. + """ + print json.dumps(results) -Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' -}, on_results) -``` +Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' +}, on_results) +``` A note about the development of Twython (specifically, 1.3) From 87c1f1e71c325a7eb9b20e67524f4c780823f062 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:36:33 +0100 Subject: [PATCH 257/687] README formatting --- README.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index b281c3f..3373d71 100644 --- a/README.markdown +++ b/README.markdown @@ -57,7 +57,7 @@ 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:** @@ -147,4 +147,4 @@ 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. From 8e26e568a6a01b40f68784bdc8c0c527d90c6486 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:37:37 +0100 Subject: [PATCH 258/687] README formatting --- README.markdown | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index 3373d71..629ca67 100644 --- a/README.markdown +++ b/README.markdown @@ -59,8 +59,7 @@ Twython, as of v1.5.0, now includes an experimental **[Twitter Streaming API](ht 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 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) ---------------------------------------------------------------------------------------------------- From 5eb7f29bffe8cd5a6c52d24e2ce7b6f3061f7609 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:19:27 -0400 Subject: [PATCH 259/687] Dynamic Callback URL works again Using POST to set dynamic callback_url decided to break within 3 hours of testing it.. haha. --- twython/twython.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 2e469ae..3ef8278 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -242,9 +242,6 @@ class Twython(object): request_args['oauth_callback'] = callback_url method = 'get' - if not OAUTH_LIB_SUPPORTS_CALLBACK: - method = 'post' - func = getattr(self.client, method) response = func(self.request_token_url, data=request_args) From f917b6bfea338e30d95e8b5d54edb5a8ff1d3fb9 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:25:25 -0400 Subject: [PATCH 260/687] PEP8 Clean up A couple variables were wrong. Somewhere was using 'r' when 'request' was the correct variable Somewhere was using json.loads and not simplejson.loads --- twython/twython.py | 149 +++++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 74 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 3ef8278..899aff1 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -95,12 +95,13 @@ class TwythonAPILimit(TwythonError): 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 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): @@ -168,44 +169,44 @@ class Twython(object): """ OAuthHook.consumer_key = twitter_token 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.twitter_token = twitter_token self.twitter_secret = twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret self.callback_url = callback_url - + # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers if self.headers is None: self.headers = {'User-agent': 'Twython Python Twitter Library v' + __version__} - + self.client = None - + if self.twitter_token is not None and self.twitter_secret is not None: 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.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. if self.client is None: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = requests.session() - + # register available funcs to allow listing name when debugging. def setFunc(key): return lambda **kwargs: self._constructFunc(key, **kwargs) for key in api_table.keys(): self.__dict__[key] = setFunc(key) - + def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] @@ -215,21 +216,21 @@ class Twython(object): lambda m: "%s" % kwargs.get(m.group(1), '1'), base_url + fn['url'] ) - + method = fn['method'].lower() if not method in ('get', 'post', 'delete'): raise TwythonError('Method must be of GET, POST or DELETE') - + 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): """ get_auth_url(self) @@ -237,38 +238,38 @@ class Twython(object): Returns an authorization URL for a user to hit. """ callback_url = self.callback_url or 'oob' - + request_args = {} request_args['oauth_callback'] = callback_url method = 'get' - + func = getattr(self.client, method) response = func(self.request_token_url, data=request_args) - + if response.status_code != 200: 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(response.content)) if not request_tokens: raise TwythonError('Unable to decode request tokens.') - + oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' - + if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: import warnings warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") oauth_callback_confirmed = False - + auth_url_params = { 'oauth_token': request_tokens['oauth_token'], } - + # Use old-style callback argument if OAUTH_CALLBACK_IN_URL or (callback_url != 'oob' and not oauth_callback_confirmed): auth_url_params['oauth_callback'] = callback_url - + request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) - + return request_tokens def get_authorized_tokens(self): @@ -283,35 +284,35 @@ class Twython(object): 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. # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ - + @staticmethod - def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query="longurl"): + 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. + Shortens url specified by url_to_shorten. Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, but we keep this here for anyone who was previously using it for alternative purposes. ;) - + Parameters: url_to_shorten - URL to shorten. shortener = In case you want to use a url shortening service other than is.gd. """ - request = requests.get('http://is.gd/api.php' , params = { + request = requests.get('http://is.gd/api.php', params={ 'query': url_to_shorten }) - - if r.status_code in [301, 201, 200]: + + if request.status_code in [301, 201, 200]: return request.text else: - raise TwythonError('shortenURL() failed with a %s error code.' % r.status_code) - + raise TwythonError('shortenURL() failed with a %s error 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()]) @@ -328,14 +329,14 @@ class Twython(object): kwargs['user_id'] = ','.join(map(str, ids)) if screen_names: 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) - + def search(self, **kwargs): """search(search_query, **kwargs) @@ -356,16 +357,16 @@ class Twython(object): 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) - + def searchGen(self, search_query, **kwargs): """searchGen(search_query, **kwargs) @@ -383,13 +384,13 @@ class Twython(object): data = simplejson.loads(response.content.decode('utf-8')) except RequestException, e: raise TwythonError("searchGen() failed with a %s error code." % e.code, e.code) - + if not data['results']: raise StopIteration - + for tweet in data['results']: yield tweet - + if 'page' not in kwargs: kwargs['page'] = '2' else: @@ -402,15 +403,15 @@ class Twython(object): except e: raise TwythonError("searchGen() failed with %s error code" % \ e.code, e.code) - + 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): """ isListMember(self, list_id, id, version) @@ -429,7 +430,7 @@ class Twython(object): return simplejson.loads(response.content.decode('utf-8')) except RequestException, e: raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) - + def isListSubscriber(self, username, list_id, id, version=1): """ isListSubscriber(self, list_id, id, version) @@ -448,7 +449,7 @@ class Twython(object): return simplejson.loads(response.content.decode('utf-8')) except RequestException, e: 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. def updateProfileBackgroundImage(self, file_, tile=True, version=1): """ updateProfileBackgroundImage(filename, tile=True) @@ -462,8 +463,8 @@ class Twython(object): """ return self._media_update('http://api.twitter.com/%d/account/update_profile_background_image.json' % version, { 'image': (file_, open(file_, 'rb')) - }, params = {'tile': tile}) - + }, params={'tile': tile}) + def updateProfileImage(self, file_, version=1): """ updateProfileImage(filename) @@ -476,7 +477,7 @@ class Twython(object): return self._media_update('http://api.twitter.com/%d/account/update_profile_image.json' % version, { 'image': (file_, open(file_, 'rb')) }) - + # statuses/update_with_media def updateStatusWithMedia(self, file_, version=1, **params): """ updateStatusWithMedia(filename) @@ -490,7 +491,7 @@ class Twython(object): 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=None): params = params or {} @@ -522,24 +523,24 @@ class Twython(object): 'oauth_token': self.oauth_token, 'oauth_timestamp': int(time.time()), } - + #create a fake request with your upload url and parameters faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params) - + #sign the fake request. signature_method = oauth.SignatureMethod_HMAC_SHA1() - + 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 @@ -548,15 +549,15 @@ class Twython(object): '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 self.headers.update(faux_req.to_header()) - + req = requests.post(url, data=params, files=file_, headers=self.headers) return req.content - + def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) @@ -570,16 +571,16 @@ class Twython(object): url = "http://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) if size: url = self.constructApiURL(url, {'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) - + @staticmethod def stream(data, callback): """ @@ -598,7 +599,7 @@ class Twython(object): done over SSL (https://), so you're not left totally vulnerable. endpoint - Optional. Override the endpoint you're using with the Twitter Streaming API. This is defaulted to the one that everyone has access to, but if Twitter <3's you feel free to set this to your wildest desires. - + Parameters: data - Required. Dictionary of attributes to attach to the request (see: params https://dev.twitter.com/docs/streaming-api/methods) callback - Required. Callback function to be fired when tweets come in (this is an event-based-ish API). @@ -606,22 +607,22 @@ class Twython(object): endpoint = 'https://stream.twitter.com/1/statuses/filter.json' if 'endpoint' in data: endpoint = data.pop('endpoint') - + needs_basic_auth = False if 'username' in data: needs_basic_auth = True username = data.pop('username') password = data.pop('password') - + if needs_basic_auth: - stream = requests.post(endpoint, data = data, auth = (username, password)) + stream = requests.post(endpoint, data=data, auth=(username, password)) else: - stream = requests.post(endpoint, data = data) - + stream = requests.post(endpoint, data=data) + for line in stream.iter_lines(): if line: - callback(json.loads(line)) - + callback(simplejson.loads(line)) + @staticmethod def unicode2utf8(text): try: @@ -630,7 +631,7 @@ class Twython(object): except: pass return text - + @staticmethod def encode(text): if isinstance(text, (str, unicode)): From 59b5733a8698cb76f2c33f16713b052467aa27c0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:27:13 -0400 Subject: [PATCH 261/687] Version Number --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e797caf..b52468d 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.0' +__version__ = '1.5.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 899aff1..66df4fc 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.5.0" +__version__ = "1.5.1" import urllib import re From 23e529e1673ef8fd6654ac645481fa060173ce17 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 23 Mar 2012 15:46:19 -0400 Subject: [PATCH 262/687] Passing params through functions now work, bug fix version bump For example: Twython.getHomeTimeline(include_rts=True) was failing. Really sorry about this. It is now fixed. --- setup.py | 2 +- twython/twython.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index b52468d..98c6925 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.1' +__version__ = '1.5.2' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 66df4fc..4bd2fa7 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "1.5.1" +__version__ = "1.5.2" import urllib import re @@ -210,7 +210,7 @@ class Twython(object): def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] - base = re.sub( + url = re.sub( '\{\{(?P[a-zA-Z_]+)\}\}', # The '1' here catches the API version. Slightly hilarious. lambda m: "%s" % kwargs.get(m.group(1), '1'), @@ -221,13 +221,14 @@ class Twython(object): if not method in ('get', 'post', 'delete'): raise TwythonError('Method must be of GET, POST or DELETE') + myargs = {} if method == 'get': - myargs = ['%s=%s' % (key, value) for (key, value) in kwargs.iteritems()] + url = '%s?%s' % (url, urllib.urlencode(kwargs)) else: myargs = kwargs func = getattr(self.client, method) - response = func(base, data=myargs) + response = func(url, data=myargs) return simplejson.loads(response.content.decode('utf-8')) From 03f3a22480fcf275c8f85a785257e62c624bd75a Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 18:12:07 -0400 Subject: [PATCH 263/687] 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 264/687] 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 265/687] 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 e17b3ed87782d5ff2c5b197a3ffdf326da703172 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:02:11 +0200 Subject: [PATCH 266/687] Removed OAuth library callback_url detection code, as callback_url passing does not depend on that anymore. --- twython/twython.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 4bd2fa7..ea7d195 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -49,21 +49,6 @@ except ImportError: # Seriously wtf is wrong with you if you get this Exception. raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -# Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback -# url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P -OAUTH_CALLBACK_IN_URL = False -OAUTH_LIB_SUPPORTS_CALLBACK = False -if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: - OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) - try: - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args - except AttributeError: - # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION -else: - OAUTH_CALLBACK_IN_URL = True - - class TwythonError(AttributeError): """ Generic error class, catch-all for most Twython issues. @@ -256,17 +241,12 @@ class Twython(object): oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') == 'true' - if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: - import warnings - warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") - oauth_callback_confirmed = False - auth_url_params = { 'oauth_token': request_tokens['oauth_token'], } - # Use old-style callback argument - if OAUTH_CALLBACK_IN_URL or (callback_url != 'oob' and not oauth_callback_confirmed): + # Use old-style callback argument if server didn't accept new-style + if callback_url != 'oob' and not oauth_callback_confirmed: auth_url_params['oauth_callback'] = callback_url request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) From f4c00ff996374ea4306ca0c352aef8f00015676a Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:08:12 +0200 Subject: [PATCH 267/687] If callback_url is not set, don't force it to 'oob' --- twython/twython.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index ea7d195..155f936 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -223,10 +223,12 @@ class Twython(object): Returns an authorization URL for a user to hit. """ - callback_url = self.callback_url or 'oob' + callback_url = self.callback_url request_args = {} - request_args['oauth_callback'] = callback_url + if callback_url: + request_args['oauth_callback'] = callback_url + method = 'get' func = getattr(self.client, method) From ffb768d24deaeb1a26c0e3d3324df88ec9c81914 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:09:43 +0200 Subject: [PATCH 268/687] Fix adding callback_url for old style servers --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 155f936..7f09e27 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -248,7 +248,7 @@ class Twython(object): } # Use old-style callback argument if server didn't accept new-style - if callback_url != 'oob' and not oauth_callback_confirmed: + if callback_url and not oauth_callback_confirmed: auth_url_params['oauth_callback'] = callback_url request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) From 703012ef2989e5c2d390e503a837d448b0812480 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 11:44:30 -0400 Subject: [PATCH 269/687] 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 e353125ef123a51a61518789b4e38697995daad5 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 18:48:26 +0200 Subject: [PATCH 270/687] Removed 'import inspect' as it is no longer needed. --- twython/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 7f09e27..bc2904d 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -13,7 +13,6 @@ __version__ = "1.5.2" import urllib import re -import inspect import time import requests From 7205aa402a96e4248aaef0d09af6b8b5d4f76cd2 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 14:19:46 -0400 Subject: [PATCH 271/687] 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 59b038656499f0ea277534b59c3bc6b9f9aebd46 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Sun, 8 Apr 2012 18:37:47 -0400 Subject: [PATCH 272/687] Several improvements to allow for debugging , error handling : - added twitter's http status codes to twitter_endpoints.py ( dict index on status code, value is a tuple of name + description ) - created an internal stash called '_last_call' that stores details of the last api call ( the call, response data, headers, url, etc ) - better error handling for api issues: - - raises an error when the status code is not 200 or 304 - - raises TwythonAPILimit when a 420 rate limit code is returned - - raises a TwythonError on other issues, setting the correct status code and using messages that are from the twitter API - wraps a successful read in a try/except block. there's an error i haven't been able to reproduce where invalid content can get in there, it would be nice to catch it and write a handler for it. ( the previous functions were all introducted to allow this to be debugged ) - added a 'get_lastfunction_header' method. if the API has not been called yet , raises a TwythonError. otherwise it attempts to return the header value twitter last sent. useful for x-ratelimit-limit , x-ratelimit-remaining , x-ratelimit-class , x-ratelimit-reset --- twython/twitter_endpoints.py | 15 ++++++++++ twython/twython.py | 57 ++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index cf4690b..3c56cd3 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 4bd2fa7..aeea26e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -28,7 +28,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 @@ -207,6 +207,9 @@ class Twython(object): for key in api_table.keys(): self.__dict__[key] = setFunc(key) + # create stash for last call intel + self._last_call= None + def _constructFunc(self, api_call, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] @@ -229,8 +232,58 @@ class Twython(object): func = getattr(self.client, method) response = func(url, data=myargs) + content= response.content.decode('utf-8') - return simplejson.loads(response.content.decode('utf-8')) + # create stash for last function intel + self._last_call= { + 'api_call':api_call, + 'api_error':None, + 'cookies':response.cookies, + 'error':response.error, + 'headers':response.headers, + 'status_code':response.status_code, + 'url':response.url, + 'content':content, + } + + if response.status_code not in ( 200 , 304 ): + # handle rate limiting first + if response.status_code == 420 : + raise TwythonAPILimit( "420 || %s || %s" % twitter_http_status_codes[420] ) + if content: + try: + as_json= simplejson.loads(content) + if 'error' in as_json: + self._last_call['api_error']= as_json['error'] + except: + pass + raise TwythonError( "%s || %s || %s" % ( response.status_code , twitter_http_status_codes[response.status_code][0] , twitter_http_status_codes[response.status_code][1] ) , error_code=response.status_code ) + + try: + # sometimes this causes an error, and i haven't caught it yet! + return simplejson.loads(content) + except: + raise + + def get_lastfunction_header(self,header): + """ + get_lastfunction_header(self) + + returns the header in the last function + this must be called after an API call, as it returns header based information. + this will return None if the header is not present + + most useful for the following header information: + x-ratelimit-limit + x-ratelimit-remaining + x-ratelimit-class + x-ratelimit-reset + """ + if self._last_call is None: + raise TwythonError('This function must be called after an API call. It delivers header information.') + if header in self._last_call['headers']: + return self._last_call['headers'][header] + return None def get_authentication_tokens(self): """ From 813626a9add1b166a760c73ecbf3ef3bb44e5f65 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 9 Apr 2012 10:59:13 -0400 Subject: [PATCH 273/687] 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) From 9b798f7ac0fecf310e9a1a34fd9ffef0038c1121 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 10 Apr 2012 10:21:00 -0400 Subject: [PATCH 274/687] added error code tracking into the TwythonError --- twython/twython.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index c4b425c..2f7f787 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -63,6 +63,7 @@ class TwythonError(AttributeError): """ def __init__(self, msg, error_code=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' % \ @@ -71,7 +72,7 @@ class TwythonError(AttributeError): self.msg) if error_code == 400 or error_code == 420: - raise TwythonAPILimit(self.msg) + raise TwythonAPILimit( self.msg , error_code) def __str__(self): return repr(self.msg) @@ -83,8 +84,9 @@ class TwythonAPILimit(TwythonError): 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): + def __init__(self, msg, error_code=None): self.msg = msg + self.error_code = error_code def __str__(self): return repr(self.msg) @@ -123,8 +125,9 @@ 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): + def __init__(self, msg, error_code=None ): self.msg = msg + self.error_code = error_code def __str__(self): return repr(self.msg) @@ -135,8 +138,9 @@ 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): + 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) @@ -405,7 +409,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 3f26325ddb0a6cc13a9c7b253207836f8c44a814 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 10 Apr 2012 14:24:50 -0400 Subject: [PATCH 275/687] 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 276/687] 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 343dcb87ffe33938a8bb5f61bb66cb298736e22f Mon Sep 17 00:00:00 2001 From: Mohammed ALDOUB Date: Sat, 14 Apr 2012 05:34:22 +0300 Subject: [PATCH 277/687] I fixed line 479 to properly URL encode the querystring (q parameter) for the search functionality. According to http://dev.twitter.com/doc/get/search, the q parameter should be URL encoded, but Twython.unicode2utf8 doesn't urlencode the query. So I enclosed it in a urllib.quote_plus function call. examples: >>> urllib.quote_plus(Twython.unicode2utf8('h ^&$')) 'h+%5E%26%24' >>> Twython.unicode2utf8('h ^&$') 'h ^&$' >>> --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 2f7f787..98d1f6f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -476,7 +476,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("http://search.twitter.com/search.json?q=%s" % urllib.quote_plus(Twython.unicode2utf8(search_query)), kwargs) try: response = self.client.get(searchURL, headers=self.headers) data = simplejson.loads(response.content.decode('utf-8')) From 01a7284a8f3928739c410eebd969715a9cccc791 Mon Sep 17 00:00:00 2001 From: Mohammed ALDOUB Date: Sat, 14 Apr 2012 05:53:51 +0300 Subject: [PATCH 278/687] I have added the following lines to the function 'request' starting in line 287: # convert any http Twitter url into https, for the sake of user security # only convert the protocol part, not all occurences of http://, in case users want to search that endpoint = endpoint.replace('http://','https://',1) This is to ensure all passed Twitter urls are converted into https, without messing with the rest of the url. --- twython/twython.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index 2f7f787..978856e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -289,6 +289,9 @@ class Twython(object): # In case they want to pass a full Twitter URL # i.e. http://search.twitter.com/ + # convert any http Twitter url into https, for the sake of user security + # only convert the protocol part, not all occurences of http://, in case users want to search that + endpoint = endpoint.replace('http://','https://',1) if endpoint.startswith('http://'): url = endpoint else: From aabd29a01ef649e76116d72d6f6504550e2e07d0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 19 Apr 2012 18:38:10 -0400 Subject: [PATCH 279/687] Swap http => https for endpoint access, added Voulnet to contributers in the README --- README.markdown | 1 + README.txt | 10 +++++----- twython/twython.py | 24 ++++++++++++------------ twython3k/twython.py | 22 +++++++++++----------- 4 files changed, 29 insertions(+), 28 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 c4b425c..28825b3 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -165,11 +165,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 @@ -285,7 +285,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.startwith('https://'): url = endpoint else: url = '%s%s.json' % (self.api_url % version, endpoint) @@ -424,7 +424,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: response = self.client.post(lookupURL, headers=self.headers) return simplejson.loads(response.content.decode('utf-8')) @@ -441,7 +441,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: response = self.client.get(searchURL, headers=self.headers) @@ -472,7 +472,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: response = self.client.get(searchURL, headers=self.headers) data = simplejson.loads(response.content.decode('utf-8')) @@ -520,7 +520,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: - response = self.client.get("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, id), headers=self.headers) + response = self.client.get("https://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) @@ -539,7 +539,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: - response = self.client.get("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, id), headers=self.headers) + response = self.client.get("https://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) @@ -662,7 +662,7 @@ 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 = "https://api.twitter.com/%s/users/profile_image/%s.json" % (version, username) if size: url = self.constructApiURL(url, {'size': size}) 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 ac18837ed66e5baeb862e4e9e877fe18686de10b Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 19 Apr 2012 19:31:07 -0400 Subject: [PATCH 280/687] 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 281/687] 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 282/687] 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) ---------------------------------------------------------------------------------------------------- From 19293b54a9981a6cc316dc8394eb7e09715a6b94 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sun, 13 May 2012 12:38:30 -0400 Subject: [PATCH 283/687] Remove exceptions and methods in 2.0 * update twitter_endpoints with isListSubscriber and isListMember instead of having them in twython.py * app_key and app_secret in place to take over twitter_token and twitter_secret * updated methods to have the short hand description show up, should always be on first line and the description.. not repeating the function * fixed other method docs and stuff --- twython/twitter_endpoints.py | 8 ++ twython/twython.py | 186 +++++------------------------------ 2 files changed, 30 insertions(+), 164 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 7b1aae7..c553c77 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -257,6 +257,10 @@ api_table = { 'url': '/lists/subscriptions.json', 'method': 'GET', }, + 'isListSubscriber': { + 'url': '/lists/subscribers/show.json', + 'method': 'GET', + }, 'deleteList': { 'url': '/lists/destroy.json', 'method': 'POST', @@ -273,6 +277,10 @@ api_table = { 'url': '/lists/statuses.json', 'method': 'GET' }, + 'isListMember': { + 'url': '/lists/members/show.json', + 'method': 'GET', + }, 'addListMember': { 'url': '/lists/members/create.json', 'method': 'POST', diff --git a/twython/twython.py b/twython/twython.py index a27686b..c3edbca 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -67,15 +67,12 @@ class TwythonError(AttributeError): (twitter_http_status_codes[error_code][0], twitter_http_status_codes[error_code][1], self.msg) - - if error_code == 400: - raise TwythonAPILimit( self.msg , error_code) - + if error_code == 420: raise TwythonRateLimitError(self.msg, error_code, retry_after=retry_after) - + def __str__(self): return repr(self.msg) @@ -105,75 +102,18 @@ class TwythonRateLimitError(TwythonError): 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 - 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, 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 - 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, 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 - 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 - - 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, \ - headers=None, callback_url=None): - """setup(self, oauth_token = None, headers = None) + def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ + headers=None, callback_url=None, twitter_token=None, twitter_secret=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). - - Parameters: - twitter_token - Given to you when you register your application with Twitter. - twitter_secret - Given to you when you register your application with Twitter. - oauth_token - If you've gone through the authentication process and have a token for this user, - pass it in and it'll be used for all requests going forward. - oauth_token_secret - see oauth_token; it's the other half. - headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} - client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} - - ** Note: versioning is not currently used by search.twitter functions; - when Twitter moves their junk, it'll be supported. + :param app_key: (optional) Your applications key + :param app_secret: (optional) Your applications secret key + :param oauth_token: (optional) Used with oauth_secret to make authenticated calls + :param oauth_secret: (optional) Used with oauth_token to make authenticated calls + :param headers: (optional) Custom headers to send along with the request + :param callback_url: (optional) If set, will overwrite the callback url set in your application """ - OAuthHook.consumer_key = twitter_token - OAuthHook.consumer_secret = twitter_secret # Needed for hitting that there API. self.api_url = 'https://api.twitter.com/%s' @@ -182,8 +122,8 @@ class Twython(object): 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 + OAuthHook.consumer_key = self.app_key = app_key or twitter_token + OAuthHook.consumer_secret = self.app_secret = app_secret or twitter_secret self.oauth_token = oauth_token self.oauth_secret = oauth_token_secret self.callback_url = callback_url @@ -195,7 +135,7 @@ class Twython(object): self.client = None - if self.twitter_token is not None and self.twitter_secret is not None: + if self.app_key is not None and self.app_secret is not None: self.client = requests.session(hooks={'pre_request': OAuthHook()}) if self.oauth_token is not None and self.oauth_secret is not None: @@ -341,10 +281,7 @@ class Twython(object): return None def get_authentication_tokens(self): - """ - get_auth_url(self) - - Returns an authorization URL for a user to hit. + """Returns an authorization URL for a user to hit. """ callback_url = self.callback_url @@ -379,10 +316,7 @@ class Twython(object): return request_tokens def get_authorized_tokens(self): - """ - get_authorized_tokens - - Returns authorized tokens after they go through the auth_url phase. + """Returns authorized tokens after they go through the auth_url phase. """ response = self.client.get(self.access_token_url) authorized_tokens = dict(parse_qsl(response.content)) @@ -399,10 +333,7 @@ class Twython(object): @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. + """Shortens url specified by url_to_shorten. Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, but we keep this here for anyone who was previously using it for alternative purposes. ;) @@ -417,7 +348,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 ) + raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code, request.status_code) @staticmethod def constructApiURL(base_url, params): @@ -454,7 +385,7 @@ class Twython(object): def search(self, **kwargs): """ Returns tweets that match a specified query. - Documentation: https://dev.twitter.com/ + Documentation: https://dev.twitter.com/doc/get/search :param q: (required) The query you want to search Twitter for @@ -484,12 +415,12 @@ class Twython(object): if 'q' in kwargs: kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(kwargs['q'])) - return self.get('http://search.twitter.com/search.json', params=kwargs) + return self.get('https://search.twitter.com/search.json', params=kwargs) def searchGen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. - Documentation: https://dev.twitter.com/doc/get/search. + Documentation: https://dev.twitter.com/doc/get/search See Twython.search() for acceptable parameters @@ -498,7 +429,7 @@ class Twython(object): print result """ kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(search_query)) - content = self.get('http://search.twitter.com/search.json', params=kwargs) + content = self.get('https://search.twitter.com/search.json', params=kwargs) if not content['results']: raise StopIteration @@ -519,63 +450,6 @@ class Twython(object): for tweet in self.searchGen(search_query, **kwargs): yield tweet - 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). - - Documentation: https://dev.twitter.com/docs/api/1/get/lists/members/show - - **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. - - 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' - """ - 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, **kwargs): - """ Check if a specified user (username) 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. - - :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) - """ - 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. def updateProfileBackgroundImage(self, file_, tile=True, version=1): @@ -764,19 +638,3 @@ 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 2f80933cb82ed7e43c4562b310003f892cc8735f Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 14 May 2012 11:12:23 -0400 Subject: [PATCH 284/687] Get rid of requests-oauth and a bunch of other schtuff --- setup.py | 2 +- twython/twython.py | 221 ++++++++++++++++++++++++--------------------- 2 files changed, 121 insertions(+), 102 deletions(-) diff --git a/setup.py b/setup.py index 67d01a8..0b4a63d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'oauth2', 'requests', 'requests-oauth'], + install_requires=['simplejson', 'oauth2', 'requests'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index c3edbca..92e1a01 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -16,7 +16,7 @@ import re import time import requests -from oauth_hook import OAuthHook +from requests.auth import OAuth1 import oauth2 as oauth try: @@ -48,7 +48,7 @@ except ImportError: raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") -class TwythonError(AttributeError): +class TwythonError(Exception): """ Generic error class, catch-all for most Twython issues. Special cases are handled by TwythonAPILimit and TwythonAuthError. @@ -122,10 +122,25 @@ class Twython(object): self.authorize_url = self.api_url % 'oauth/authorize' self.authenticate_url = self.api_url % 'oauth/authenticate' - OAuthHook.consumer_key = self.app_key = app_key or twitter_token - OAuthHook.consumer_secret = self.app_secret = app_secret or twitter_secret - self.oauth_token = oauth_token - self.oauth_secret = oauth_token_secret + # Enforce unicode on keys and secrets + self.app_key = None + if app_key is not None or twitter_token is not None: + self.app_key = u'%s' % app_key or twitter_token + + self.app_secret = None + if app_secret is not None or twitter_secret is not None: + self.app_secret = u'%s' % app_secret or twitter_secret + + self.oauth_token = None + if oauth_token is not None: + self.oauth_token = u'%s' % oauth_token + + self.oauth_secret = None + if oauth_token_secret is not None: + self.oauth_secret = u'%s' % oauth_token_secret + + print type(self.app_key), type(self.app_secret), type(self.oauth_token), type(self.oauth_secret) + self.callback_url = callback_url # If there's headers, set them, otherwise be an embarassing parent for their own good. @@ -136,11 +151,13 @@ class Twython(object): self.client = None if self.app_key is not None and self.app_secret is not None: - self.client = requests.session(hooks={'pre_request': OAuthHook()}) + self.auth = OAuth1(self.app_key, self.app_secret, + signature_type='auth_header') if self.oauth_token is not None and self.oauth_secret is not None: - self.oauth_hook = OAuthHook(self.oauth_token, self.oauth_secret, header_auth=True) - self.client = requests.session(hooks={'pre_request': self.oauth_hook}) + self.auth = OAuth1(self.app_key, self.app_secret, + self.oauth_token, self.oauth_secret, + signature_type='auth_header') # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if self.client is None: @@ -174,7 +191,7 @@ class Twython(object): return content - def _request(self, url, method='GET', params=None, api_call=None): + def _request(self, url, method='GET', params=None, files=None, api_call=None): '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' @@ -187,7 +204,7 @@ class Twython(object): myargs = params func = getattr(self.client, method) - response = func(url, data=myargs) + response = func(url, data=myargs, files=files, auth=self.auth) content = response.content.decode('utf-8') # create stash for last function intel @@ -207,6 +224,7 @@ class Twython(object): # `simplejson` will throw simplejson.decoder.JSONDecodeError # But excepting just ValueError will work with both. o.O try: + print content content = simplejson.loads(content) except ValueError: raise TwythonError('Response was not valid JSON, unable to decode.') @@ -232,7 +250,7 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None, version=1): + def request(self, endpoint, method='GET', params=None, files=None, version=1): params = params or {} # In case they want to pass a full Twitter URL @@ -242,7 +260,7 @@ class Twython(object): else: url = '%s/%s.json' % (self.api_url % version, endpoint) - content = self._request(url, method=method, params=params, api_call=url) + content = self._request(url, method=method, params=params, files=files, api_call=url) return content @@ -250,9 +268,9 @@ class Twython(object): params = params or {} return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, version=1): + def post(self, endpoint, params=None, files=None, version=1): params = params or {} - return self.request(endpoint, 'POST', params=params, version=version) + return self.request(endpoint, 'POST', params=params, files=files, version=version) def delete(self, endpoint, params=None, version=1): params = params or {} @@ -261,14 +279,13 @@ class Twython(object): # End Dynamic Request Methods def get_lastfunction_header(self, header): - """ - get_lastfunction_header(self) + """Returns the header in the last function + This must be called after an API call, as it returns header based + information. - returns the header in the last function - this must be called after an API call, as it returns header based information. - this will return None if the header is not present + This will return None if the header is not present - most useful for the following header information: + Most useful for the following header information: x-ratelimit-limit x-ratelimit-remaining x-ratelimit-class @@ -292,7 +309,7 @@ class Twython(object): method = 'get' func = getattr(self.client, method) - response = func(self.request_token_url, data=request_args) + response = func(self.request_token_url, data=request_args, auth=self.auth) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) @@ -318,7 +335,7 @@ class Twython(object): def get_authorized_tokens(self): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url) + response = self.client.get(self.access_token_url, auth=self.auth) authorized_tokens = dict(parse_qsl(response.content)) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') @@ -332,16 +349,19 @@ class Twython(object): # ------------------------------------------------------------------------------------------------------------------------ @staticmethod - def shortenURL(url_to_shorten, shortener="http://is.gd/api.php", query="longurl"): + def shortenURL(url_to_shorten, shortener='http://is.gd/api.php'): """Shortens url specified by url_to_shorten. Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, but we keep this here for anyone who was previously using it for alternative purposes. ;) - Parameters: - url_to_shorten - URL to shorten. - shortener = In case you want to use a url shortening service other than is.gd. + :param url_to_shorten: (required) The URL to shorten + :param shortener: (optional) In case you want to use a different + URL shortening service """ - request = requests.get('http://is.gd/api.php', params={ + if shortener == '': + raise TwythonError('Please provide a URL shortening service.') + + request = requests.get(shortener, params={ 'query': url_to_shorten }) @@ -453,42 +473,41 @@ class Twython(object): # 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) + """Updates the authenticating user's profile background image. - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - 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. + :param file_: (required) A string to the location of the file + (less than 800KB in size, larger than 2048px width will scale down) + :param tile: (optional) Default ``True`` If set to true the background image + will be displayed tiled. The image will not be tiled otherwise. + :param version: (optional) A number, default 1 because that's the + only API version Twitter has now """ - url = 'http://api.twitter.com/%d/account/update_profile_background_image.json' % version + url = 'https://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) + """Updates the authenticating user's profile image (avatar). - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. + :param file_: (required) A string to the location of the file + :param version: (optional) A number, default 1 because that's the + only API version Twitter has now """ - url = 'http://api.twitter.com/%d/account/update_profile_image.json' % version + url = 'https://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): - """ updateStatusWithMedia(filename) + """Updates the users status with media - Updates the authenticating user's profile image (avatar). + :param file_: (required) A string to the location of the file + :param version: (optional) A number, default 1 because that's the + only API version Twitter has now - Parameters: - 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. + **params - You may pass items that are taken in this doc + (https://dev.twitter.com/docs/api/1/post/statuses/update_with_media) """ url = 'https://upload.twitter.com/%d/statuses/update_with_media.json' % version return self._media_update(url, @@ -497,33 +516,7 @@ class Twython(object): 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_consumer_key': self.oauth_hook.consumer_key, - 'oauth_token': self.oauth_token, 'oauth_timestamp': int(time.time()), } @@ -545,8 +538,8 @@ class Twython(object): __delattr__ = dict.__delitem__ consumer = { - 'key': self.oauth_hook.consumer_key, - 'secret': self.oauth_hook.consumer_secret + 'key': self.app_key, + 'secret': self.app_secret } token = { 'key': self.oauth_token, @@ -562,14 +555,16 @@ class Twython(object): return req.content def getProfileImageUrl(self, username, size='normal', version=1): - """ getProfileImageUrl(username) + """Gets the URL for the user's profile image. - Gets the URL for the user's profile image. - - Parameters: - username - Required. User name of the user you want the image url of. - 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. + :param username: (required) Username, self explanatory. + :param size: (optional) Default 'normal' (48px by 48px) + bigger - 73px by 73px + mini - 24px by 24px + original - undefined, be careful -- images may be + large in bytes and/or size. + :param version: A number, default 1 because that's the only API + version Twitter has now """ endpoint = 'users/profile_image/%s' % username url = self.api_url % version + '/' + endpoint + '?' + urllib.urlencode({'size': size}) @@ -580,43 +575,53 @@ class Twython(object): if response.status_code in (301, 302, 303, 307) and image_url is not None: return image_url else: - raise TwythonError('getProfileImageUrl() threw an error.', error_code=response.status_code) + raise TwythonError('getProfileImageUrl() threw an error.', + error_code=response.status_code) @staticmethod def stream(data, callback): - """ - A Streaming API endpoint, because requests (by the lovely Kenneth Reitz) makes this not - stupidly annoying to implement. In reality, Twython does absolutely *nothing special* here, - but people new to programming expect this type of function to exist for this library, so we - provide it for convenience. + """A Streaming API endpoint, because requests (by Kenneth Reitz) + makes this not stupidly annoying to implement. + + In reality, Twython does absolutely *nothing special* here, + but people new to programming expect this type of function to + exist for this library, so we provide it for convenience. Seriously, this is nothing special. :) - For the basic stream you're probably accessing, you'll want to pass the following as data dictionary - keys. If you need to use OAuth (newer streams), passing secrets/etc as keys SHOULD work... + For the basic stream you're probably accessing, you'll want to + pass the following as data dictionary keys. If you need to use + OAuth (newer streams), passing secrets/etc + as keys SHOULD work... - username - Required. User name, self explanatory. - password - Required. The Streaming API doesn't use OAuth, so we do this the old school way. It's all - done over SSL (https://), so you're not left totally vulnerable. - endpoint - Optional. Override the endpoint you're using with the Twitter Streaming API. This is defaulted to the one - that everyone has access to, but if Twitter <3's you feel free to set this to your wildest desires. + This is all done over SSL (https://), so you're not left + totally vulnerable by passing your password. - Parameters: - data - Required. Dictionary of attributes to attach to the request (see: params https://dev.twitter.com/docs/streaming-api/methods) - callback - Required. Callback function to be fired when tweets come in (this is an event-based-ish API). + :param username: (required) Username, self explanatory. + :param password: (required) The Streaming API doesn't use OAuth, + so we do this the old school way. + :param callback: (required) Callback function to be fired when + tweets come in (this is an event-based-ish API). + :param endpoint: (optional) Override the endpoint you're using + with the Twitter Streaming API. This is defaulted + to the one that everyone has access to, but if + Twitter <3's you feel free to set this to your + wildest desires. """ endpoint = 'https://stream.twitter.com/1/statuses/filter.json' if 'endpoint' in data: endpoint = data.pop('endpoint') needs_basic_auth = False - if 'username' in data: + if 'username' in data and 'password' in data: needs_basic_auth = True username = data.pop('username') password = data.pop('password') if needs_basic_auth: - stream = requests.post(endpoint, data=data, auth=(username, password)) + stream = requests.post(endpoint, + data=data, + auth=(username, password)) else: stream = requests.post(endpoint, data=data) @@ -638,3 +643,17 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) + +if __name__ == '__main__': + apk = 'hoLZOOxQAdzzmQEH4KoZ2A' + aps = 'IUgE3lIPVoaacV0O2o8GTYHSyoKdFIsERbBBRNEk' + ot = '142832463-Nlu6m5iBWIus8tTSr5ewoxAdf6AWyxfvYcbeTlaO' + ots = '9PVW2xz2xSeHY8VhVvtV9ph9LHgRQva1KAjKNVg2VpQ' + + t = Twython(app_key=apk, + app_secret=aps, + oauth_token=ot, + oauth_token_secret=ots) + + file_ = '/Users/michaelhelmick/Dropbox/Avatars/avvy1004112.jpg' + print t.updateStatusWithMedia(file_, params={'status':'TESTING STfasdfssfdFF OUTTT !!!'}) From a4e3af1ad46bce70f4d887b97e781bc5e142a3e2 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 14 May 2012 15:16:50 -0400 Subject: [PATCH 285/687] Critical bug fixes --- twython/twython.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 92e1a01..d06eb5b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -125,11 +125,11 @@ class Twython(object): # Enforce unicode on keys and secrets self.app_key = None if app_key is not None or twitter_token is not None: - self.app_key = u'%s' % app_key or twitter_token + self.app_key = u'%s' % (app_key or twitter_token) self.app_secret = None if app_secret is not None or twitter_secret is not None: - self.app_secret = u'%s' % app_secret or twitter_secret + self.app_secret = u'%s' % (app_secret or twitter_secret) self.oauth_token = None if oauth_token is not None: @@ -139,8 +139,6 @@ class Twython(object): if oauth_token_secret is not None: self.oauth_secret = u'%s' % oauth_token_secret - print type(self.app_key), type(self.app_secret), type(self.oauth_token), type(self.oauth_secret) - self.callback_url = callback_url # If there's headers, set them, otherwise be an embarassing parent for their own good. @@ -191,7 +189,7 @@ class Twython(object): return content - def _request(self, url, method='GET', params=None, files=None, api_call=None): + def _request(self, url, method='GET', params=None, api_call=None): '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' @@ -204,7 +202,7 @@ class Twython(object): myargs = params func = getattr(self.client, method) - response = func(url, data=myargs, files=files, auth=self.auth) + response = func(url, data=myargs, auth=self.auth) content = response.content.decode('utf-8') # create stash for last function intel @@ -224,7 +222,6 @@ class Twython(object): # `simplejson` will throw simplejson.decoder.JSONDecodeError # But excepting just ValueError will work with both. o.O try: - print content content = simplejson.loads(content) except ValueError: raise TwythonError('Response was not valid JSON, unable to decode.') @@ -250,7 +247,7 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None, files=None, version=1): + def request(self, endpoint, method='GET', params=None, version=1): params = params or {} # In case they want to pass a full Twitter URL @@ -260,7 +257,7 @@ class Twython(object): else: url = '%s/%s.json' % (self.api_url % version, endpoint) - content = self._request(url, method=method, params=params, files=files, api_call=url) + content = self._request(url, method=method, params=params, api_call=url) return content @@ -268,9 +265,9 @@ class Twython(object): params = params or {} return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, files=None, version=1): + def post(self, endpoint, params=None, version=1): params = params or {} - return self.request(endpoint, 'POST', params=params, files=files, version=version) + return self.request(endpoint, 'POST', params=params, version=version) def delete(self, endpoint, params=None, version=1): params = params or {} @@ -643,17 +640,3 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) - -if __name__ == '__main__': - apk = 'hoLZOOxQAdzzmQEH4KoZ2A' - aps = 'IUgE3lIPVoaacV0O2o8GTYHSyoKdFIsERbBBRNEk' - ot = '142832463-Nlu6m5iBWIus8tTSr5ewoxAdf6AWyxfvYcbeTlaO' - ots = '9PVW2xz2xSeHY8VhVvtV9ph9LHgRQva1KAjKNVg2VpQ' - - t = Twython(app_key=apk, - app_secret=aps, - oauth_token=ot, - oauth_token_secret=ots) - - file_ = '/Users/michaelhelmick/Dropbox/Avatars/avvy1004112.jpg' - print t.updateStatusWithMedia(file_, params={'status':'TESTING STfasdfssfdFF OUTTT !!!'}) From f2cd0d5284f9b032817fe67e7826fe7846ed9c5a Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 11:59:47 -0400 Subject: [PATCH 286/687] 2.1.0 release * README.rst, kind of tried to clean up docs with more examples * No longer need oauth2 lib :thumbsup: --- MANIFEST.in | 2 +- README.rst | 196 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.txt | 136 ------------------------------------ setup.py | 6 +- 4 files changed, 200 insertions(+), 140 deletions(-) create mode 100644 README.rst delete mode 100644 README.txt diff --git a/MANIFEST.in b/MANIFEST.in index 0878b48..948c10c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.markdown README.txt +include LICENSE README.markdown README.rst recursive-include examples * recursive-exclude examples *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9753f59 --- /dev/null +++ b/README.rst @@ -0,0 +1,196 @@ +Twython +======= + +``Twython`` is library providing an easy (and up-to-date) way to access Twitter data in Python + +Features +-------- + +* Query data for: + - User information + - Twitter lists + - Timelines + - User avatar URL + - and anything found in `the docs `_ +* Image Uploading! + - **Update user status with an image** + - Change user avatar + - Change user background image + +Installation +------------ +:: + + pip install twython + +...or, you can clone the repo and install it the old fashioned way. + +:: + + git clone git://github.com/ryanmcgrath/twython.git + cd twython + sudo python setup.py install + + +Usage +----- + +Authorization URL +~~~~~~~~~~~~~~~~~ +:: + + t = Twython(app_key=app_key, + app_secret=app_secret, + callback_url='http://google.com/') + + auth_props = t.get_authentication_tokens() + + oauth_token = auth_props['oauth_token'] + oauth_token_secret = auth_props['oauth_token_secret'] + + print 'Connect to Twitter via: %s' % auth_props['auth_url'] + +Be sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date. + +Handling the callback +~~~~~~~~~~~~~~~~~~~~~ +:: + + ''' + oauth_token and oauth_token_secret come from the previous step + if needed, store those in a session variable or something + ''' + + t = Twython(app_key=app_key, + app_secret=app_secret, + oauth_token=oauth_token, + oauth_token_secret=oauth_token_secret) + + auth_tokens = t.get_authorized_tokens() + print auth_tokens + +*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* + +Getting a user home timeline +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + ''' + oauth_token and oauth_token_secret are the final tokens produced + from the `Handling the callback` step + ''' + + t = Twython(app_key=app_key, + app_secret=app_secret, + oauth_token=oauth_token, + oauth_token_secret=oauth_token_secret) + + # Returns an dict of the user home timeline + print t.getHomeTimeline() + +Get a user avatar url (no authentication needed) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + t = Twython() + print t.getProfileImageUrl('ryanmcgrath', size='bigger') + print t.getProfileImageUrl('mikehelmick') + +Search Twitter (no authentication needed) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + t = Twython() + print t.search(q='python') + +Streaming API +~~~~~~~~~~~~~ +*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.* + +:: + + def on_results(results): + """A callback to handle passed results. Wheeee. + """ + + print results + + Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' + }, on_results) + + +Notes +----- +As of Twython 2.0.0, we have changed routes for functions to abide by the `Twitter Spring 2012 clean up `_ Please make changes to your code accordingly. + + +A note about the development of Twython (specifically, 1.3) +----------------------------------------------------------- +As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored +in a separate Python file, and the class itself catches calls to methods that match up in said table. + +Certain functions require a bit more legwork, and get to stay in the main file, but for the most part +it's all abstracted out. + +As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main +Twython class to import and use. If you need to catch exceptions, import those from twython as well. + +Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that +whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped +as a named argument to any Twitter function. + +For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). + +Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up +from you using them by this library. + +Twython 3k +---------- +There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to +work in all situations, but it's provided so that others can grab it and hack on it. +If you choose to try it out, be aware of this. + +**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab +his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** + +Questions, Comments, etc? +------------------------- +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. + +You can also follow me on Twitter - `@ryanmcgrath `_ + +*Twython is released under an MIT License - see the LICENSE file for more information.* + +Want to help? +------------- +Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! + + +Special Thanks to... +-------------------- +This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's +exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact +me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;)). + +- `Mike Helmick (michaelhelmick) `_, multiple fixes and proper ``requests`` integration. +- `kracekumar `_, early ``requests`` work and various fixes. +- `Erik Scheffers (eriks5) `_, various fixes regarding OAuth callback URLs. +- `Jordan Bouvier (jbouvier) `_, various fixes regarding OAuth callback URLs. +- `Dick Brouwer (dikbrouwer) `_, fixes for OAuth Verifier in ``get_authorized_tokens``. +- `hades `_, Fixes to various initial OAuth issues and updates to ``Twython3k`` to stay current. +- `Alex Sutton (alexdsutton) `_, fix for parameter substitution regular expression (catch underscores!). +- `Levgen Pyvovarov (bsn) `_, Various argument fixes, cyrillic text support. +- `Mark Liu (mliu7) `_, Missing parameter fix for ``addListMember``. +- `Randall Degges (rdegges) `_, PEP-8 fixes, MANIFEST.in, installer fixes. +- `Idris Mokhtarzada (idris) `_, Fixes for various example code pieces. +- `Jonathan Elsas (jelsas) `_, Fix for original Streaming API stub causing import errors. +- `LuqueDaniel `_, Extended example code where necessary. +- `Mesar Hameed (mhameed) `_, Commit to swap ``__getattr__`` trick for a more debuggable solution. +- `Remy DeCausemaker (decause) `_, PEP-8 contributions. +- `[mckellister](https://github.com/mckellister) `_, Fixes to ``Exception`` raised by Twython (Rate Limits, etc). +- `tatz_tsuchiya `_, Fix for ``lambda`` scoping in key injection phase. +- `Voulnet (Mohammed ALDOUB) `_, Fixes for ``http/https`` access endpoints diff --git a/README.txt b/README.txt deleted file mode 100644 index 6efbb77..0000000 --- a/README.txt +++ /dev/null @@ -1,136 +0,0 @@ -Twython - Easy Twitter utilities in Python -========================================================================================= -Ah, Twitter, your API used to be so awesome, before you went and implemented the crap known -as OAuth 1.0. However, since you decided to force your entire development community over a barrel -about it, I suppose Twython has to support this. So, that said... - -Does Twython handle OAuth? -========================================================================================================= -Yes, in a sense. There's a variety of builtin-methods that you can use to handle the authentication ritual. -There's an **[example Django application](https://github.com/ryanmcgrath/twython-django)** that showcases -this - feel free to peruse and use! - -Installation ------------------------------------------------------------------------------------------------------ -Installing Twython is fairly easy. You can... - - (pip install | easy_install) twython - -...or, you can clone the repo and install it the old fashioned way. - - git clone git://github.com/ryanmcgrath/twython.git - cd twython - sudo python setup.py install - -Please note: ------------------------------------------------------------------------------------------------------ -As of Twython 2.0.0, we have changed 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 -from twython import Twython - -twitter = Twython() -results = twitter.search(q = "bert") - -# More function definitions can be found by reading over twython/twitter_endpoints.py, as well -# as skimming the source file. Both are kept human-readable, and are pretty well documented or -# very self documenting. -``` - -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 -Kenneth Reitz. - -``` python -import json -from twython import Twython - -def on_results(results): - """ - A callback to handle passed results. Wheeee. - """ - print json.dumps(results) - -Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' -}, on_results) -``` - -A note about the development of Twython (specifically, 1.3) ----------------------------------------------------------------------------------------------------- -As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored -in a separate Python file, and the class itself catches calls to methods that match up in said table. - -Certain functions require a bit more legwork, and get to stay in the main file, but for the most part -it's all abstracted out. - -As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main -Twython class to import and use. If you need to catch exceptions, import those from twython as well. - -Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that -whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped -as a named argument to any Twitter function. - -For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). - -Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up -from you using them by this library. - -Twython 3k ------------------------------------------------------------------------------------------------------ -There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to -work in all situations, but it's provided so that others can grab it and hack on it. -If you choose to try it out, be aware of this. - -**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab -his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** - -Questions, Comments, etc? ------------------------------------------------------------------------------------------------------ -My hope is that Twython is so simple that you'd never *have* to ask any questions, but if -you feel the need to contact me for this (or other) reasons, you can hit me up -at ryan@venodesigns.net. - -You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. - -Twython is released under an MIT License - see the LICENSE file for more information. - -Want to help? ------------------------------------------------------------------------------------------------------ -Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd -like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help -is always appreciated! - - -Special Thanks to... ------------------------------------------------------------------------------------------------------ -This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's -exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact -me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;)). - -- **[Mike Helmick (michaelhelmick)](https://github.com/michaelhelmick)**, multiple fixes and proper `requests` integration. -- **[kracekumar](https://github.com/kracekumar)**, early `requests` work and various fixes. -- **[Erik Scheffers (eriks5)](https://github.com/eriks5)**, various fixes regarding OAuth callback URLs. -- **[Jordan Bouvier (jbouvier)](https://github.com/jbouvier)**, various fixes regarding OAuth callback URLs. -- **[Dick Brouwer (dikbrouwer)](https://github.com/dikbrouwer)**, fixes for OAuth Verifier in `get_authorized_tokens`. -- **[hades](https://github.com/hades)**, Fixes to various initial OAuth issues and updates to `Twython3k` to stay current. -- **[Alex Sutton (alexdsutton)](https://github.com/alexsdutton/twython/)**, fix for parameter substitution regular expression (catch underscores!). -- **[Levgen Pyvovarov (bsn)](https://github.com/bsn)**, Various argument fixes, cyrillic text support. -- **[Mark Liu (mliu7)](https://github.com/mliu7)**, Missing parameter fix for `addListMember`. -- **[Randall Degges (rdegges)](https://github.com/rdegges)**, PEP-8 fixes, MANIFEST.in, installer fixes. -- **[Idris Mokhtarzada (idris)](https://github.com/idris)**, Fixes for various example code pieces. -- **[Jonathan Elsas (jelsas)](https://github.com/jelsas)**, Fix for original Streaming API stub causing import errors. -- **[LuqueDaniel](https://github.com/LuqueDaniel)**, Extended example code where necessary. -- **[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. -- **[Voulnet (Mohammed ALDOUB)](https://github.com/Voulnet)**, Fixes for `http`/`https` access endpoints diff --git a/setup.py b/setup.py index 0b4a63d..b4786df 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.0.0' +__version__ = '2.1.0' setup( # Basic package information. @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'oauth2', 'requests'], + install_requires=['simplejson', 'requests'], # Metadata for PyPI. author='Ryan McGrath', @@ -25,7 +25,7 @@ setup( url='http://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython', description='An easy (and up to date) way to access Twitter data with Python.', - long_description=open('README.markdown').read(), + long_description=open('README.rst').read(), classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', From 5e817195acd21aa13f7420de0c4e7a41e521a5e5 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 12:09:26 -0400 Subject: [PATCH 287/687] 2.1.0 Release * Removal of oauth2 lib, `requests` has fully taken over. :) * FIXED: Obtaining auth url with specified callback was broke.. wouldn't give you auth url if you specified a callback url * Updated requests to pass the headers that are passed in the init, so User-Agent is once again `Twython Python Twitter Library v2.1.0` :thumbsup: :) * Catching exception when Stream API doesn't return valid JSON to parse * Removed `DELETE` method. As of the Spring 2012 clean up, Twitter no longer supports this method * Updated `post` internal func to take files as kwarg * `params - params or {}` only needs to be done in `_request`, just a lot of redundant code on my part, sorry ;P * Removed `bulkUserLookup`, there is no need for this to be a special case, anyone can pass a string of username or user ids and chances are if they're reading the docs and using this library they'll understand how to use `lookupUser()` in `twitter_endpoints.py` passing params provided in the Twitter docs * Changed internal `oauth_secret` variable to be more consistent with the keyword arg in the init `oauth_token_secret` --- twython/twython.py | 134 ++++++++++----------------------------------- 1 file changed, 30 insertions(+), 104 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index d06eb5b..a6671e4 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,15 +9,13 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.0.0" +__version__ = "2.1.0" import urllib import re -import time import requests from requests.auth import OAuth1 -import oauth2 as oauth try: from urlparse import parse_qsl @@ -109,8 +107,8 @@ class Twython(object): :param app_key: (optional) Your applications key :param app_secret: (optional) Your applications secret key - :param oauth_token: (optional) Used with oauth_secret to make authenticated calls - :param oauth_secret: (optional) Used with oauth_token to make authenticated calls + :param oauth_token: (optional) Used with oauth_token_secret to make authenticated calls + :param oauth_token_secret: (optional) Used with oauth_token to make authenticated calls :param headers: (optional) Custom headers to send along with the request :param callback_url: (optional) If set, will overwrite the callback url set in your application """ @@ -135,9 +133,9 @@ class Twython(object): if oauth_token is not None: self.oauth_token = u'%s' % oauth_token - self.oauth_secret = None + self.oauth_token_secret = None if oauth_token_secret is not None: - self.oauth_secret = u'%s' % oauth_token_secret + self.oauth_token_secret = u'%s' % oauth_token_secret self.callback_url = callback_url @@ -152,9 +150,9 @@ class Twython(object): self.auth = OAuth1(self.app_key, self.app_secret, signature_type='auth_header') - if self.oauth_token is not None and self.oauth_secret is not None: + if self.oauth_token is not None and self.oauth_token_secret is not None: self.auth = OAuth1(self.app_key, self.app_secret, - self.oauth_token, self.oauth_secret, + self.oauth_token, self.oauth_token_secret, signature_type='auth_header') # Filter down through the possibilities here - if they have a token, if they're first stage, etc. @@ -182,27 +180,29 @@ class Twython(object): ) method = fn['method'].lower() - if not method in ('get', 'post', 'delete'): - raise TwythonError('Method must be of GET, POST or DELETE') + if not method in ('get', 'post'): + raise TwythonError('Method must be of GET or POST') content = self._request(url, method=method, params=kwargs) return content - def _request(self, url, method='GET', params=None, api_call=None): + def _request(self, url, method='GET', params=None, files=None, api_call=None): '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' myargs = {} method = method.lower() + params = params or {} + if method == 'get': url = '%s?%s' % (url, urllib.urlencode(params)) else: myargs = params func = getattr(self.client, method) - response = func(url, data=myargs, auth=self.auth) + response = func(url, data=myargs, files=files, headers=self.headers, auth=self.auth) content = response.content.decode('utf-8') # create stash for last function intel @@ -247,31 +247,23 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None, version=1): - params = params or {} - + def request(self, endpoint, method='GET', params=None, files=None, version=1): # In case they want to pass a full Twitter URL - # i.e. http://search.twitter.com/ + # i.e. https://search.twitter.com/ if endpoint.startswith('http://') or endpoint.startswith('https://'): url = endpoint else: url = '%s/%s.json' % (self.api_url % version, endpoint) - content = self._request(url, method=method, params=params, api_call=url) + content = self._request(url, method=method, params=params, files=files, api_call=url) return content def get(self, endpoint, params=None, version=1): - params = params or {} return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, version=1): - params = params or {} - return self.request(endpoint, 'POST', params=params, version=version) - - def delete(self, endpoint, params=None, version=1): - params = params or {} - return self.request(endpoint, 'DELETE', params=params, version=version) + def post(self, endpoint, params=None, files=None, version=1): + return self.request(endpoint, 'POST', params=params, files=files, version=version) # End Dynamic Request Methods @@ -297,16 +289,12 @@ class Twython(object): def get_authentication_tokens(self): """Returns an authorization URL for a user to hit. """ - callback_url = self.callback_url - request_args = {} - if callback_url: - request_args['oauth_callback'] = callback_url + if self.callback_url: + request_args['oauth_callback'] = self.callback_url - method = 'get' - - func = getattr(self.client, method) - response = func(self.request_token_url, data=request_args, auth=self.auth) + req_url = self.request_token_url + '?' + urllib.urlencode(request_args) + response = self.client.get(req_url, headers=self.headers, auth=self.auth) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) @@ -322,8 +310,8 @@ class Twython(object): } # Use old-style callback argument if server didn't accept new-style - if callback_url and not oauth_callback_confirmed: - auth_url_params['oauth_callback'] = callback_url + if self.callback_url and not oauth_callback_confirmed: + auth_url_params['oauth_callback'] = self.callback_url request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) @@ -332,7 +320,7 @@ class Twython(object): def get_authorized_tokens(self): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url, auth=self.auth) + response = self.client.get(self.access_token_url, headers=self.headers, auth=self.auth) authorized_tokens = dict(parse_qsl(response.content)) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') @@ -371,34 +359,6 @@ class Twython(object): 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()]) - def bulkUserLookup(self, ids=None, screen_names=None, version=1, **kwargs): - """ A method to do bulk user lookups against the Twitter API. - - Documentation: https://dev.twitter.com/docs/api/1/get/users/lookup - - :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 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 is not None: - kwargs['screen_name'] = ','.join(screen_names) - - return self.get('users/lookup', params=kwargs, version=version) - def search(self, **kwargs): """ Returns tweets that match a specified query. @@ -512,44 +472,7 @@ class Twython(object): **params) def _media_update(self, url, file_, params=None): - params = params or {} - oauth_params = { - 'oauth_timestamp': int(time.time()), - } - - #create a fake request with your upload url and parameters - faux_req = oauth.Request(method='POST', url=url, parameters=oauth_params) - - #sign the fake request. - signature_method = oauth.SignatureMethod_HMAC_SHA1() - - 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.app_key, - 'secret': self.app_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 - self.headers.update(faux_req.to_header()) - - req = requests.post(url, data=params, files=file_, headers=self.headers) - return req.content + return self.post(url, params=params, files=file_) def getProfileImageUrl(self, username, size='normal', version=1): """Gets the URL for the user's profile image. @@ -624,7 +547,10 @@ class Twython(object): for line in stream.iter_lines(): if line: - callback(simplejson.loads(line)) + try: + callback(simplejson.loads(line)) + except ValueError: + raise TwythonError('Response was not valid JSON, unable to decode.') @staticmethod def unicode2utf8(text): From 32a83a6b79498790c0e401c6f2de988bba3d4c44 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 18:39:31 -0400 Subject: [PATCH 288/687] 2.1.0 Release * .md just look cleaner * Updating documentation to look clean, imo. :P --- MANIFEST.in | 2 +- README.markdown => README.md | 164 +++++++++++++++++++++++------------ README.rst | 16 ++-- 3 files changed, 118 insertions(+), 64 deletions(-) rename README.markdown => README.md (59%) diff --git a/MANIFEST.in b/MANIFEST.in index 948c10c..9d3d7b1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.markdown README.rst +include LICENSE README.md README.rst recursive-include examples * recursive-exclude examples *.pyc diff --git a/README.markdown b/README.md similarity index 59% rename from README.markdown rename to README.md index 6efbb77..66f72d7 100644 --- a/README.markdown +++ b/README.md @@ -1,71 +1,128 @@ -Twython - Easy Twitter utilities in Python -========================================================================================= -Ah, Twitter, your API used to be so awesome, before you went and implemented the crap known -as OAuth 1.0. However, since you decided to force your entire development community over a barrel -about it, I suppose Twython has to support this. So, that said... +Twython +======= -Does Twython handle OAuth? -========================================================================================================= -Yes, in a sense. There's a variety of builtin-methods that you can use to handle the authentication ritual. -There's an **[example Django application](https://github.com/ryanmcgrath/twython-django)** that showcases -this - feel free to peruse and use! +```Twython``` is library providing an easy (and up-to-date) way to access Twitter data in Python + +Features +-------- + +* Query data for: + - User information + - Twitter lists + - Timelines + - User avatar URL + - and anything found in `the docs `_ +* Image Uploading! + - **Update user status with an image** + - Change user avatar + - Change user background image Installation ------------------------------------------------------------------------------------------------------ -Installing Twython is fairly easy. You can... +------------ (pip install | easy_install) twython -...or, you can clone the repo and install it the old fashioned way. +... or, you can clone the repo and install it the old fashioned way git clone git://github.com/ryanmcgrath/twython.git cd twython sudo python setup.py install -Please note: ------------------------------------------------------------------------------------------------------ -As of Twython 2.0.0, we have changed 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. +Usage +----- -Example Use ------------------------------------------------------------------------------------------------------ -``` python -from twython import Twython +Authorization URL -twitter = Twython() -results = twitter.search(q = "bert") +```python +t = Twython(app_key=app_key, + app_secret=app_secret, + callback_url='http://google.com/') -# More function definitions can be found by reading over twython/twitter_endpoints.py, as well -# as skimming the source file. Both are kept human-readable, and are pretty well documented or -# very self documenting. +auth_props = t.get_authentication_tokens() + +oauth_token = auth_props['oauth_token'] +oauth_token_secret = auth_props['oauth_token_secret'] + +print 'Connect to Twitter via: %s' % auth_props['auth_url'] +``` + +Be sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date. + +Handling the callback + +```python +''' +oauth_token and oauth_token_secret come from the previous step +if needed, store those in a session variable or something +''' + +t = Twython(app_key=app_key, + app_secret=app_secret, + oauth_token=oauth_token, + oauth_token_secret=oauth_token_secret) + +auth_tokens = t.get_authorized_tokens() +print auth_tokens +``` + +*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* + +Getting a user home timeline + +```python +''' +oauth_token and oauth_token_secret are the final tokens produced +from the `Handling the callback` step +''' + +t = Twython(app_key=app_key, + app_secret=app_secret, + oauth_token=oauth_token, + oauth_token_secret=oauth_token_secret) + +# Returns an dict of the user home timeline +print t.getHomeTimeline() +``` + +Get a user avatar url *(no authentication needed)* + +```python +t = Twython() +print t.getProfileImageUrl('ryanmcgrath', size='bigger') +print t.getProfileImageUrl('mikehelmick') +``` + +Search Twitter *(no authentication needed)* + +```python +t = Twython() +print t.search(q='python') ``` 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 -Kenneth Reitz. - -``` python -import json -from twython import Twython +*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.* -def on_results(results): - """ - A callback to handle passed results. Wheeee. - """ - print json.dumps(results) +```python +def on_results(results): + """A callback to handle passed results. Wheeee. + """ -Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' -}, on_results) + print results + +Twython.stream({ + 'username': 'your_username', + 'password': 'your_password', + 'track': 'python' +}, on_results) ``` -A note about the development of Twython (specifically, 1.3) ----------------------------------------------------------------------------------------------------- +Notes +----- +As of Twython 2.0.0, we have changed 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. + +Development of Twython (specifically, 1.3) +------------------------------------------ As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored in a separate Python file, and the class itself catches calls to methods that match up in said table. @@ -85,7 +142,7 @@ Doing this allows us to be incredibly flexible in querying the Twitter API, so c from you using them by this library. Twython 3k ------------------------------------------------------------------------------------------------------ +---------- There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to work in all situations, but it's provided so that others can grab it and hack on it. If you choose to try it out, be aware of this. @@ -94,9 +151,8 @@ If you choose to try it out, be aware of this. his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** Questions, Comments, etc? ------------------------------------------------------------------------------------------------------ -My hope is that Twython is so simple that you'd never *have* to ask any questions, but if -you feel the need to contact me for this (or other) reasons, you can hit me up +------------------------- +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. @@ -104,10 +160,8 @@ You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgr Twython is released under an MIT License - see the LICENSE file for more information. Want to help? ------------------------------------------------------------------------------------------------------ -Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd -like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help -is always appreciated! +------------- +Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! Special Thanks to... diff --git a/README.rst b/README.rst index 9753f59..5f26824 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ Installation pip install twython -...or, you can clone the repo and install it the old fashioned way. +... or, you can clone the repo and install it the old fashioned way :: @@ -88,16 +88,16 @@ Getting a user home timeline # Returns an dict of the user home timeline print t.getHomeTimeline() -Get a user avatar url (no authentication needed) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get a user avatar url *(no authentication needed)* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: t = Twython() print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') -Search Twitter (no authentication needed) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Search Twitter *(no authentication needed)* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: t = Twython() @@ -125,11 +125,11 @@ streams.* Notes ----- -As of Twython 2.0.0, we have changed routes for functions to abide by the `Twitter Spring 2012 clean up `_ Please make changes to your code accordingly. +* As of Twython 2.0.0, we have changed routes for functions to abide by the `Twitter Spring 2012 clean up `_ Please make changes to your code accordingly. -A note about the development of Twython (specifically, 1.3) ------------------------------------------------------------ +Development of Twython (specifically, 1.3) +------------------------------------------ As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored in a separate Python file, and the class itself catches calls to methods that match up in said table. From d93b48cded90fa28454e6ec068a3eb56658b2de5 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 17 May 2012 12:22:37 -0400 Subject: [PATCH 289/687] 2.1.0 Release Set `self.auth` = None so that calls (like searching or getting a profile avatar don't error out) Fixes 90 --- twython/twython.py | 1 + 1 file changed, 1 insertion(+) diff --git a/twython/twython.py b/twython/twython.py index a6671e4..ea52292 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -145,6 +145,7 @@ class Twython(object): self.headers = {'User-agent': 'Twython Python Twitter Library v' + __version__} self.client = None + self.auth = None if self.app_key is not None and self.app_secret is not None: self.auth = OAuth1(self.app_key, self.app_secret, From fc9e21435ef77e1d555704832edca17efb25ea02 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 22 May 2012 10:40:38 -0400 Subject: [PATCH 290/687] Auth fixes for search and callback url --- twython/twython.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index d06eb5b..5e05ecc 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -147,6 +147,7 @@ class Twython(object): self.headers = {'User-agent': 'Twython Python Twitter Library v' + __version__} self.client = None + self.auth = None if self.app_key is not None and self.app_secret is not None: self.auth = OAuth1(self.app_key, self.app_secret, @@ -157,9 +158,9 @@ class Twython(object): self.oauth_token, self.oauth_secret, signature_type='auth_header') - # Filter down through the possibilities here - if they have a token, if they're first stage, etc. if self.client is None: - # 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 = requests.session() # register available funcs to allow listing name when debugging. @@ -297,16 +298,12 @@ class Twython(object): def get_authentication_tokens(self): """Returns an authorization URL for a user to hit. """ - callback_url = self.callback_url - request_args = {} - if callback_url: - request_args['oauth_callback'] = callback_url + if self.callback_url: + request_args['oauth_callback'] = self.callback_url - method = 'get' - - func = getattr(self.client, method) - response = func(self.request_token_url, data=request_args, auth=self.auth) + req_url = self.request_token_url + '?' + urllib.urlencode(request_args) + response = self.client.get(req_url, headers=self.headers, auth=self.auth) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) @@ -322,8 +319,8 @@ class Twython(object): } # Use old-style callback argument if server didn't accept new-style - if callback_url and not oauth_callback_confirmed: - auth_url_params['oauth_callback'] = callback_url + if self.callback_url and not oauth_callback_confirmed: + auth_url_params['oauth_callback'] = self.callback_url request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) From 0d00ae97fb53347b2a720b988f2d4f356f307f4d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 23 May 2012 18:49:08 +0900 Subject: [PATCH 291/687] 2.0.1 release, fixes auth error --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0b4a63d..df5aef3 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.0.0' +__version__ = '2.0.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 5e05ecc..92862c7 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.0.0" +__version__ = "2.0.1" import urllib import re From 0b3f36f9b6a73ce12cde372266fe91ed5348dd20 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:03 -0300 Subject: [PATCH 292/687] Need to at least have requests 0.13.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b4786df..812066e 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests'], + install_requires=['simplejson', 'requests>=0.13.0'], # Metadata for PyPI. author='Ryan McGrath', From 072a257a1ffb1d9ee987f0a7c7b93127a22f6ec9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:30 -0300 Subject: [PATCH 293/687] 2.2.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 812066e..ada1d37 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.1.0' +__version__ = '2.2.0' setup( # Basic package information. From f232b873cb7a4caf5cf0061423bedf3b003226fa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:51 -0300 Subject: [PATCH 294/687] 2.2.0 --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index ea52292..a934e22 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.1.0" +__version__ = "2.2.0" import urllib import re From 92f9c941466dc9fa53aa43a3208b8e5115d8bc89 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Thu, 31 May 2012 10:59:43 -0300 Subject: [PATCH 295/687] Corrected when search q gets encoded twice --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 92862c7..e8c84c6 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -427,7 +427,7 @@ class Twython(object): e.g x.search(q='jjndf', page='2') """ if 'q' in kwargs: - kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(kwargs['q'])) + kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(kwargs['q'], ':')) return self.get('https://search.twitter.com/search.json', params=kwargs) From 7caa68814631203cb63231918e42e54eee4d2273 Mon Sep 17 00:00:00 2001 From: fumieval Date: Sun, 3 Jun 2012 18:25:25 +0900 Subject: [PATCH 296/687] Supported proxies, just added an argument to Twython.__init__. --- twython/twython.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 92862c7..2f8609e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -104,7 +104,7 @@ class TwythonRateLimitError(TwythonError): class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ - headers=None, callback_url=None, twitter_token=None, twitter_secret=None): + headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -113,6 +113,7 @@ class Twython(object): :param oauth_secret: (optional) Used with oauth_token to make authenticated calls :param headers: (optional) Custom headers to send along with the request :param callback_url: (optional) If set, will overwrite the callback url set in your application + :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. """ # Needed for hitting that there API. @@ -161,7 +162,8 @@ class Twython(object): if self.client is None: # If they don't do authentication, but still want to request # unprotected resources, we need an opener. - self.client = requests.session() + print proxies + self.client = requests.session(proxies=proxies) # register available funcs to allow listing name when debugging. def setFunc(key): From 3f4e374911cddabe18acfb54c9fcf015733dd44b Mon Sep 17 00:00:00 2001 From: fumieval Date: Sun, 3 Jun 2012 18:43:09 +0900 Subject: [PATCH 297/687] fixed the mistake that prints proxies to console for debugging. --- twython/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 2f8609e..ebc6078 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -162,7 +162,6 @@ class Twython(object): if self.client is None: # If they don't do authentication, but still want to request # unprotected resources, we need an opener. - print proxies self.client = requests.session(proxies=proxies) # register available funcs to allow listing name when debugging. From 1261b7b3045b75ed49fe8f6230f4aee726b5dede Mon Sep 17 00:00:00 2001 From: terrycojones Date: Mon, 11 Jun 2012 15:48:24 -0400 Subject: [PATCH 298/687] Some small suggested clean-ups with error / exception processing. --- twython/twython.py | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 92862c7..5a0d3da 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.0.1" +__version__ = "2.0.2" import urllib import re @@ -68,11 +68,6 @@ class TwythonError(Exception): twitter_http_status_codes[error_code][1], self.msg) - if error_code == 420: - raise TwythonRateLimitError(self.msg, - error_code, - retry_after=retry_after) - def __str__(self): return repr(self.msg) @@ -81,12 +76,7 @@ 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, error_code=None): - self.msg = msg - self.error_code = error_code - - def __str__(self): - return repr(self.msg) + pass class TwythonRateLimitError(TwythonError): @@ -94,12 +84,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): + TwythonError.__init__(self, msg, error_code=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) + self.msg = '%s (Retry after %d seconds)' % (msg, retry_after) class Twython(object): @@ -228,16 +215,19 @@ class Twython(object): raise TwythonError('Response was not valid JSON, unable to decode.') if response.status_code > 304: - # 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'] - + # If there is no error message, use a default. + error_msg = content.get( + 'error', 'An error occurred processing your request.') self._last_call['api_error'] = error_msg - raise TwythonError(error_msg, - error_code=response.status_code, - retry_after=response.headers.get('retry-after')) + if response.status_code == 420: + exceptionType = TwythonRateLimitError + else: + exceptionType = TwythonError + + raise exceptionType(error_msg, + error_code=response.status_code, + retry_after=response.headers.get('retry-after')) return content @@ -362,7 +352,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) + raise TwythonError('shortenURL() failed with a %s error code.' % request.status_code) @staticmethod def constructApiURL(base_url, params): From 8ea61af4fc30e83e4d239ed3911a600cbb438fa4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 25 Jun 2012 05:08:16 +0900 Subject: [PATCH 299/687] Documentation showcasing proper importing; kinda sorta needed. --- README.md | 12 ++++++++++++ README.rst | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 20ae8b5..d19f34f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Usage Authorization URL ```python +from twython import Twython + t = Twython(app_key=app_key, app_secret=app_secret, callback_url='http://google.com/') @@ -51,6 +53,8 @@ Be sure you have a URL set up to handle the callback after the user has allowed Handling the callback ```python +from twython import Twython + ''' oauth_token and oauth_token_secret come from the previous step if needed, store those in a session variable or something @@ -70,6 +74,8 @@ print auth_tokens Getting a user home timeline ```python +from twython import Twython + ''' oauth_token and oauth_token_secret are the final tokens produced from the `Handling the callback` step @@ -87,6 +93,8 @@ print t.getHomeTimeline() Get a user avatar url *(no authentication needed)* ```python +from twython import Twython + t = Twython() print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') @@ -95,6 +103,8 @@ print t.getProfileImageUrl('mikehelmick') Search Twitter *(no authentication needed)* ```python +from twython import Twython + t = Twython() print t.search(q='python') ``` @@ -104,6 +114,8 @@ Streaming API streams.* ```python +from twython import Twython + def on_results(results): """A callback to handle passed results. Wheeee. """ diff --git a/README.rst b/README.rst index 2025567..100dc27 100644 --- a/README.rst +++ b/README.rst @@ -37,7 +37,8 @@ Usage Authorization URL ~~~~~~~~~~~~~~~~~ :: - + from twython import Twython + t = Twython(app_key=app_key, app_secret=app_secret, callback_url='http://google.com/') @@ -59,6 +60,7 @@ Handling the callback oauth_token and oauth_token_secret come from the previous step if needed, store those in a session variable or something ''' + from twython import Twython t = Twython(app_key=app_key, app_secret=app_secret, @@ -78,19 +80,22 @@ Getting a user home timeline oauth_token and oauth_token_secret are the final tokens produced from the `Handling the callback` step ''' - + from twython import Twython + t = Twython(app_key=app_key, app_secret=app_secret, oauth_token=oauth_token, oauth_token_secret=oauth_token_secret) - + # Returns an dict of the user home timeline print t.getHomeTimeline() Get a user avatar url *(no authentication needed)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - + + from twython import Twython + t = Twython() print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') @@ -98,7 +103,8 @@ Get a user avatar url *(no authentication needed)* Search Twitter *(no authentication needed)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - + + from twython import Twython t = Twython() print t.search(q='python') @@ -109,6 +115,8 @@ streams.* :: + from twython import Twython + def on_results(results): """A callback to handle passed results. Wheeee. """ From 2155ae0c2344a70d06c8082ff6aef8fd4082d439 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 25 Jun 2012 11:50:44 -0400 Subject: [PATCH 300/687] Fix error in README.md, strip some not-needed comments and fixed a ternary --- README.md | 2 +- twython/twython.py | 43 +++++++++++++------------------------------ 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index d19f34f..8bfe7b5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Features - Twitter lists - Timelines - User avatar URL - - and anything found in `the docs `_ + - and anything found in [the docs](https://dev.twitter.com/docs/api) * Image Uploading! - **Update user status with an image** - Change user avatar diff --git a/twython/twython.py b/twython/twython.py index e5d268c..0d6bb01 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -27,12 +27,7 @@ except ImportError: # table is a file with a dictionary of every API endpoint that Twython supports. from twitter_endpoints import base_url, api_table, twitter_http_status_codes - -# There are some special setups (like, oh, a Django application) where -# simplejson exists behind the scenes anyway. Past Python 2.6, this should -# never really cause any problems to begin with. try: - # If they have the library, chances are they're gonna want to use that. import simplejson except ImportError: try: @@ -40,7 +35,6 @@ except ImportError: 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. from django.utils import simplejson except: # Seriously wtf is wrong with you if you get this Exception. @@ -110,21 +104,11 @@ class Twython(object): self.authenticate_url = self.api_url % 'oauth/authenticate' # Enforce unicode on keys and secrets - self.app_key = None - if app_key is not None or twitter_token is not None: - self.app_key = u'%s' % (app_key or twitter_token) + self.app_key = app_key and unicode(app_key) or twitter_token and unicode(twitter_token) + self.app_secret = app_key and unicode(app_secret) or twitter_secret and unicode(twitter_secret) - self.app_secret = None - if app_secret is not None or twitter_secret is not None: - self.app_secret = u'%s' % (app_secret or twitter_secret) - - self.oauth_token = None - if oauth_token is not None: - self.oauth_token = u'%s' % oauth_token - - self.oauth_token_secret = None - if oauth_token_secret is not None: - self.oauth_token_secret = u'%s' % oauth_token_secret + self.oauth_token = oauth_token and u'%s' % oauth_token + self.oauth_token_secret = oauth_token_secret and u'%s' % oauth_token_secret self.callback_url = callback_url @@ -146,8 +130,7 @@ class Twython(object): signature_type='auth_header') if self.client is None: - # If they don't do authentication, but still want to request - # unprotected resources, we need an opener. + # Allow unauthenticated requests to be made. self.client = requests.session(proxies=proxies) # register available funcs to allow listing name when debugging. @@ -181,11 +164,16 @@ class Twython(object): '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' - myargs = {} method = method.lower() + if not method in ('get', 'post'): + raise TwythonError('Method must be of GET or POST') params = params or {} + # In the next release of requests after 0.13.1, we can get rid of this + # myargs variable and line 184, urlencoding the params and just + # pass params=params in the func() + myargs = {} if method == 'get': url = '%s?%s' % (url, urllib.urlencode(params)) else: @@ -207,10 +195,6 @@ class Twython(object): 'content': content, } - # 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(content) except ValueError: @@ -436,7 +420,7 @@ class Twython(object): return self._media_update(url, {'image': (file_, open(file_, 'rb'))}, params={'tile': tile}) - + def bulkUserLookup(self, **kwargs): """Stub for a method that has been deprecated, kept for now to raise errors properly if people are relying on this (which they are...). @@ -446,7 +430,7 @@ class Twython(object): DeprecationWarning, stacklevel=2 ) - + def updateProfileImage(self, file_, version=1): """Updates the authenticating user's profile image (avatar). @@ -458,7 +442,6 @@ class Twython(object): return self._media_update(url, {'image': (file_, open(file_, 'rb'))}) - # statuses/update_with_media def updateStatusWithMedia(self, file_, version=1, **params): """Updates the users status with media From dfdbbec5a8a4c288bf3f7e118280c7a85ca829bc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 25 Jun 2012 11:54:33 -0400 Subject: [PATCH 301/687] Add H6's to README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8bfe7b5..dd8be37 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Installation Usage ----- -Authorization URL +###### Authorization URL ```python from twython import Twython @@ -50,7 +50,7 @@ print 'Connect to Twitter via: %s' % auth_props['auth_url'] Be sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date. -Handling the callback +###### Handling the callback ```python from twython import Twython @@ -71,7 +71,7 @@ print auth_tokens *Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* -Getting a user home timeline +###### Getting a user home timeline ```python from twython import Twython @@ -90,7 +90,7 @@ t = Twython(app_key=app_key, print t.getHomeTimeline() ``` -Get a user avatar url *(no authentication needed)* +###### Get a user avatar url *(no authentication needed)* ```python from twython import Twython @@ -100,7 +100,7 @@ print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') ``` -Search Twitter *(no authentication needed)* +###### Search Twitter *(no authentication needed)* ```python from twython import Twython @@ -109,7 +109,7 @@ t = Twython() print t.search(q='python') ``` -Streaming API +###### Streaming API *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.* From b552913e53bd6710009a022d17f4aabcebe31e41 Mon Sep 17 00:00:00 2001 From: Mike Grouchy Date: Wed, 27 Jun 2012 09:53:35 -0400 Subject: [PATCH 302/687] Fixed broken params kwargs which was breaking updateStatusWithMedia * params are passed as **kwargs everywhere else, so updated _media_update to be consistent with that. * updated to updateProfileBackgroundImage to fall in line with _media_update changes. --- twython/twython.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index e5d268c..6a7fb99 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -435,8 +435,8 @@ class Twython(object): url = 'https://api.twitter.com/%d/account/update_profile_background_image.json' % version return self._media_update(url, {'image': (file_, open(file_, 'rb'))}, - params={'tile': tile}) - + **{'tile': tile}) + def bulkUserLookup(self, **kwargs): """Stub for a method that has been deprecated, kept for now to raise errors properly if people are relying on this (which they are...). @@ -446,7 +446,7 @@ class Twython(object): DeprecationWarning, stacklevel=2 ) - + def updateProfileImage(self, file_, version=1): """Updates the authenticating user's profile image (avatar). @@ -474,7 +474,7 @@ class Twython(object): {'media': (file_, open(file_, 'rb'))}, **params) - def _media_update(self, url, file_, params=None): + def _media_update(self, url, file_, **params): return self.post(url, params=params, files=file_) def getProfileImageUrl(self, username, size='normal', version=1): From a9b7b836c985a963ddbdf89522d5222cd74f0db1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 28 Jun 2012 11:08:59 -0400 Subject: [PATCH 303/687] Fixes #103 Fix #93 is incorrect. We can avoid this by just removing the check and quote_plus --- twython/twython.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 0d6bb01..4d5822c 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -366,8 +366,6 @@ 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('https://search.twitter.com/search.json', params=kwargs) From f4b2ebc40a9c1e8b91ce84c25378091e51bb5828 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 28 Jun 2012 11:13:49 -0400 Subject: [PATCH 304/687] This func no longer needs to urlencode the query, _request does it --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 4d5822c..3405c2f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -380,7 +380,7 @@ class Twython(object): for result in search: print result """ - kwargs['q'] = urllib.quote_plus(Twython.unicode2utf8(search_query)) + kwargs['q'] = search_query content = self.get('https://search.twitter.com/search.json', params=kwargs) if not content['results']: From 73a1910066893fa7b0dc3951d8afde593520c6c5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 30 Jun 2012 00:52:40 +0900 Subject: [PATCH 305/687] Version bump for 2.3.1 release --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d4f529b..c228df1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.3.0' +__version__ = '2.3.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 6a7fb99..a6db900 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.3.0" +__version__ = "2.3.1" import urllib import re From d86437681679462a6d84e069a20fdad3bb1b76f3 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 29 Jun 2012 12:19:37 -0400 Subject: [PATCH 306/687] Code cleanup, Update requests version * No sense in setting self.auth twice * Make self.client a requests.session to reuse headers and auth * requests 0.13.2 dependency isn't needed, but doesn't hurt --- setup.py | 2 +- twython/twython.py | 44 ++++++++++++++++++-------------------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index d4f529b..8f4f883 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.13.0'], + install_requires=['simplejson', 'requests>=0.13.2'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index 3405c2f..b74938e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -113,25 +113,25 @@ class Twython(object): self.callback_url = callback_url # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers - if self.headers is None: - self.headers = {'User-agent': 'Twython Python Twitter Library v' + __version__} + self.headers = headers or {'User-Agent': 'Twython v' + __version__} - self.client = None + # Allow for unauthenticated requests + self.client = requests.session(proxies=proxies) self.auth = None - if self.app_key is not None and self.app_secret is not None: + if self.app_key is not None and self.app_secret is not None and \ + self.oauth_token is None and self.oauth_token_secret is None: self.auth = OAuth1(self.app_key, self.app_secret, signature_type='auth_header') - if self.oauth_token is not None and self.oauth_token_secret is not None: + if self.app_key is not None and self.app_secret is not None and \ + self.oauth_token is not None and self.oauth_token_secret is not None: self.auth = OAuth1(self.app_key, self.app_secret, self.oauth_token, self.oauth_token_secret, signature_type='auth_header') - if self.client is None: - # Allow unauthenticated requests to be made. - self.client = requests.session(proxies=proxies) + if self.auth is not None: + self.client = requests.session(headers=self.headers, auth=self.auth, proxies=proxies) # register available funcs to allow listing name when debugging. def setFunc(key): @@ -152,11 +152,7 @@ class Twython(object): base_url + fn['url'] ) - method = fn['method'].lower() - if not method in ('get', 'post'): - raise TwythonError('Method must be of GET or POST') - - content = self._request(url, method=method, params=kwargs) + content = self._request(url, method=fn['method'], params=kwargs) return content @@ -170,17 +166,13 @@ class Twython(object): params = params or {} - # In the next release of requests after 0.13.1, we can get rid of this - # myargs variable and line 184, urlencoding the params and just - # pass params=params in the func() - myargs = {} - if method == 'get': - url = '%s?%s' % (url, urllib.urlencode(params)) - else: - myargs = params - func = getattr(self.client, method) - response = func(url, data=myargs, files=files, headers=self.headers, auth=self.auth) + if method == 'get': + # Still wasn't fixed in `requests` 0.13.2? :( + url = url + '?' + urllib.urlencode(params) + response = func(url) + else: + response = func(url, data=params, files=files) content = response.content.decode('utf-8') # create stash for last function intel @@ -271,7 +263,7 @@ class Twython(object): request_args['oauth_callback'] = self.callback_url req_url = self.request_token_url + '?' + urllib.urlencode(request_args) - response = self.client.get(req_url, headers=self.headers, auth=self.auth) + response = self.client.get(req_url) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) @@ -297,7 +289,7 @@ class Twython(object): def get_authorized_tokens(self): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url, headers=self.headers, auth=self.auth) + 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.') From 7986da859f5ca42c4f1388c10e38097b8cdfe276 Mon Sep 17 00:00:00 2001 From: Mohmmadhd Date: Tue, 10 Jul 2012 15:22:31 +0300 Subject: [PATCH 307/687] Update master --- twython/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/twython/__init__.py b/twython/__init__.py index 59aac86..8de237b 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1 +1,2 @@ from twython import Twython +from twython import TwythonError, TwythonAPILimit, TwythonAuthError From c5468ee1b5c6a0c554cecd9eb309b5b081e8fa4f Mon Sep 17 00:00:00 2001 From: lucadex Date: Tue, 24 Jul 2012 12:45:32 +0300 Subject: [PATCH 308/687] Update twython/__init__.py --- twython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/__init__.py b/twython/__init__.py index 8de237b..26a3860 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1,2 +1,2 @@ from twython import Twython -from twython import TwythonError, TwythonAPILimit, TwythonAuthError +from twython import TwythonError, TwythonRateLimitError, TwythonAuthError From ee940288548e3d1c5a6a0882be9b3b07bf2ab43a Mon Sep 17 00:00:00 2001 From: lucadex Date: Tue, 24 Jul 2012 12:46:16 +0300 Subject: [PATCH 309/687] Update core_examples/search_results.py --- core_examples/search_results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core_examples/search_results.py b/core_examples/search_results.py index 74cfd40..57b4c51 100644 --- a/core_examples/search_results.py +++ b/core_examples/search_results.py @@ -2,7 +2,7 @@ from twython import Twython """ Instantiate Twython with no Authentication """ twitter = Twython() -search_results = twitter.searchTwitter(q="WebsDotCom", rpp="50") +search_results = twitter.search(q="WebsDotCom", rpp="50") for tweet in search_results["results"]: print "Tweet from @%s Date: %s" % (tweet['from_user'].encode('utf-8'),tweet['created_at']) From e1c4035a637f737e602c6c05ba1d30d998e0264f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 25 Jul 2012 03:56:12 +0900 Subject: [PATCH 310/687] Version bump for bug-fix rollout --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index fd1a9a1..223c02b 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.3.2' +__version__ = '2.3.3' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 40857cf..a1185d2 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.3.2" +__version__ = "2.3.3" import urllib import re From 9e5a96655db1313feda400993a45f3b46d07ba29 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 27 Jul 2012 12:10:36 -0400 Subject: [PATCH 311/687] 2.3.4 release, requires requests 0.13.4 >, basically we don't have to url encode params anymore and can just pass a dict of params to the request (finally! :D) --- setup.py | 4 ++-- twython/twython.py | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 223c02b..a952e58 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.3.3' +__version__ = '2.3.4' setup( # Basic package information. @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.13.2'], + install_requires=['simplejson', 'requests>=0.13.4'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index a1185d2..2c8d627 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.3.3" +__version__ = "2.3.4" import urllib import re @@ -168,9 +168,7 @@ class Twython(object): func = getattr(self.client, method) if method == 'get': - # Still wasn't fixed in `requests` 0.13.2? :( - url = url + '?' + urllib.urlencode(params) - response = func(url) + response = func(url, params=params) else: response = func(url, data=params, files=files) content = response.content.decode('utf-8') @@ -262,8 +260,7 @@ class Twython(object): if self.callback_url: request_args['oauth_callback'] = self.callback_url - req_url = self.request_token_url + '?' + urllib.urlencode(request_args) - response = self.client.get(req_url) + response = self.client.get(self.request_token_url, params=request_args) if response.status_code != 200: raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) @@ -463,9 +460,9 @@ class Twython(object): version Twitter has now """ endpoint = 'users/profile_image/%s' % username - url = self.api_url % version + '/' + endpoint + '?' + urllib.urlencode({'size': size}) + url = self.api_url % version + '/' + endpoint - response = self.client.get(url, allow_redirects=False) + response = self.client.get(url, params={'size': size}, allow_redirects=False) image_url = response.headers.get('location') if response.status_code in (301, 302, 303, 307) and image_url is not None: From 9d21865409c47fdf7c1109445ae89b0cd4f65815 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Wed, 1 Aug 2012 13:32:06 -0400 Subject: [PATCH 312/687] adjusted the logic in the twitter response reader to better handle API errors. this could likely be done a bit cleaner -- but this works. --- twython/twython.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index a1185d2..aa1126a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -175,6 +175,9 @@ class Twython(object): response = func(url, data=params, files=files) content = response.content.decode('utf-8') + print response + print response.__dict__ + # create stash for last function intel self._last_call = { 'api_call': api_call, @@ -187,10 +190,15 @@ class Twython(object): 'content': content, } + + # wrap the json loads in a try, and defer an error + # why? twitter will return invalid json with an error code in the headers + json_error = False try: content = simplejson.loads(content) except ValueError: - raise TwythonError('Response was not valid JSON, unable to decode.') + json_error= True + content= {} if response.status_code > 304: # If there is no error message, use a default. @@ -207,6 +215,10 @@ class Twython(object): error_code=response.status_code, retry_after=response.headers.get('retry-after')) + # if we have a json error here , then it's not an official TwitterAPI error + if json_error: + raise TwythonError('Response was not valid JSON, unable to decode.') + return content ''' From 341fdd8f49242d6dd4ac273135243f5e27be6289 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Wed, 1 Aug 2012 13:53:44 -0400 Subject: [PATCH 313/687] removed `print`s. dumb me --- twython/twython.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index aa1126a..7ced06f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -175,9 +175,6 @@ class Twython(object): response = func(url, data=params, files=files) content = response.content.decode('utf-8') - print response - print response.__dict__ - # create stash for last function intel self._last_call = { 'api_call': api_call, From 156368cf6e00d7cf15bd5c4fe3d3b53f8954df5f Mon Sep 17 00:00:00 2001 From: Denis Veselov Date: Tue, 28 Aug 2012 00:48:32 +0400 Subject: [PATCH 314/687] add myTotals endpoint for py2k --- twython/twitter_endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index c553c77..aeb9941 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -159,6 +159,10 @@ api_table = { 'url': '/account/update_profile_colors.json', 'method': 'POST', }, + 'myTotals': { + 'url' : '/account/totals.json', + 'method': 'GET', + }, # Favorites methods 'getFavorites': { From a2e95d40efabc53d292a256e2fe81d54b98a7734 Mon Sep 17 00:00:00 2001 From: Denis Veselov Date: Tue, 28 Aug 2012 00:49:09 +0400 Subject: [PATCH 315/687] add myTotals endpoint for py3k --- twython3k/twitter_endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython3k/twitter_endpoints.py b/twython3k/twitter_endpoints.py index 030d110..1c90aa8 100644 --- a/twython3k/twitter_endpoints.py +++ b/twython3k/twitter_endpoints.py @@ -175,6 +175,10 @@ api_table = { 'url': '/account/update_profile_colors.json', 'method': 'POST', }, + 'myTotals': { + 'url' : '/account/totals.json', + 'method': 'GET', + }, # Favorites methods 'getFavorites': { From 8e72adead2c7fead5b07063f20f0536ecdb523cd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 31 Aug 2012 14:22:48 -0400 Subject: [PATCH 316/687] Version bump since new endpoint added, update requests version --- setup.py | 4 ++-- twython/twython.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a952e58..0e1adb7 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.3.4' +__version__ = '2.4.0' setup( # Basic package information. @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.13.4'], + install_requires=['simplejson', 'requests>=0.13.9'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index 3adb6a6..3fa2b5d 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.3.4" +__version__ = "2.4.0" import urllib import re From 0c2565192180e2e93d56d4f10b582a5c08dbb77f Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 13 Sep 2012 22:38:05 -0500 Subject: [PATCH 317/687] Use version 1.1 for everything, especially search --- twython/twython.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 3fa2b5d..55c231d 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -223,7 +223,7 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None, files=None, version=1): + def request(self, endpoint, method='GET', params=None, files=None, version='1.1'): # In case they want to pass a full Twitter URL # i.e. https://search.twitter.com/ if endpoint.startswith('http://') or endpoint.startswith('https://'): @@ -235,10 +235,10 @@ class Twython(object): return content - def get(self, endpoint, params=None, version=1): + def get(self, endpoint, params=None, version='1.1'): return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, files=None, version=1): + def post(self, endpoint, params=None, files=None, version='1.1'): return self.request(endpoint, 'POST', params=params, files=files, version=version) # End Dynamic Request Methods @@ -365,7 +365,7 @@ class Twython(object): e.g x.search(q='jjndf', page='2') """ - return self.get('https://search.twitter.com/search.json', params=kwargs) + return self.get('https://api.twitter.com/1.1/search/tweets.json', params=kwargs) def searchGen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. @@ -379,7 +379,7 @@ class Twython(object): print result """ kwargs['q'] = search_query - content = self.get('https://search.twitter.com/search.json', params=kwargs) + content = self.get('https://api.twitter.com/1.1/search/tweets.json', params=kwargs) if not content['results']: raise StopIteration @@ -402,7 +402,7 @@ class Twython(object): # 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): + def updateProfileBackgroundImage(self, file_, tile=True, version='1.1'): """Updates the authenticating user's profile background image. :param file_: (required) A string to the location of the file @@ -427,7 +427,7 @@ class Twython(object): stacklevel=2 ) - def updateProfileImage(self, file_, version=1): + def updateProfileImage(self, file_, version='1.1'): """Updates the authenticating user's profile image (avatar). :param file_: (required) A string to the location of the file @@ -438,7 +438,7 @@ class Twython(object): return self._media_update(url, {'image': (file_, open(file_, 'rb'))}) - def updateStatusWithMedia(self, file_, version=1, **params): + def updateStatusWithMedia(self, file_, version='1.1', **params): """Updates the users status with media :param file_: (required) A string to the location of the file @@ -456,7 +456,7 @@ class Twython(object): def _media_update(self, url, file_, **params): return self.post(url, params=params, files=file_) - def getProfileImageUrl(self, username, size='normal', version=1): + def getProfileImageUrl(self, username, size='normal', version='1.1'): """Gets the URL for the user's profile image. :param username: (required) Username, self explanatory. From 448b4f27b61d4d6336b3875bdddeb5d387eacaa5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 14:08:46 -0400 Subject: [PATCH 318/687] Update requests version and Twython version --- setup.py | 4 ++-- twython/twython.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a952e58..eeb5832 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.3.4' +__version__ = '2.4.0' setup( # Basic package information. @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.13.4'], + install_requires=['simplejson', 'requests>=0.14.1'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index 3adb6a6..3fa2b5d 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.3.4" +__version__ = "2.4.0" import urllib import re From cc31322102e0c2942713b8a98c1977644116d41d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 14:10:56 -0400 Subject: [PATCH 319/687] Fixes #119 --- core_examples/public_timeline.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 core_examples/public_timeline.py diff --git a/core_examples/public_timeline.py b/core_examples/public_timeline.py deleted file mode 100644 index da28e7e..0000000 --- a/core_examples/public_timeline.py +++ /dev/null @@ -1,8 +0,0 @@ -from twython import Twython - -# Getting the public timeline requires no authentication, huzzah -twitter = Twython() -public_timeline = twitter.getPublicTimeline() - -for tweet in public_timeline: - print tweet["text"] From a3967390e10f8a5422cda6cee1d605701c3e24b5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 16:53:28 -0400 Subject: [PATCH 320/687] Moved around some code, support for removing and updating Profile Banners Line 213 needs to check for status code as well now because remove/updating banner does not return content, only status code --- twython/twitter_endpoints.py | 6 +++- twython/twython.py | 66 ++++++++++++++++++++++++++---------- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index aeb9941..9bc6806 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -160,9 +160,13 @@ api_table = { 'method': 'POST', }, 'myTotals': { - 'url' : '/account/totals.json', + 'url': '/account/totals.json', 'method': 'GET', }, + 'removeProfileBanner': { + 'url': '/account/remove_profile_banner.json', + 'method': 'POST', + }, # Favorites methods 'getFavorites': { diff --git a/twython/twython.py b/twython/twython.py index 3fa2b5d..f16ab68 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -185,15 +185,14 @@ class Twython(object): 'content': content, } - # wrap the json loads in a try, and defer an error # why? twitter will return invalid json with an error code in the headers json_error = False try: content = simplejson.loads(content) except ValueError: - json_error= True - content= {} + json_error = True + content = {} if response.status_code > 304: # If there is no error message, use a default. @@ -210,8 +209,8 @@ class Twython(object): error_code=response.status_code, retry_after=response.headers.get('retry-after')) - # if we have a json error here , then it's not an official TwitterAPI error - if json_error: + # if we have a json error here, then it's not an official TwitterAPI error + if json_error and not response.status_code in (200, 201, 202): raise TwythonError('Response was not valid JSON, unable to decode.') return content @@ -400,8 +399,24 @@ class Twython(object): for tweet in self.searchGen(search_query, **kwargs): yield tweet + def bulkUserLookup(self, **kwargs): + """Stub for a method that has been deprecated, kept for now to raise errors + properly if people are relying on this (which they are...). + """ + warnings.warn( + "This function has been deprecated. Please migrate to .lookupUser() - params should be the same.", + DeprecationWarning, + stacklevel=2 + ) + # The following methods are apart from the other Account methods, # because they rely on a whole multipart-data posting function set. + + ## Media Uploading functions ############################################## + + def _media_update(self, url, file_, **params): + return self.post(url, params=params, files=file_) + def updateProfileBackgroundImage(self, file_, tile=True, version=1): """Updates the authenticating user's profile background image. @@ -417,16 +432,6 @@ class Twython(object): {'image': (file_, open(file_, 'rb'))}, **{'tile': tile}) - def bulkUserLookup(self, **kwargs): - """Stub for a method that has been deprecated, kept for now to raise errors - properly if people are relying on this (which they are...). - """ - warnings.warn( - "This function has been deprecated. Please migrate to .lookupUser() - params should be the same.", - DeprecationWarning, - stacklevel=2 - ) - def updateProfileImage(self, file_, version=1): """Updates the authenticating user's profile image (avatar). @@ -453,8 +458,22 @@ class Twython(object): {'media': (file_, open(file_, 'rb'))}, **params) - def _media_update(self, url, file_, **params): - return self.post(url, params=params, files=file_) + def updateProfileBannerImage(self, file_, version=1, **params): + """Updates the users profile banner + + :param file_: (required) A string to the location of the file + :param version: (optional) A number, default 1 because that's the + only API version Twitter has now + + **params - You may pass items that are taken in this doc + (https://dev.twitter.com/docs/api/1/post/account/update_profile_banner) + """ + url = 'https://api.twitter.com/%d/account/update_profile_banner.json' % version + return self._media_update(url, + {'banner': (file_, open(file_, 'rb'))}, + **params) + + ########################################################################### def getProfileImageUrl(self, username, size='normal', version=1): """Gets the URL for the user's profile image. @@ -548,3 +567,16 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) + +if __name__ == '__main__': + app_key = 'KEkNAu84zC5brBehnhAz9g' + app_secret = 'Z0KOh2Oyf1yDVnQAvRemIslaZfeDfaG79TJ4JoJHXbk' + oauth_token = '142832463-U6l4WX5pnxSY9wdDWE0Ahzz03yYuhiUvsIjBAyOH' + oauth_token_secret = 'PJBXfanIZ89hLLDI7ylNDvWyqALVxBMOBELhLW0A' + + t = Twython(app_key=app_key, + app_secret=app_secret, + oauth_token=oauth_token, + oauth_token_secret=oauth_token_secret) + + print t.updateProfileBannerImage('/Users/mikehelmick/Desktop/Stuff/Screen Shot 2012-08-15 at 2.54.58 PM.png') From b314a5606e2d464ee187a2c7306c4f63aa5d71ca Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 16:56:20 -0400 Subject: [PATCH 321/687] Deleting auth stuff, oops --- twython/twython.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index f16ab68..535ecec 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -567,16 +567,3 @@ class Twython(object): if isinstance(text, (str, unicode)): return Twython.unicode2utf8(text) return str(text) - -if __name__ == '__main__': - app_key = 'KEkNAu84zC5brBehnhAz9g' - app_secret = 'Z0KOh2Oyf1yDVnQAvRemIslaZfeDfaG79TJ4JoJHXbk' - oauth_token = '142832463-U6l4WX5pnxSY9wdDWE0Ahzz03yYuhiUvsIjBAyOH' - oauth_token_secret = 'PJBXfanIZ89hLLDI7ylNDvWyqALVxBMOBELhLW0A' - - t = Twython(app_key=app_key, - app_secret=app_secret, - oauth_token=oauth_token, - oauth_token_secret=oauth_token_secret) - - print t.updateProfileBannerImage('/Users/mikehelmick/Desktop/Stuff/Screen Shot 2012-08-15 at 2.54.58 PM.png') From 5a516c2bfbbea3a1474bde98dc466d152eca8143 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 10 Oct 2012 12:55:35 -0400 Subject: [PATCH 322/687] Version bump --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0e1adb7..ca03758 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.4.0' +__version__ = '2.5.0' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 3fa2b5d..d8c50b9 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,7 +9,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.4.0" +__version__ = "2.5.0" import urllib import re From 4e86da4aec63acb581d02eb3046ae1a098485356 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Oct 2012 12:36:45 -0400 Subject: [PATCH 323/687] Posts with files and params works with requests 0.14.0 #122 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ca03758..c9177e3 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.13.9'], + install_requires=['simplejson', 'requests>=0.14.0'], # Metadata for PyPI. author='Ryan McGrath', From 98e213df9c8f6c46ca4dda6093a98bc958086caa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Oct 2012 12:37:16 -0400 Subject: [PATCH 324/687] Fixes #121 --- twython/twython.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index d8c50b9..9ba5370 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -165,6 +165,11 @@ class Twython(object): raise TwythonError('Method must be of GET or POST') params = params or {} + # requests doesn't like items that can't be converted to unicode, + # so let's be nice and do that for the user + for k, v in params.items(): + if isinstance(v, (int, bool)): + params[k] = u'%s' % v func = getattr(self.client, method) if method == 'get': @@ -185,15 +190,14 @@ class Twython(object): 'content': content, } - # wrap the json loads in a try, and defer an error # why? twitter will return invalid json with an error code in the headers json_error = False try: content = simplejson.loads(content) except ValueError: - json_error= True - content= {} + json_error = True + content = {} if response.status_code > 304: # If there is no error message, use a default. From 909f1919b14c19b5255aa5c159ad32dbd9750699 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 9 Nov 2012 04:57:56 -0500 Subject: [PATCH 325/687] Added @chbrown to list of known contributors --- README.md | 1 + README.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index dd8be37..31c3cc7 100644 --- a/README.md +++ b/README.md @@ -203,3 +203,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - **[fumieval](https://github.com/fumieval)**, Re-added Proxy support for 2.3.0. - **[terrycojones](https://github.com/terrycojones)**, Error cleanup and Exception processing in 2.3.0. - **[Leandro Ferreira](https://github.com/leandroferreira)**, Fix for double-encoding of search queries in 2.3.0. +- **[Chris Brown](https://github.com/chbrown)**, Updated to use v1.1 endpoints over v1 diff --git a/README.rst b/README.rst index 100dc27..fc834ec 100644 --- a/README.rst +++ b/README.rst @@ -208,3 +208,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - `fumieval `_, Re-added Proxy support for 2.3.0. - `terrycojones `_, Error cleanup and Exception processing in 2.3.0. - `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. +- `Chris Brown `_, Updated to use v1.1 endpoints over v1 From b8084905d3fd681087062ee9576efc9094591ed5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 10:30:37 -0500 Subject: [PATCH 326/687] requests==0.14.0 requirement Requests needs to be on 0.14.0, otherwise calls with params and files will not work. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5839e46..4a10bf1 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=0.14.0'], + install_requires=['simplejson', 'requests==0.14.0'], # Metadata for PyPI. author='Ryan McGrath', From d52ce03de43c6bd1e96952b2da4df7a32c7de8c8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 10:50:32 -0500 Subject: [PATCH 327/687] Version number bump, no need for env python line --- twython/twython.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 5bf25fb..a97909b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - """ Twython is a library for Python that wraps the Twitter API. It aims to abstract away all the API endpoints, so that additions to the library @@ -9,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.0" +__version__ = "2.5.1" import urllib import re From be494d4c77c9fa305673a926b765f91d44b17c21 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 11:14:36 -0500 Subject: [PATCH 328/687] Fixing up some urls, cleaning up code * Cleaned up exceptionType into ternary * getProfileImage is only supported in Twitter API v1 * Updated other media update methods to use 1.1 and pass dynamic params --- twython/twython.py | 50 ++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 5bf25fb..b9bd28a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -205,10 +205,7 @@ class Twython(object): 'error', 'An error occurred processing your request.') self._last_call['api_error'] = error_msg - if response.status_code == 420: - exceptionType = TwythonRateLimitError - else: - exceptionType = TwythonError + exceptionType = TwythonRateLimitError if response.status_code == 420 else TwythonError raise exceptionType(error_msg, error_code=response.status_code, @@ -422,43 +419,48 @@ class Twython(object): def _media_update(self, url, file_, **params): return self.post(url, params=params, files=file_) - def updateProfileBackgroundImage(self, file_, tile=True, version=1): + def updateProfileBackgroundImage(self, file_, version='1.1', **params): """Updates the authenticating user's profile background image. :param file_: (required) A string to the location of the file (less than 800KB in size, larger than 2048px width will scale down) - :param tile: (optional) Default ``True`` If set to true the background image - will be displayed tiled. The image will not be tiled otherwise. - :param version: (optional) A number, default 1 because that's the - only API version Twitter has now + :param version: (optional) A number, default 1.1 because that's the + current API version for Twitter (Legacy = 1) + + **params - You may pass items that are stated in this doc + (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image) """ - url = 'https://api.twitter.com/%d/account/update_profile_background_image.json' % version + url = 'https://api.twitter.com/%s/account/update_profile_background_image.json' % version return self._media_update(url, {'image': (file_, open(file_, 'rb'))}, - **{'tile': tile}) + **params) - def updateProfileImage(self, file_, version=1): + def updateProfileImage(self, file_, version='1.1', **params): """Updates the authenticating user's profile image (avatar). :param file_: (required) A string to the location of the file - :param version: (optional) A number, default 1 because that's the - only API version Twitter has now + :param version: (optional) A number, default 1.1 because that's the + current API version for Twitter (Legacy = 1) + + **params - You may pass items that are stated in this doc + (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image) """ - url = 'https://api.twitter.com/%d/account/update_profile_image.json' % version + url = 'https://api.twitter.com/%s/account/update_profile_image.json' % version return self._media_update(url, - {'image': (file_, open(file_, 'rb'))}) + {'image': (file_, open(file_, 'rb'))}, + **params) def updateStatusWithMedia(self, file_, version='1.1', **params): """Updates the users status with media :param file_: (required) A string to the location of the file - :param version: (optional) A number, default 1 because that's the - only API version Twitter has now + :param version: (optional) A number, default 1.1 because that's the + current API version for Twitter (Legacy = 1) **params - You may pass items that are taken in this doc - (https://dev.twitter.com/docs/api/1/post/statuses/update_with_media) + (https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media) """ - url = 'https://upload.twitter.com/%d/statuses/update_with_media.json' % version + url = 'https://upload.twitter.com/%s/statuses/update_with_media.json' % version return self._media_update(url, {'media': (file_, open(file_, 'rb'))}, **params) @@ -468,7 +470,7 @@ class Twython(object): :param file_: (required) A string to the location of the file :param version: (optional) A number, default 1 because that's the - only API version Twitter has now + only API version for Twitter that supports this call **params - You may pass items that are taken in this doc (https://dev.twitter.com/docs/api/1/post/account/update_profile_banner) @@ -480,7 +482,7 @@ class Twython(object): ########################################################################### - def getProfileImageUrl(self, username, size='normal', version='1.1'): + def getProfileImageUrl(self, username, size='normal', version='1'): """Gets the URL for the user's profile image. :param username: (required) Username, self explanatory. @@ -489,8 +491,8 @@ class Twython(object): mini - 24px by 24px original - undefined, be careful -- images may be large in bytes and/or size. - :param version: A number, default 1 because that's the only API - version Twitter has now + :param version: (optional) A number, default 1 because that's the + only API version for Twitter that supports this call """ endpoint = 'users/profile_image/%s' % username url = self.api_url % version + '/' + endpoint From a125ad6048ee4938e64d6915c2f12b825b79121e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 10 Nov 2012 20:16:34 -0500 Subject: [PATCH 329/687] Version bump --- setup.py | 4 +--- twython-django | 2 +- twython/twython.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 4a10bf1..f6c8066 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,8 @@ -#!/usr/bin/env python - from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.5.1' +__version__ = '2.5.2' setup( # Basic package information. diff --git a/twython-django b/twython-django index e9b3190..3ffebcc 160000 --- a/twython-django +++ b/twython-django @@ -1 +1 @@ -Subproject commit e9b31903727af8e38c4e2f047b8f9e6c9aa9a38f +Subproject commit 3ffebcc57f57ad5db1d0ba8f940f2bab02f671a5 diff --git a/twython/twython.py b/twython/twython.py index 1f6917e..ab89a94 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.1" +__version__ = "2.5.2" import urllib import re From 80282d9aa72f307a36c76e7a1e48ddab9ce348fe Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 14 Nov 2012 15:13:16 -0500 Subject: [PATCH 330/687] UpdateStatusWithMedia url accounting for API v1 Fixes #130 --- twython/twython.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index ab89a94..4431a67 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -458,7 +458,8 @@ class Twython(object): **params - You may pass items that are taken in this doc (https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media) """ - url = 'https://upload.twitter.com/%s/statuses/update_with_media.json' % version + subdomain = 'upload' if version == '1' else 'api' + url = 'https://%s.twitter.com/%s/statuses/update_with_media.json' % (subdomain, version) return self._media_update(url, {'media': (file_, open(file_, 'rb'))}, **params) From 7d8220661440054a87bfe82f39ebc87eb8082833 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 29 Nov 2012 15:32:07 -0500 Subject: [PATCH 331/687] Support for Friends and Followers list endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Two new methods in API v1.1 provide simplified access to user friend & follower data: https://dev.twitter.com/docs/api/1.1/get/followers/list … https://dev.twitter.com/docs/api/1.1/get/friends/list … ^TS" - @TwitterAPI --- twython/twitter_endpoints.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 9bc6806..c470e59 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -62,6 +62,14 @@ api_table = { 'url': '/followers/ids.json', 'method': 'GET', }, + 'getFriendsList': { + 'url': '/friends/list.json', + 'method': 'GET', + }, + 'getFollowersList': { + 'url': '/followers/list.json', + 'method': 'GET', + }, 'getIncomingFriendshipIDs': { 'url': '/friendships/incoming.json', 'method': 'GET', From 34e9474b91af875e14553b781f253b98619ea765 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 1 Dec 2012 01:34:38 -0500 Subject: [PATCH 332/687] Version bump --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f6c8066..462858e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.5.2' +__version__ = '2.5.3' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 4431a67..17dd39f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.2" +__version__ = "2.5.3" import urllib import re From d83cc32b3dd5a9bbb9fff546a73e6626e7f1b2bd Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 6 Dec 2012 16:51:34 -0500 Subject: [PATCH 333/687] Added ability to grab oembed html given a tweet id. --- twython/twitter_endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index c470e59..be0c718 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -339,6 +339,10 @@ api_table = { 'url': '/report_spam.json', 'method': 'POST', }, + 'getOembedTweet': { + 'url': '/statuses/oembed.json', + 'method': 'GET', + }, } # from https://dev.twitter.com/docs/error-codes-responses From 2d3d5b5b680905b73f6bdf717268defce056044d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 14 Dec 2012 07:13:28 -0500 Subject: [PATCH 334/687] Bump to 2.5.4 --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 462858e..9d6f429 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.5.3' +__version__ = '2.5.4' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 17dd39f..0a57d92 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.3" +__version__ = "2.5.4" import urllib import re From fe3fcbdb04e2096cc2b4146383ad8f55dfd437c2 Mon Sep 17 00:00:00 2001 From: Ajay Nadathur Date: Sun, 30 Dec 2012 23:06:36 +0000 Subject: [PATCH 335/687] moved api version into __init__ method and added method to delete multiple users from a list in batch mode --- twython/twitter_endpoints.py | 4 ++++ twython/twython.py | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index be0c718..ca9a34f 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -309,6 +309,10 @@ api_table = { 'url': '/lists/members/destroy.json', 'method': 'POST', }, + 'deleteListMembers': { + 'url': '/lists/members/destroy_all.json', + 'method': 'POST' + }, 'getListSubscribers': { 'url': '/lists/subscribers.json', 'method': 'GET', diff --git a/twython/twython.py b/twython/twython.py index 0a57d92..816ed81 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -82,7 +82,7 @@ class TwythonRateLimitError(TwythonError): class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ - headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None): + headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -95,6 +95,7 @@ class Twython(object): """ # Needed for hitting that there API. + self.api_version = version 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' @@ -145,8 +146,7 @@ class Twython(object): fn = api_table[api_call] url = re.sub( '\{\{(?P[a-zA-Z_]+)\}\}', - # The '1' here catches the API version. Slightly hilarious. - lambda m: "%s" % kwargs.get(m.group(1), '1'), + lambda m: "%s" % kwargs.get(m.group(1), self.api_version), base_url + fn['url'] ) From 87a3b44a306d4c3c55ee745ba0c7fe866567cecd Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 1 Jan 2013 20:47:02 -0500 Subject: [PATCH 336/687] Bump to 2.5.5 --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9d6f429..7b4cb5b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.5.4' +__version__ = '2.5.5' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 816ed81..49c2f65 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.4" +__version__ = "2.5.5" import urllib import re From 6baa4fdd1482e7abf2dd28e8357137a80cd23b96 Mon Sep 17 00:00:00 2001 From: Ryan Merl Date: Thu, 10 Jan 2013 03:41:22 -0500 Subject: [PATCH 337/687] Updated rate limit status API Endpoint to the v1.1 endpoint --- twython/twitter_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index ca9a34f..6ba82c3 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -17,7 +17,7 @@ base_url = 'http://api.twitter.com/{{version}}' api_table = { 'getRateLimitStatus': { - 'url': '/account/rate_limit_status.json', + 'url': '/application/rate_limit_status.json', 'method': 'GET', }, From 7f0751c27eb97fd4e888c6e5b4447af2b12bfbc5 Mon Sep 17 00:00:00 2001 From: Guru Devanla Date: Tue, 15 Jan 2013 16:40:57 -0600 Subject: [PATCH 338/687] Update error code for Twitter Rate Limits. This is for Twitter Api 1.1 --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 49c2f65..2e2edb9 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -203,7 +203,7 @@ class Twython(object): 'error', 'An error occurred processing your request.') self._last_call['api_error'] = error_msg - exceptionType = TwythonRateLimitError if response.status_code == 420 else TwythonError + exceptionType = TwythonRateLimitError if response.status_code == 429 else TwythonError raise exceptionType(error_msg, error_code=response.status_code, From a28febb0b4283be0d3d024bd9bd3709898d4a14c Mon Sep 17 00:00:00 2001 From: Guru Devanla Date: Tue, 15 Jan 2013 16:43:50 -0600 Subject: [PATCH 339/687] update error code and comment --- twython/twython.py | 1 + 1 file changed, 1 insertion(+) diff --git a/twython/twython.py b/twython/twython.py index 2e2edb9..631b34e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -203,6 +203,7 @@ class Twython(object): 'error', 'An error occurred processing your request.') self._last_call['api_error'] = error_msg + #Twitter API 1.1 , always return 429 when rate limit is exceeded exceptionType = TwythonRateLimitError if response.status_code == 429 else TwythonError raise exceptionType(error_msg, From f0db93c59ea29b2d6e9ef626ed535c4ab1c0e2d9 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 30 Jan 2013 01:00:32 +1100 Subject: [PATCH 340/687] updated for requests==1.1.0 --- setup.py | 2 +- twython/twython.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 7b4cb5b..ac4d637 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests==0.14.0'], + install_requires=['simplejson', 'requests==1.1.0', 'requests_oauthlib==0.3.0'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index 49c2f65..ce6931f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -14,7 +14,7 @@ import re import warnings import requests -from requests.auth import OAuth1 +from requests_oauthlib import OAuth1 try: from urlparse import parse_qsl @@ -115,7 +115,8 @@ class Twython(object): self.headers = headers or {'User-Agent': 'Twython v' + __version__} # Allow for unauthenticated requests - self.client = requests.session(proxies=proxies) + self.client = requests.Session() + self.client.proxies = proxies self.auth = None if self.app_key is not None and self.app_secret is not None and \ @@ -130,7 +131,10 @@ class Twython(object): signature_type='auth_header') if self.auth is not None: - self.client = requests.session(headers=self.headers, auth=self.auth, proxies=proxies) + self.client = requests.Session() + self.client.headers = self.headers + self.client.auth = self.auth + self.client.proxies = proxies # register available funcs to allow listing name when debugging. def setFunc(key): @@ -181,7 +185,6 @@ class Twython(object): 'api_call': api_call, 'api_error': None, 'cookies': response.cookies, - 'error': response.error, 'headers': response.headers, 'status_code': response.status_code, 'url': response.url, From dbf2a461b84e62b9736bcb0fbeed3ad4edcc532c Mon Sep 17 00:00:00 2001 From: Mahdi Yusuf Date: Tue, 5 Feb 2013 16:18:56 -0500 Subject: [PATCH 341/687] Update twython/twython.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit making it so that when you don't pass in header you get all of them back.  --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 49c2f65..372d259 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -259,7 +259,7 @@ class Twython(object): raise TwythonError('This function must be called after an API call. It delivers header information.') if header in self._last_call['headers']: return self._last_call['headers'][header] - return None + return self._last_call def get_authentication_tokens(self): """Returns an authorization URL for a user to hit. From b0f5af37d5a813c2e1740160a9a4643e3b146531 Mon Sep 17 00:00:00 2001 From: Mahdi Yusuf Date: Tue, 5 Feb 2013 16:53:47 -0500 Subject: [PATCH 342/687] Update twython/twython.py updating default version of api as well. come on playa. --- twython/twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index 372d259..9a81ce4 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -82,7 +82,7 @@ class TwythonRateLimitError(TwythonError): class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ - headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1'): + headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key From 9bda75b5200790bb2c68e256207d8fc5d45a76c6 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Thu, 7 Feb 2013 22:20:35 +1100 Subject: [PATCH 343/687] Allow versions of requests between 1.0.0 and 2.0.0 Requests is semantically versioned, so minor version changes are expected to be compatible. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ac4d637..3583fec 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests==1.1.0', 'requests_oauthlib==0.3.0'], + install_requires=['simplejson', 'requests>=1.0.0, <2.0.0', 'requests_oauthlib==0.3.0'], # Metadata for PyPI. author='Ryan McGrath', From a172136f3edf9c6b085d8a350788767da0dd098b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 19 Mar 2013 15:40:40 -0400 Subject: [PATCH 344/687] Update Twitter Endpoints & Internal Functions * Twitter Endpoints are now in the order of https://dev.twitter.com/docs/api/1.1 * No need to repeat search function internally when it is available via `twitter_endpoints.py` * Make `searchGen` use self.search, instead of self.get with the full search url --- README.md | 9 - README.rst | 8 - setup.py | 2 +- twython/twitter_endpoints.py | 496 +++++++++++++++++++---------------- twython/twython.py | 37 +-- 5 files changed, 270 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index 31c3cc7..c3fcca5 100644 --- a/README.md +++ b/README.md @@ -100,15 +100,6 @@ print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') ``` -###### Search Twitter *(no authentication needed)* - -```python -from twython import Twython - -t = Twython() -print t.search(q='python') -``` - ###### Streaming API *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.* diff --git a/README.rst b/README.rst index fc834ec..1bb6677 100644 --- a/README.rst +++ b/README.rst @@ -100,14 +100,6 @@ Get a user avatar url *(no authentication needed)* print t.getProfileImageUrl('ryanmcgrath', size='bigger') print t.getProfileImageUrl('mikehelmick') -Search Twitter *(no authentication needed)* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:: - - from twython import Twython - t = Twython() - print t.search(q='python') - Streaming API ~~~~~~~~~~~~~ *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) diff --git a/setup.py b/setup.py index 3583fec..94d4006 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.5.5' +__version__ = '2.6.0' setup( # Basic package information. diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 6ba82c3..03c418a 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -10,50 +10,97 @@ 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). + + This map is organized the order functions are documented at: + https://dev.twitter.com/docs/api/1.1 """ # Base Twitter API url, no need to repeat this junk... base_url = 'http://api.twitter.com/{{version}}' api_table = { - 'getRateLimitStatus': { - 'url': '/application/rate_limit_status.json', - 'method': 'GET', - }, - - 'verifyCredentials': { - 'url': '/account/verify_credentials.json', - 'method': 'GET', - }, - - 'endSession': { - 'url': '/account/end_session.json', - 'method': 'POST', - }, - - # Timeline methods - 'getHomeTimeline': { - 'url': '/statuses/home_timeline.json', + # Timelines + 'getMentionsTimeline': { + 'url': 'statuses/mentions_timeline', 'method': 'GET', }, 'getUserTimeline': { 'url': '/statuses/user_timeline.json', 'method': 'GET', }, - - # Interfacing with friends/followers - 'getUserMentions': { - 'url': '/statuses/mentions.json', + 'getHomeTimeline': { + 'url': '/statuses/home_timeline.json', 'method': 'GET', }, - 'createFriendship': { - 'url': '/friendships/create.json', + 'retweetedOfMe': { + 'url': '/statuses/retweets_of_me.json', + 'method': 'GET', + }, + + + # Tweets + 'getRetweets': { + 'url': '/statuses/retweets/{{id}}.json', + 'method': 'GET', + }, + 'showStatus': { + 'url': '/statuses/show/{{id}}.json', + 'method': 'GET', + }, + 'destroyStatus': { + 'url': '/statuses/destroy/{{id}}.json', 'method': 'POST', }, - 'destroyFriendship': { - 'url': '/friendships/destroy.json', + 'updateStatus': { + 'url': '/statuses/update.json', 'method': 'POST', }, + 'retweet': { + 'url': '/statuses/retweet/{{id}}.json', + 'method': 'POST', + }, + # See twython.py for update_status_with_media + 'getOembedTweet': { + 'url': '/statuses/oembed.json', + 'method': 'GET', + }, + + + # Search + 'search': { + 'url': '/search/tweets.json', + 'method': 'GET', + }, + + + # Direct Messages + 'getDirectMessages': { + 'url': '/direct_messages.json', + 'method': 'GET', + }, + 'getSentMessages': { + 'url': '/direct_messages/sent.json', + 'method': 'GET', + }, + 'getDirectMessage': { + 'url': '/direct_messages/show.json', + 'method': 'GET', + }, + 'destroyDirectMessage': { + 'url': '/direct_messages/destroy/{{id}}.json', + 'method': 'POST', + }, + 'sendDirectMessage': { + 'url': '/direct_messages/new.json', + 'method': 'POST', + }, + + + # Friends & Followers + 'getUserIdsOfBlockedRetweets': { + 'url': '/friendships/no_retweets/ids.json', + 'method': 'GET', + }, 'getFriendsIDs': { 'url': '/friends/ids.json', 'method': 'GET', @@ -62,12 +109,8 @@ api_table = { 'url': '/followers/ids.json', 'method': 'GET', }, - 'getFriendsList': { - 'url': '/friends/list.json', - 'method': 'GET', - }, - 'getFollowersList': { - 'url': '/followers/list.json', + 'lookupFriendships': { + 'url': '/friendships/lookup.json', 'method': 'GET', }, 'getIncomingFriendshipIDs': { @@ -78,119 +121,67 @@ api_table = { 'url': '/friendships/outgoing.json', 'method': 'GET', }, - - # Retweets - 'reTweet': { - 'url': '/statuses/retweet/{{id}}.json', + 'createFriendship': { + 'url': '/friendships/create.json', 'method': 'POST', }, - 'getRetweets': { - 'url': '/statuses/retweets/{{id}}.json', - 'method': 'GET', - }, - 'retweetedOfMe': { - 'url': '/statuses/retweets_of_me.json', - 'method': 'GET', - }, - 'retweetedByMe': { - 'url': '/statuses/retweeted_by_me.json', - 'method': 'GET', - }, - 'retweetedToMe': { - 'url': '/statuses/retweeted_to_me.json', - 'method': 'GET', - }, - - # User methods - 'showUser': { - 'url': '/users/show.json', - 'method': 'GET', - }, - 'searchUsers': { - 'url': '/users/search.json', - 'method': 'GET', - }, - - 'lookupUser': { - 'url': '/users/lookup.json', - 'method': 'GET', - }, - - # Status methods - showing, updating, destroying, etc. - 'showStatus': { - 'url': '/statuses/show.json', - 'method': 'GET', - }, - 'updateStatus': { - 'url': '/statuses/update.json', + 'destroyFriendship': { + 'url': '/friendships/destroy.json', 'method': 'POST', }, - 'destroyStatus': { - 'url': '/statuses/destroy/{{id}}.json', + 'updateFriendship': { + 'url': '/friendships/update.json', 'method': 'POST', }, - - # Direct Messages - getting, sending, effing, etc. - 'getDirectMessages': { - 'url': '/direct_messages.json', - 'method': 'GET', - }, - 'getSentMessages': { - 'url': '/direct_messages/sent.json', - 'method': 'GET', - }, - 'sendDirectMessage': { - 'url': '/direct_messages/new.json', - 'method': 'POST', - }, - 'destroyDirectMessage': { - 'url': '/direct_messages/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Friendship methods - 'checkIfFriendshipExists': { - 'url': '/friendships/exists.json', - 'method': 'GET', - }, 'showFriendship': { 'url': '/friendships/show.json', 'method': 'GET', }, + 'getFriendsList': { + 'url': '/friends/list.json', + 'method': 'GET', + }, + 'getFollowersList': { + 'url': '/followers/list.json', + 'method': 'GET', + }, - # Profile methods + + # Users + 'getAccountSettings': { + 'url': '/account/settings.json', + 'method': 'GET', + }, + 'verifyCredentials': { + 'url': '/account/verify_credentials.json', + 'method': 'GET', + }, + 'updateAccountSettings': { + 'url': '/account/settings.json', + 'method': 'POST', + }, + 'updateDeliveryService': { + 'url': '/account/update_delivery_device.json', + 'method': 'POST', + }, 'updateProfile': { 'url': '/account/update_profile.json', 'method': 'POST', }, + # See twython.py for update_profile_background_image 'updateProfileColors': { 'url': '/account/update_profile_colors.json', 'method': 'POST', }, - 'myTotals': { - 'url': '/account/totals.json', + # See twython.py for update_profile_image + 'listBlocks': { + 'url': '/blocks/list.json', 'method': 'GET', }, - 'removeProfileBanner': { - 'url': '/account/remove_profile_banner.json', - 'method': 'POST', - }, - - # Favorites methods - 'getFavorites': { - 'url': '/favorites.json', + 'listBlockIds': { + 'url': '/blocks/ids.json', 'method': 'GET', }, - 'createFavorite': { - 'url': '/favorites/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyFavorite': { - 'url': '/favorites/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Blocking methods 'createBlock': { 'url': '/blocks/create/{{id}}.json', 'method': 'POST', @@ -199,119 +190,79 @@ api_table = { 'url': '/blocks/destroy/{{id}}.json', 'method': 'POST', }, - 'getBlocking': { - 'url': '/blocks/blocking.json', + 'lookupUser': { + 'url': '/users/lookup.json', 'method': 'GET', }, - 'getBlockedIDs': { - 'url': '/blocks/blocking/ids.json', + 'showUser': { + 'url': '/users/show.json', 'method': 'GET', }, - 'checkIfBlockExists': { - 'url': '/blocks/exists.json', + 'searchUsers': { + 'url': '/users/search.json', 'method': 'GET', }, - - # Trending methods - 'getCurrentTrends': { - 'url': '/trends/current.json', + 'getContributees': { + 'url': '/users/contributees.json', 'method': 'GET', }, - 'getDailyTrends': { - 'url': '/trends/daily.json', + 'getContributors': { + 'url': '/users/contributors.json', 'method': 'GET', }, - 'getWeeklyTrends': { - 'url': '/trends/weekly.json', - 'method': 'GET', - }, - 'availableTrends': { - 'url': '/trends/available.json', - 'method': 'GET', - }, - 'trendsByLocation': { - 'url': '/trends/{{woeid}}.json', - 'method': 'GET', - }, - - # Saved Searches - 'getSavedSearches': { - 'url': '/saved_searches.json', - 'method': 'GET', - }, - 'showSavedSearch': { - 'url': '/saved_searches/show/{{id}}.json', - 'method': 'GET', - }, - 'createSavedSearch': { - 'url': '/saved_searches/create.json', - 'method': 'GET', - }, - 'destroySavedSearch': { - 'url': '/saved_searches/destroy/{{id}}.json', - 'method': 'GET', - }, - - # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P - 'createList': { - 'url': '/lists/create.json', + 'removeProfileBanner': { + 'url': '/account/remove_profile_banner.json', 'method': 'POST', }, - 'updateList': { - 'url': '/lists/update.json', + # See twython.py for update_profile_banner + + + # Suggested Users + 'getUserSuggestionsBySlug': { + 'url': '/users/suggestions/{{slug}}.json', + 'method': 'GET', + }, + 'getUserSuggestions': { + 'url': '/users/suggestions.json', + 'method': 'GET', + }, + 'getUserSuggestionsStatusesBySlug': { + 'url': '/users/suggestions/{{slug}}/members.json', + 'method': 'GET', + }, + + + # Favorites + 'getFavorites': { + 'url': '/favorites/list.json', + 'method': 'GET', + }, + 'destroyFavorite': { + 'url': '/favorites/destroy.json', 'method': 'POST', }, + 'createFavorite': { + 'url': '/favorites/create.json', + 'method': 'POST', + }, + + + # Lists 'showLists': { - 'url': '/lists.json', - 'method': 'GET', - }, - 'getListMemberships': { - 'url': '/lists/memberships.json', - 'method': 'GET', - }, - 'getListSubscriptions': { - 'url': '/lists/subscriptions.json', - 'method': 'GET', - }, - 'isListSubscriber': { - 'url': '/lists/subscribers/show.json', - 'method': 'GET', - }, - 'deleteList': { - 'url': '/lists/destroy.json', - 'method': 'POST', - }, - 'getListTimeline': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'getSpecificList': { - 'url': '/lists/show.json', + 'url': '/lists/list.json', 'method': 'GET', }, 'getListStatuses': { 'url': '/lists/statuses.json', 'method': 'GET' }, - 'isListMember': { - 'url': '/lists/members/show.json', - 'method': 'GET', - }, - 'addListMember': { - 'url': '/lists/members/create.json', - 'method': 'POST', - }, - 'getListMembers': { - 'url': '/lists/members.json', - 'method': 'GET', - }, 'deleteListMember': { 'url': '/lists/members/destroy.json', 'method': 'POST', }, - 'deleteListMembers': { - 'url': '/lists/members/destroy_all.json', - 'method': 'POST' + 'getListMemberships': { + 'url': '/lists/memberships.json', + 'method': 'GET', }, 'getListSubscribers': { 'url': '/lists/subscribers.json', @@ -321,34 +272,121 @@ api_table = { 'url': '/lists/subscribers/create.json', 'method': 'POST', }, + 'isListSubscriber': { + 'url': '/lists/subscribers/show.json', + 'method': 'GET', + }, 'unsubscribeFromList': { 'url': '/lists/subscribers/destroy.json', 'method': 'POST', }, - - # The one-offs - 'notificationFollow': { - 'url': '/notifications/follow/follow.json', - 'method': 'POST', + 'createListMembers': { + 'url': '/lists/members/create_all.json', + 'method': 'POST' }, - 'notificationLeave': { - 'url': '/notifications/leave/leave.json', - 'method': 'POST', - }, - 'updateDeliveryService': { - 'url': '/account/update_delivery_device.json', - 'method': 'POST', - }, - 'reportSpam': { - 'url': '/report_spam.json', - 'method': 'POST', - }, - 'getOembedTweet': { - 'url': '/statuses/oembed.json', + 'isListMember': { + 'url': '/lists/members/show.json', 'method': 'GET', }, + 'getListMembers': { + 'url': '/lists/members.json', + 'method': 'GET', + }, + 'addListMember': { + 'url': '/lists/members/create.json', + 'method': 'POST', + }, + 'deleteList': { + 'url': '/lists/destroy.json', + 'method': 'POST', + }, + 'updateList': { + 'url': '/lists/update.json', + 'method': 'POST', + }, + 'createList': { + 'url': '/lists/create.json', + 'method': 'POST', + }, + 'getSpecificList': { + 'url': '/lists/show.json', + 'method': 'GET', + }, + 'getListSubscriptions': { + 'url': '/lists/subscriptions.json', + 'method': 'GET', + }, + 'deleteListMembers': { + 'url': '/lists/members/destroy_all.json', + 'method': 'POST' + }, + + + # Saved Searches + 'getSavedSearches': { + 'url': '/saved_searches/list.json', + 'method': 'GET', + }, + 'showSavedSearch': { + 'url': '/saved_searches/show/{{id}}.json', + 'method': 'GET', + }, + 'createSavedSearch': { + 'url': '/saved_searches/create.json', + 'method': 'POST', + }, + 'destroySavedSearch': { + 'url': '/saved_searches/destroy/{{id}}.json', + 'method': 'POST', + }, + + + # Places & Geo + 'getGeoInfo': { + 'url': '/geo/id/{{place_id}}.json', + 'method': 'GET', + }, + 'reverseGeocode': { + 'url': '/geo/reverse_geocode.json', + 'method': 'GET', + }, + 'searchGeo': { + 'url': '/geo/search.json', + 'method': 'GET', + }, + 'getSimilarPlaces': { + 'url': '/geo/similar_places.json', + 'method': 'GET', + }, + 'createPlace': { + 'url': '/geo/place.json', + 'method': 'POST', + }, + + + # Trends + 'getPlaceTrends': { + 'url': '/trends/place.json', + 'method': 'GET', + }, + 'getAvailableTrends': { + 'url': '/trends/available.json', + 'method': 'GET', + }, + 'getClosestTrends': { + 'url': '/trends/closest.json', + 'method': 'GET', + }, + + + # Spam Reporting + 'reportSpam': { + 'url': '/users/report_spam.json', + 'method': 'POST', + }, } + # from https://dev.twitter.com/docs/error-codes-responses twitter_http_status_codes = { 200: ('OK', 'Success!'), diff --git a/twython/twython.py b/twython/twython.py index 717080e..29b7418 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.5.5" +__version__ = "2.6.0" import urllib import re @@ -337,39 +337,6 @@ class Twython(object): 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()]) - def search(self, **kwargs): - """ Returns tweets that match a specified query. - - Documentation: https://dev.twitter.com/doc/get/search - - :param q: (required) The query you want to search Twitter for - - :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') - """ - - return self.get('https://api.twitter.com/1.1/search/tweets.json', params=kwargs) - def searchGen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. @@ -382,7 +349,7 @@ class Twython(object): print result """ kwargs['q'] = search_query - content = self.get('https://api.twitter.com/1.1/search/tweets.json', params=kwargs) + content = self.search(q=search_query, **kwargs) if not content['results']: raise StopIteration From 454a41fe94b08ca93ae04b9b580cf2ff51e2fa75 Mon Sep 17 00:00:00 2001 From: Virendra Rajput Date: Sun, 31 Mar 2013 21:23:16 +0530 Subject: [PATCH 345/687] added the missing slash in "getMentionsTimeline" was unable to fetch mentions because of the missing slash and the missing '.json' --- twython/twitter_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 03c418a..5f79e66 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -21,7 +21,7 @@ base_url = 'http://api.twitter.com/{{version}}' api_table = { # Timelines 'getMentionsTimeline': { - 'url': 'statuses/mentions_timeline', + 'url': '/statuses/mentions_timeline.json', 'method': 'GET', }, 'getUserTimeline': { From a6afb2cf5ce1a2f6c5750f0b4dd43ea260acf8c7 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sun, 31 Mar 2013 12:38:07 -0400 Subject: [PATCH 346/687] Version bump! --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 94d4006..e6d49c9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.6.0' +__version__ = '2.6.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 29b7418..93c5c42 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.6.0" +__version__ = "2.6.1" import urllib import re From 7d1ffefc45a7f29877034c0fe2a95ab00a26d5ac Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:44:05 -0400 Subject: [PATCH 347/687] Fixes #158, #159, #160 --- twython/twython.py | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 93c5c42..09b2783 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -99,7 +99,6 @@ class Twython(object): 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' # Enforce unicode on keys and secrets @@ -265,8 +264,11 @@ class Twython(object): return self._last_call['headers'][header] return self._last_call - def get_authentication_tokens(self): + def get_authentication_tokens(self, force_login=False, screen_name=''): """Returns an authorization URL for a user to hit. + + :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. + :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value """ request_args = {} if self.callback_url: @@ -287,6 +289,12 @@ class Twython(object): 'oauth_token': request_tokens['oauth_token'], } + if force_login: + auth_url_params.update({ + 'force_login': force_login, + 'screen_name': screen_name + }) + # Use old-style callback argument if server didn't accept new-style if self.callback_url and not oauth_callback_confirmed: auth_url_params['oauth_callback'] = self.callback_url @@ -453,28 +461,12 @@ class Twython(object): ########################################################################### def getProfileImageUrl(self, username, size='normal', version='1'): - """Gets the URL for the user's profile image. - - :param username: (required) Username, self explanatory. - :param size: (optional) Default 'normal' (48px by 48px) - bigger - 73px by 73px - mini - 24px by 24px - original - undefined, be careful -- images may be - large in bytes and/or size. - :param version: (optional) A number, default 1 because that's the - only API version for Twitter that supports this call - """ - endpoint = 'users/profile_image/%s' % username - url = self.api_url % version + '/' + endpoint - - response = self.client.get(url, params={'size': size}, 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 - else: - raise TwythonError('getProfileImageUrl() threw an error.', - error_code=response.status_code) + warnings.warn( + "This function has been deprecated. Twitter API v1.1 will not have a dedicated endpoint \ + for this functionality.", + DeprecationWarning, + stacklevel=2 + ) @staticmethod def stream(data, callback): From fffedd4588a73c85032c09227af6c6e2ca5857c7 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:52:32 -0400 Subject: [PATCH 348/687] Version bump --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e6d49c9..a6bf312 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.6.1' +__version__ = '2.7.0' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 09b2783..cddf739 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.6.1" +__version__ = "2.7.0" import urllib import re From e65790d7170c97c120dd6037102358c276907f45 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:52:54 -0400 Subject: [PATCH 349/687] New showOwnedLists method Returns the lists owned by the specified Twitter user. Private lists will only be shown if the authenticated user is also the owner of the lists. --- twython/twitter_endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index 5f79e66..ff78779 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -320,6 +320,10 @@ api_table = { 'url': '/lists/members/destroy_all.json', 'method': 'POST' }, + 'showOwnedLists': { + 'url': '/lists/ownerships.json', + 'method': 'GET' + }, # Saved Searches From 99a6dccbce2fea77689475b18b411f49cc97d076 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 5 Apr 2013 00:12:54 +0200 Subject: [PATCH 350/687] added oauth_verifier arg --- twython/twython.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index cddf739..cd724a3 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -81,7 +81,7 @@ class TwythonRateLimitError(TwythonError): class Twython(object): - def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ + def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, oauth_verifier=None, \ headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -110,6 +110,8 @@ class Twython(object): self.callback_url = callback_url + self.oauth_verifier = oauth_verifier + # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers or {'User-Agent': 'Twython v' + __version__} @@ -306,7 +308,7 @@ class Twython(object): def get_authorized_tokens(self): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url) + response = self.client.get(self.access_token_url,params={'oauth_verifier' : self.oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content)) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') From 26b3a232d0be46ce17920bca287260ec8eca43bc Mon Sep 17 00:00:00 2001 From: hansenrum Date: Fri, 5 Apr 2013 00:25:23 +0200 Subject: [PATCH 351/687] oauth_verifier fix --- twython/twython.py | 1 - 1 file changed, 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index cd724a3..e9f701c 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -109,7 +109,6 @@ class Twython(object): self.oauth_token_secret = oauth_token_secret and u'%s' % oauth_token_secret self.callback_url = callback_url - self.oauth_verifier = oauth_verifier # If there's headers, set them, otherwise be an embarassing parent for their own good. From 1eb1bd080d88e35b13de355926e64a14ebe767a7 Mon Sep 17 00:00:00 2001 From: hansenrum Date: Fri, 5 Apr 2013 18:36:58 +0200 Subject: [PATCH 352/687] moved oauth_verifier from init to method --- twython/twython.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index e9f701c..2e27ee5 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -81,7 +81,7 @@ class TwythonRateLimitError(TwythonError): class Twython(object): - def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, oauth_verifier=None, \ + def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). @@ -109,7 +109,6 @@ class Twython(object): self.oauth_token_secret = oauth_token_secret and u'%s' % oauth_token_secret self.callback_url = callback_url - self.oauth_verifier = oauth_verifier # If there's headers, set them, otherwise be an embarassing parent for their own good. self.headers = headers or {'User-Agent': 'Twython v' + __version__} @@ -304,10 +303,10 @@ class Twython(object): return request_tokens - def get_authorized_tokens(self): + def get_authorized_tokens(self, oauth_verifier): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url,params={'oauth_verifier' : self.oauth_verifier}) + response = self.client.get(self.access_token_url, params={'oauth_verifier' : oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content)) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') From abaa3e558a75d225a31d5ad1df93a459361334f9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 11:49:12 -0400 Subject: [PATCH 353/687] oauth_verifier required, remove simplejson dependency, update endpoint * Update `updateProfileBannerImage` to use the v1.1 endpoint * Added `getProfileBannerSizes` method using the GET /users/profile_banner.json endpoint * Fixed a couple of endpoints using variable in the url: * destroyDirectMessage, createBlock, destroyBlock no longer use id in their urls, this shouldn't break anything though. (t.destroyDirectMessage(id=123) should still work) * `oauth_verifier` is now **required** when calling `get_authorized_tokens` * Updated docs - removed getProfileImageUrl docs since it is deprecated. Noted since `Twython` 2.7.0 that users should focus on migrating to v1.1 endpoints since Twitter is deprecating v1 endpoints in May!, --- README.md | 18 +++++------------- README.rst | 17 +++++------------ setup.py | 2 +- twython/twitter_endpoints.py | 12 ++++++++---- twython/twython.py | 24 ++++++++---------------- 5 files changed, 27 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c3fcca5..cbf80d1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Features - Twitter lists - Timelines - User avatar URL - - and anything found in [the docs](https://dev.twitter.com/docs/api) + - and anything found in [the docs](https://dev.twitter.com/docs/api/1.1) * Image Uploading! - **Update user status with an image** - Change user avatar @@ -57,7 +57,7 @@ from twython import Twython ''' oauth_token and oauth_token_secret come from the previous step -if needed, store those in a session variable or something +if needed, store those in a session variable or something. oauth_verifier from the previous call is now required to pass to get_authorized_tokens ''' t = Twython(app_key=app_key, @@ -65,7 +65,7 @@ t = Twython(app_key=app_key, oauth_token=oauth_token, oauth_token_secret=oauth_token_secret) -auth_tokens = t.get_authorized_tokens() +auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens ``` @@ -90,16 +90,6 @@ t = Twython(app_key=app_key, print t.getHomeTimeline() ``` -###### Get a user avatar url *(no authentication needed)* - -```python -from twython import Twython - -t = Twython() -print t.getProfileImageUrl('ryanmcgrath', size='bigger') -print t.getProfileImageUrl('mikehelmick') -``` - ###### Streaming API *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.* @@ -122,6 +112,8 @@ Twython.stream({ Notes ----- +Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! + As of Twython 2.0.0, we have changed 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. Development of Twython (specifically, 1.3) diff --git a/README.rst b/README.rst index 1bb6677..02cfa0a 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ Features - Twitter lists - Timelines - User avatar URL - - and anything found in `the docs `_ + - and anything found in `the docs `_ * Image Uploading! - **Update user status with an image** - Change user avatar @@ -58,7 +58,7 @@ Handling the callback ''' oauth_token and oauth_token_secret come from the previous step - if needed, store those in a session variable or something + if needed, store those in a session variable or something. oauth_verifier from the previous call is now required to pass to get_authorized_tokens ''' from twython import Twython @@ -67,7 +67,7 @@ Handling the callback oauth_token=oauth_token, oauth_token_secret=oauth_token_secret) - auth_tokens = t.get_authorized_tokens() + auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens *Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* @@ -90,15 +90,6 @@ Getting a user home timeline # Returns an dict of the user home timeline print t.getHomeTimeline() -Get a user avatar url *(no authentication needed)* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -:: - - from twython import Twython - - t = Twython() - print t.getProfileImageUrl('ryanmcgrath', size='bigger') - print t.getProfileImageUrl('mikehelmick') Streaming API ~~~~~~~~~~~~~ @@ -124,6 +115,8 @@ streams.* Notes ----- +* Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the `Twitter API Documentation `_ to help migrate your API calls! + * As of Twython 2.0.0, we have changed routes for functions to abide by the `Twitter Spring 2012 clean up `_ Please make changes to your code accordingly. diff --git a/setup.py b/setup.py index a6bf312..d8f6863 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['simplejson', 'requests>=1.0.0, <2.0.0', 'requests_oauthlib==0.3.0'], + install_requires=['requests>=1.0.0, <2.0.0', 'requests_oauthlib==0.3.0'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index ff78779..d946b25 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -9,7 +9,7 @@ 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 - with 47, instead of defaulting to 1 (said defaulting takes place at conversion time). + with 47, instead of defaulting to 1.1 (said defaulting takes place at conversion time). This map is organized the order functions are documented at: https://dev.twitter.com/docs/api/1.1 @@ -87,7 +87,7 @@ api_table = { 'method': 'GET', }, 'destroyDirectMessage': { - 'url': '/direct_messages/destroy/{{id}}.json', + 'url': '/direct_messages/destroy.json', 'method': 'POST', }, 'sendDirectMessage': { @@ -183,11 +183,11 @@ api_table = { 'method': 'GET', }, 'createBlock': { - 'url': '/blocks/create/{{id}}.json', + 'url': '/blocks/create.json', 'method': 'POST', }, 'destroyBlock': { - 'url': '/blocks/destroy/{{id}}.json', + 'url': '/blocks/destroy.json', 'method': 'POST', }, 'lookupUser': { @@ -215,6 +215,10 @@ api_table = { 'method': 'POST', }, # See twython.py for update_profile_banner + 'getProfileBannerSizes': { + 'url': '/users/profile_banner.json', + 'method': 'GET', + }, # Suggested Users diff --git a/twython/twython.py b/twython/twython.py index 2e27ee5..b189d3f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -26,17 +26,9 @@ except ImportError: from twitter_endpoints import base_url, api_table, twitter_http_status_codes try: - import simplejson + import simplejson as json except ImportError: - try: - # Python 2.6 and up - import json as simplejson - except ImportError: - try: - from django.utils import simplejson - except: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/") + import json class TwythonError(Exception): @@ -194,7 +186,7 @@ class Twython(object): # why? twitter will return invalid json with an error code in the headers json_error = False try: - content = simplejson.loads(content) + content = content.json() except ValueError: json_error = True content = {} @@ -437,13 +429,13 @@ class Twython(object): **params - You may pass items that are taken in this doc (https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media) """ - subdomain = 'upload' if version == '1' else 'api' - url = 'https://%s.twitter.com/%s/statuses/update_with_media.json' % (subdomain, version) + + url = 'https://api.twitter.com/%s/statuses/update_with_media.json' % version return self._media_update(url, {'media': (file_, open(file_, 'rb'))}, **params) - def updateProfileBannerImage(self, file_, version=1, **params): + def updateProfileBannerImage(self, file_, version='1.1', **params): """Updates the users profile banner :param file_: (required) A string to the location of the file @@ -453,7 +445,7 @@ class Twython(object): **params - You may pass items that are taken in this doc (https://dev.twitter.com/docs/api/1/post/account/update_profile_banner) """ - url = 'https://api.twitter.com/%d/account/update_profile_banner.json' % version + url = 'https://api.twitter.com/%s/account/update_profile_banner.json' % version return self._media_update(url, {'banner': (file_, open(file_, 'rb'))}, **params) @@ -518,7 +510,7 @@ class Twython(object): for line in stream.iter_lines(): if line: try: - callback(simplejson.loads(line)) + callback(json.loads(line)) except ValueError: raise TwythonError('Response was not valid JSON, unable to decode.') From 4a181d3ac14ef41abff46cec9298044787791d8b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 11:51:34 -0400 Subject: [PATCH 354/687] Version bump! --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d8f6863..ceb205f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.7.0' +__version__ = '2.7.1' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index b189d3f..aff9967 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.7.0" +__version__ = "2.7.1" import urllib import re From 6a3539882caf500e1287c0f71d107a4db1a45862 Mon Sep 17 00:00:00 2001 From: Virendra Rajput Date: Mon, 8 Apr 2013 22:59:27 +0530 Subject: [PATCH 355/687] Update twython.py if unicode object is detected, convert it to json using simplejson/json --- twython/twython.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/twython/twython.py b/twython/twython.py index aff9967..afd968b 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -186,7 +186,12 @@ class Twython(object): # why? twitter will return invalid json with an error code in the headers json_error = False try: - content = content.json() + try: + # try to get json + content = content.json() + except AttributeError: + # if unicode detected + content = json.loads(content) except ValueError: json_error = True content = {} From 10dbe11b565aaa824d3ebc5d0c787072e6b9cbc6 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 16:40:59 -0400 Subject: [PATCH 356/687] Version bump and update contributors! --- README.md | 1 + README.rst | 1 + setup.py | 2 +- twython/twython.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbf80d1..b5f5d82 100644 --- a/README.md +++ b/README.md @@ -187,3 +187,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - **[terrycojones](https://github.com/terrycojones)**, Error cleanup and Exception processing in 2.3.0. - **[Leandro Ferreira](https://github.com/leandroferreira)**, Fix for double-encoding of search queries in 2.3.0. - **[Chris Brown](https://github.com/chbrown)**, Updated to use v1.1 endpoints over v1 +- **[Virendra Rajput](https://github.com/bkvirendra)**, Fixed unicode (json) encoding in twython.py 2.7.2. diff --git a/README.rst b/README.rst index 02cfa0a..cca5a26 100644 --- a/README.rst +++ b/README.rst @@ -194,3 +194,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - `terrycojones `_, Error cleanup and Exception processing in 2.3.0. - `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. - `Chris Brown `_, Updated to use v1.1 endpoints over v1 +- `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. diff --git a/setup.py b/setup.py index ceb205f..198584c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.7.1' +__version__ = '2.7.2' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index afd968b..b3c50b0 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.7.1" +__version__ = "2.7.2" import urllib import re From 8dfb076f11ef9806c9dfa34b97c1a90a2b75751e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 10 Apr 2013 23:08:43 -0400 Subject: [PATCH 357/687] Update authors --- README.md | 1 + README.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index b5f5d82..c1119d2 100644 --- a/README.md +++ b/README.md @@ -188,3 +188,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - **[Leandro Ferreira](https://github.com/leandroferreira)**, Fix for double-encoding of search queries in 2.3.0. - **[Chris Brown](https://github.com/chbrown)**, Updated to use v1.1 endpoints over v1 - **[Virendra Rajput](https://github.com/bkvirendra)**, Fixed unicode (json) encoding in twython.py 2.7.2. +- **[Paul Solbach](https://github.com/hansenrum)**, fixed requirement for oauth_verifier diff --git a/README.rst b/README.rst index cca5a26..5267099 100644 --- a/README.rst +++ b/README.rst @@ -195,3 +195,4 @@ me and let me know (or just issue a pull request on GitHub, and leave a note abo - `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. - `Chris Brown `_, Updated to use v1.1 endpoints over v1 - `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. +- `Paul Solbach `_, fixed requirement for oauth_verifier From 12eb1610c895357da49b759bb4bea2f1f44097ce Mon Sep 17 00:00:00 2001 From: Greg Nofi Date: Thu, 11 Apr 2013 19:42:16 -0400 Subject: [PATCH 358/687] Use built-in Exception attributes for storing and retrieving error message. Keeping msg as a property so it's backwards compatible. Note that this only fixes Python 2.x --- twython/twython.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index b3c50b0..aca83fd 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -42,17 +42,18 @@ class TwythonError(Exception): from twython import TwythonError, TwythonAPILimit, TwythonAuthError """ 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' % \ - (twitter_http_status_codes[error_code][0], - twitter_http_status_codes[error_code][1], - self.msg) + msg = '%s: %s -- %s' % (twitter_http_status_codes[error_code][0], + twitter_http_status_codes[error_code][1], + msg) - def __str__(self): - return repr(self.msg) + super(TwythonError, self).__init__(msg) + + @property + def msg(self): + return self.args[0] class TwythonAuthError(TwythonError): @@ -67,9 +68,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): - TwythonError.__init__(self, msg, error_code=error_code) if isinstance(retry_after, int): - self.msg = '%s (Retry after %d seconds)' % (msg, retry_after) + msg = '%s (Retry after %d seconds)' % (msg, retry_after) + TwythonError.__init__(self, msg, error_code=error_code) class Twython(object): From 7469f8bc739fa28fde7644bd2743bb8bc5511841 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:17:40 -0400 Subject: [PATCH 359/687] Fixes #175, #177 * Auth Errors are thrown in the correct spots * Error messages are a lot cleaner than before and correspond with error codes on https://dev.twitter.com/docs/error-codes-responses --- twython/twitter_endpoints.py | 5 ++++- twython/twython.py | 40 +++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/twython/twitter_endpoints.py b/twython/twitter_endpoints.py index d946b25..1c0d8a0 100644 --- a/twython/twitter_endpoints.py +++ b/twython/twitter_endpoints.py @@ -404,8 +404,11 @@ twitter_http_status_codes = { 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.'), + 410: ('Gone', 'This resource is gone. Used to indicate that an API endpoint has been turned off.'), + 422: ('Unprocessable Entity', 'Returned when an image uploaded to POST account/update_profile_banner is unable to be processed.'), + 429: ('Too Many Requests', 'Returned in API v1.1 when a request cannot be served due to the application\'s rate limit having been exhausted for the resource.'), 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.'), + 504: ('Gateway Timeout', 'The Twitter servers are up, but the request couldn\'t be serviced due to some failure within our stack. Try again later.'), } diff --git a/twython/twython.py b/twython/twython.py index aca83fd..07be9a9 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -39,15 +39,15 @@ class TwythonError(Exception): Note: To use these, the syntax has changed as of Twython 1.3. To catch these, you need to explicitly import them into your code, e.g: - from twython import TwythonError, TwythonAPILimit, TwythonAuthError + from twython import TwythonError, TwythonRateLimitError, TwythonAuthError """ def __init__(self, msg, error_code=None, retry_after=None): self.error_code = error_code if error_code is not None and error_code in twitter_http_status_codes: - msg = '%s: %s -- %s' % (twitter_http_status_codes[error_code][0], - twitter_http_status_codes[error_code][1], - msg) + msg = 'Twitter API returned a %s (%s), %s' % (error_code, + twitter_http_status_codes[error_code][0], + msg) super(TwythonError, self).__init__(msg) @@ -74,8 +74,8 @@ class TwythonRateLimitError(TwythonError): class Twython(object): - def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, \ - headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): + def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, + headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -199,14 +199,21 @@ class Twython(object): if response.status_code > 304: # If there is no error message, use a default. - error_msg = content.get( - 'error', 'An error occurred processing your request.') - self._last_call['api_error'] = error_msg + errors = content.get('errors', + [{'message': 'An error occurred processing your request.'}]) + error_message = errors[0]['message'] + self._last_call['api_error'] = error_message - #Twitter API 1.1 , always return 429 when rate limit is exceeded - exceptionType = TwythonRateLimitError if response.status_code == 429 else TwythonError + ExceptionType = TwythonError + if response.status_code == 429: + # Twitter API 1.1, always return 429 when rate limit is exceeded + ExceptionType = TwythonRateLimitError + elif response.status_code == 401 or 'Bad Authentication data' in error_message: + # Twitter API 1.1, returns a 401 Unauthorized or + # a 400 "Bad Authentication data" for invalid/expired app keys/user tokens + ExceptionType = TwythonAuthError - raise exceptionType(error_msg, + raise ExceptionType(error_message, error_code=response.status_code, retry_after=response.headers.get('retry-after')) @@ -273,9 +280,10 @@ class Twython(object): request_args['oauth_callback'] = self.callback_url response = self.client.get(self.request_token_url, params=request_args) - - if response.status_code != 200: - raise TwythonAuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (response.status_code, response.content)) + if response.status_code == 401: + raise TwythonAuthError(response.content, error_code=response.status_code) + elif response.status_code != 200: + raise TwythonError(response.content, error_code=response.status_code) request_tokens = dict(parse_qsl(response.content)) if not request_tokens: @@ -304,7 +312,7 @@ class Twython(object): def get_authorized_tokens(self, oauth_verifier): """Returns authorized tokens after they go through the auth_url phase. """ - response = self.client.get(self.access_token_url, params={'oauth_verifier' : oauth_verifier}) + response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content)) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') From 969c0f5e72344e814f52959935f75e24f0ddcd87 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:28:58 -0400 Subject: [PATCH 360/687] Move AUTHORS into their own file remove README md as well, see how the rst looks --- AUTHORS.rst | 39 +++++++++++++++++++++++++++++++++++++++ README.md | 34 ---------------------------------- README.rst | 35 ----------------------------------- 3 files changed, 39 insertions(+), 69 deletions(-) create mode 100644 AUTHORS.rst diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..34f03b8 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,39 @@ +Special Thanks +-------------- +This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's +exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact +me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;). + +Development Lead +```````````````` + +- Ryan Mcgrath + + +Patches and Suggestions +```````````````````````` + +- `Mike Helmick `_, multiple fixes and proper ``requests`` integration. Too much to list here. +- `kracekumar `_, early ``requests`` work and various fixes. +- `Erik Scheffers `_, various fixes regarding OAuth callback URLs. +- `Jordan Bouvier `_, various fixes regarding OAuth callback URLs. +- `Dick Brouwer `_, fixes for OAuth Verifier in ``get_authorized_tokens``. +- `hades `_, Fixes to various initial OAuth issues and keeping ``Twython3k`` up-to-date. +- `Alex Sutton `_, fix for parameter substitution regular expression (catch underscores!). +- `Levgen Pyvovarov `_, Various argument fixes, cyrillic text support. +- `Mark Liu `_, Missing parameter fix for ``addListMember``. +- `Randall Degges `_, PEP-8 fixes, MANIFEST.in, installer fixes. +- `Idris Mokhtarzada `_, Fixes for various example code pieces. +- `Jonathan Elsas `_, Fix for original Streaming API stub causing import errors. +- `LuqueDaniel `_, Extended example code where necessary. +- `Mesar Hameed `_, Commit to swap ``__getattr__`` trick for a more debuggable solution. +- `Remy DeCausemaker `_, PEP-8 contributions. +- `mckellister `_ Twitter Spring 2012 Clean Up fixes to ``Exception`` raised by Twython (Rate Limits, etc). +- `Tatz Tsuchiya `_, Fix for ``lambda`` scoping in key injection phase. +- `Mohammed ALDOUB `_, Fixes for ``http/https`` access endpoints. +- `Fumiaki Kinoshita `_, Re-added Proxy support for 2.3.0. +- `Terry Jones `_, Error cleanup and Exception processing in 2.3.0. +- `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. +- `Chris Brown `_, Updated to use v1.1 endpoints over v1 +- `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. +- `Paul Solbach `_, fixed requirement for oauth_verifier \ No newline at end of file diff --git a/README.md b/README.md index c1119d2..aa41a4c 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,6 @@ Notes ----- Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! -As of Twython 2.0.0, we have changed 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. - Development of Twython (specifically, 1.3) ------------------------------------------ As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored @@ -157,35 +155,3 @@ Twython is released under an MIT License - see the LICENSE file for more informa Want to help? ------------- Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! - - -Special Thanks to... ------------------------------------------------------------------------------------------------------ -This is a list of all those who have contributed code to Twython in some way, shape, or form. I think it's -exhaustive, but I could be wrong - if you think your name should be here and it's not, please contact -me and let me know (or just issue a pull request on GitHub, and leave a note about it so I can just accept it ;)). - -- **[Mike Helmick (michaelhelmick)](https://github.com/michaelhelmick)**, multiple fixes and proper `requests` integration. -- **[kracekumar](https://github.com/kracekumar)**, early `requests` work and various fixes. -- **[Erik Scheffers (eriks5)](https://github.com/eriks5)**, various fixes regarding OAuth callback URLs. -- **[Jordan Bouvier (jbouvier)](https://github.com/jbouvier)**, various fixes regarding OAuth callback URLs. -- **[Dick Brouwer (dikbrouwer)](https://github.com/dikbrouwer)**, fixes for OAuth Verifier in `get_authorized_tokens`. -- **[hades](https://github.com/hades)**, Fixes to various initial OAuth issues and updates to `Twython3k` to stay current. -- **[Alex Sutton (alexdsutton)](https://github.com/alexsdutton/twython/)**, fix for parameter substitution regular expression (catch underscores!). -- **[Levgen Pyvovarov (bsn)](https://github.com/bsn)**, Various argument fixes, cyrillic text support. -- **[Mark Liu (mliu7)](https://github.com/mliu7)**, Missing parameter fix for `addListMember`. -- **[Randall Degges (rdegges)](https://github.com/rdegges)**, PEP-8 fixes, MANIFEST.in, installer fixes. -- **[Idris Mokhtarzada (idris)](https://github.com/idris)**, Fixes for various example code pieces. -- **[Jonathan Elsas (jelsas)](https://github.com/jelsas)**, Fix for original Streaming API stub causing import errors. -- **[LuqueDaniel](https://github.com/LuqueDaniel)**, Extended example code where necessary. -- **[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. -- **[Voulnet (Mohammed ALDOUB)](https://github.com/Voulnet)**, Fixes for `http`/`https` access endpoints -- **[fumieval](https://github.com/fumieval)**, Re-added Proxy support for 2.3.0. -- **[terrycojones](https://github.com/terrycojones)**, Error cleanup and Exception processing in 2.3.0. -- **[Leandro Ferreira](https://github.com/leandroferreira)**, Fix for double-encoding of search queries in 2.3.0. -- **[Chris Brown](https://github.com/chbrown)**, Updated to use v1.1 endpoints over v1 -- **[Virendra Rajput](https://github.com/bkvirendra)**, Fixed unicode (json) encoding in twython.py 2.7.2. -- **[Paul Solbach](https://github.com/hansenrum)**, fixed requirement for oauth_verifier diff --git a/README.rst b/README.rst index 5267099..f39d2b7 100644 --- a/README.rst +++ b/README.rst @@ -117,9 +117,6 @@ Notes ----- * Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the `Twitter API Documentation `_ to help migrate your API calls! -* As of Twython 2.0.0, we have changed routes for functions to abide by the `Twitter Spring 2012 clean up `_ Please make changes to your code accordingly. - - Twython && Django ----------------- If you're using Twython with Django, there's a sample project showcasing OAuth and such **[that can be found here](https://github.com/ryanmcgrath/twython-django)**. Feel free to peruse! @@ -164,35 +161,3 @@ You can also follow me on Twitter - `@ryanmcgrath `_, multiple fixes and proper ``requests`` integration. Too much to list here. -- `kracekumar `_, early ``requests`` work and various fixes. -- `Erik Scheffers (eriks5) `_, various fixes regarding OAuth callback URLs. -- `Jordan Bouvier (jbouvier) `_, various fixes regarding OAuth callback URLs. -- `Dick Brouwer (dikbrouwer) `_, fixes for OAuth Verifier in ``get_authorized_tokens``. -- `hades `_, Fixes to various initial OAuth issues and updates to ``Twython3k`` to stay current. -- `Alex Sutton (alexdsutton) `_, fix for parameter substitution regular expression (catch underscores!). -- `Levgen Pyvovarov (bsn) `_, Various argument fixes, cyrillic text support. -- `Mark Liu (mliu7) `_, Missing parameter fix for ``addListMember``. -- `Randall Degges (rdegges) `_, PEP-8 fixes, MANIFEST.in, installer fixes. -- `Idris Mokhtarzada (idris) `_, Fixes for various example code pieces. -- `Jonathan Elsas (jelsas) `_, Fix for original Streaming API stub causing import errors. -- `LuqueDaniel `_, Extended example code where necessary. -- `Mesar Hameed (mhameed) `_, Commit to swap ``__getattr__`` trick for a more debuggable solution. -- `Remy DeCausemaker (decause) `_, PEP-8 contributions. -- `[mckellister](https://github.com/mckellister) `_, Fixes to ``Exception`` raised by Twython (Rate Limits, etc). -- `tatz_tsuchiya `_, Fix for ``lambda`` scoping in key injection phase. -- `Voulnet (Mohammed ALDOUB) `_, Fixes for ``http/https`` access endpoints. -- `fumieval `_, Re-added Proxy support for 2.3.0. -- `terrycojones `_, Error cleanup and Exception processing in 2.3.0. -- `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. -- `Chris Brown `_, Updated to use v1.1 endpoints over v1 -- `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. -- `Paul Solbach `_, fixed requirement for oauth_verifier From d228e04bc098866fa680e84eab4cf8c72c40f5b3 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:40:05 -0400 Subject: [PATCH 361/687] Version bump! --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 198584c..249035c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.7.2' +__version__ = '2.7.3' setup( # Basic package information. diff --git a/twython/twython.py b/twython/twython.py index 07be9a9..bf66740 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -7,7 +7,7 @@ """ __author__ = "Ryan McGrath " -__version__ = "2.7.2" +__version__ = "2.7.3" import urllib import re From 023b29b202f8aa9b2027a6baa62378d38024b4c8 Mon Sep 17 00:00:00 2001 From: Adrien Tronche Date: Sun, 14 Apr 2013 00:16:28 -0300 Subject: [PATCH 362/687] Small correction in comments Headers have changed and a - is now needed between rate and limit. --- twython/twython.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index bf66740..853e6bd 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -258,10 +258,10 @@ class Twython(object): This will return None if the header is not present Most useful for the following header information: - x-ratelimit-limit - x-ratelimit-remaining - x-ratelimit-class - x-ratelimit-reset + x-rate-limit-limit + x-rate-limit-remaining + x-rate-limit-class + x-rate-limit-reset """ if self._last_call is None: raise TwythonError('This function must be called after an API call. It delivers header information.') From 6d1c439a8920e808dd7a3099c81ff9ef2eef1257 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 15 Apr 2013 16:12:02 -0400 Subject: [PATCH 363/687] Move Exceptions to own file --- twython/__init__.py | 2 +- twython/exceptions.py | 48 +++++++++++++++++++++++++++++++++++++++++++ twython/twython.py | 45 ++-------------------------------------- 3 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 twython/exceptions.py diff --git a/twython/__init__.py b/twython/__init__.py index 26a3860..97be55a 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1,2 +1,2 @@ from twython import Twython -from twython import TwythonError, TwythonRateLimitError, TwythonAuthError +from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError diff --git a/twython/exceptions.py b/twython/exceptions.py new file mode 100644 index 0000000..7c939af --- /dev/null +++ b/twython/exceptions.py @@ -0,0 +1,48 @@ +from twitter_endpoints import twitter_http_status_codes + + +class TwythonError(Exception): + """ + Generic error class, catch-all for most Twython issues. + Special cases are handled by TwythonAuthError & TwythonRateLimitError. + + Note: Syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: + + from twython import ( + TwythonError, TwythonRateLimitError, TwythonAuthError + ) + """ + def __init__(self, msg, error_code=None, retry_after=None): + self.error_code = error_code + + if error_code is not None and error_code in twitter_http_status_codes: + msg = 'Twitter API returned a %s (%s), %s' % \ + (error_code, + twitter_http_status_codes[error_code][0], + msg) + + super(TwythonError, self).__init__(msg) + + @property + def msg(self): + return self.args[0] + + +class TwythonAuthError(TwythonError): + """ Raised when you try to access a protected resource and it fails due to + some issue with your authentication. + """ + pass + + +class TwythonRateLimitError(TwythonError): + """ Raised when you've hit a rate limit. + + The amount of seconds to retry your request in will be appended + to the message. + """ + def __init__(self, msg, error_code, retry_after=None): + if isinstance(retry_after, int): + msg = '%s (Retry after %d seconds)' % (msg, retry_after) + TwythonError.__init__(self, msg, error_code=error_code) diff --git a/twython/twython.py b/twython/twython.py index bf66740..415b145 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -23,7 +23,8 @@ 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, twitter_http_status_codes +from twitter_endpoints import base_url, api_table +from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError try: import simplejson as json @@ -31,48 +32,6 @@ except ImportError: import json -class TwythonError(Exception): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by TwythonAPILimit and TwythonAuthError. - - Note: To use these, the syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: - - from twython import TwythonError, TwythonRateLimitError, TwythonAuthError - """ - def __init__(self, msg, error_code=None, retry_after=None): - self.error_code = error_code - - if error_code is not None and error_code in twitter_http_status_codes: - msg = 'Twitter API returned a %s (%s), %s' % (error_code, - twitter_http_status_codes[error_code][0], - msg) - - super(TwythonError, self).__init__(msg) - - @property - def msg(self): - return self.args[0] - - -class TwythonAuthError(TwythonError): - """ Raised when you try to access a protected resource and it fails due to - some issue with your authentication. - """ - pass - - -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): - if isinstance(retry_after, int): - msg = '%s (Retry after %d seconds)' % (msg, retry_after) - TwythonError.__init__(self, msg, error_code=error_code) - - class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): From 80c74880b1a5370d6fde4299f9fb547ed89e44be Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 15 Apr 2013 16:15:52 -0400 Subject: [PATCH 364/687] Update authors --- AUTHORS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 34f03b8..dff2a73 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -36,4 +36,5 @@ Patches and Suggestions - `Leandro Ferreira `_, Fix for double-encoding of search queries in 2.3.0. - `Chris Brown `_, Updated to use v1.1 endpoints over v1 - `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. -- `Paul Solbach `_, fixed requirement for oauth_verifier \ No newline at end of file +- `Paul Solbach `_, fixed requirement for oauth_verifier +- `Greg Nofi `_, fixed using built-in Exception attributes for storing & retrieving error message From bb019d3a577dda00d59b6873f0d61191d30c35d9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 17 Apr 2013 18:59:11 -0400 Subject: [PATCH 365/687] Making twython work (again?) in Python 3 - Added a ``HISTORY.rst`` to start tracking history of changes - Updated ``twitter_endpoints.py`` to ``endpoints.py`` for cleanliness - Removed twython3k directory, no longer needed - Added ``compat.py`` for compatability with Python 2.6 and greater - Added some ascii art, moved description of Twython and ``__author__`` to ``__init__.py`` - Added ``version.py`` to store the current Twython version, instead of repeating it twice -- it also had to go into it's own file because of dependencies of ``requests`` and ``requests-oauthlib``, install would fail because those libraries weren't installed yet (on fresh install of Twython) - Removed ``find_packages()`` from ``setup.py``, only one package -- we can just define it - added quick publish method for Ryan and I: ``python setup.py publish`` is faster to type and easier to remember than ``python setup.py sdist upload`` - Removed ``base_url`` from ``endpoints.py`` because we're just repeating it in ``Twython.__init__`` - ``Twython.get_authentication_tokens()`` now takes ``callback_url`` argument rather than passing the ``callback_url`` through ``Twython.__init__``, ``callback_url`` is only used in the ``get_authentication_tokens`` method and nowhere else (kept in init though for backwards compatability) - Updated README to better reflect current Twython codebase - Added ``warnings.simplefilter('default')`` line in ``twython.py`` for Python 2.7 and greater to display Deprecation Warnings in console - Added Deprecation Warnings for usage of ``twitter_token``, ``twitter_secret`` and ``callback_url`` in ``Twython.__init__`` - Headers now always include the User-Agent as Twython vXX unless User-Agent is overwritten - Removed senseless TwythonError thrown if method is not GET or POST, who cares -- if the user passes something other than GET or POST just let Twitter return the error that they messed up - Removed conversion to unicode of (int, bool) params passed to a requests. ``requests`` isn't greedy about variables that can't be converted to unicode anymore --- .gitignore | 39 +- AUTHORS.rst | 2 +- HISTORY.rst | 58 ++ MANIFEST.in | 2 +- README.md | 64 ++- README.rst | 77 +-- setup.py | 17 +- twython/__init__.py | 23 +- twython/compat.py | 38 ++ .../{twitter_endpoints.py => endpoints.py} | 3 - twython/exceptions.py | 2 +- twython/twython.py | 132 ++--- twython/version.py | 1 + twython3k/__init__.py | 1 - twython3k/twitter_endpoints.py | 334 ------------ twython3k/twython.py | 513 ------------------ 16 files changed, 306 insertions(+), 1000 deletions(-) create mode 100644 HISTORY.rst create mode 100644 twython/compat.py rename twython/{twitter_endpoints.py => endpoints.py} (99%) create mode 100644 twython/version.py delete mode 100644 twython3k/__init__.py delete mode 100644 twython3k/twitter_endpoints.py delete mode 100644 twython3k/twython.py diff --git a/.gitignore b/.gitignore index 0b15049..5684153 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,36 @@ -*.pyc -build +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info dist -twython.egg-info -*.swp +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject \ No newline at end of file diff --git a/AUTHORS.rst b/AUTHORS.rst index dff2a73..b6a6dfb 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,7 +13,7 @@ Development Lead Patches and Suggestions ```````````````````````` -- `Mike Helmick `_, multiple fixes and proper ``requests`` integration. Too much to list here. +- `Mike Helmick `_, multiple fixes and proper ``requests`` integration, Python 3 compatibility, too much to list here. - `kracekumar `_, early ``requests`` work and various fixes. - `Erik Scheffers `_, various fixes regarding OAuth callback URLs. - `Jordan Bouvier `_, various fixes regarding OAuth callback URLs. diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..015f38d --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,58 @@ +History +------- + +2.8.0 (2013-xx-xx) +++++++++++++++++++ + +- Added a ``HISTORY.rst`` to start tracking history of changes +- Updated ``twitter_endpoints.py`` to ``endpoints.py`` for cleanliness +- Removed twython3k directory, no longer needed +- Added ``compat.py`` for compatability with Python 2.6 and greater +- Added some ascii art, moved description of Twython and ``__author__`` to ``__init__.py`` +- Added ``version.py`` to store the current Twython version, instead of repeating it twice -- it also had to go into it's own file because of dependencies of ``requests`` and ``requests-oauthlib``, install would fail because those libraries weren't installed yet (on fresh install of Twython) +- Removed ``find_packages()`` from ``setup.py``, only one package -- we can +just define it +- added quick publish method for Ryan and I: ``python setup.py publish`` is faster to type and easier to remember than ``python setup.py sdist upload`` +- Removed ``base_url`` from ``endpoints.py`` because we're just repeating it in +``Twython.__init__`` +- ``Twython.get_authentication_tokens()`` now takes ``callback_url`` argument rather than passing the ``callback_url`` through ``Twython.__init__``, ``callback_url`` is only used in the ``get_authentication_tokens`` method and nowhere else (kept in init though for backwards compatability) +- Updated README to better reflect current Twython codebase +- Added ``warnings.simplefilter('default')`` line in ``twython.py`` for Python 2.7 and greater to display Deprecation Warnings in console +- Added Deprecation Warnings for usage of ``twitter_token``, ``twitter_secret`` and ``callback_url`` in ``Twython.__init__`` +- Headers now always include the User-Agent as Twython vXX unless User-Agent is overwritten +- Removed senseless TwythonError thrown if method is not GET or POST, who cares -- if the user passes something other than GET or POST just let Twitter return the error that they messed up +- Removed conversion to unicode of (int, bool) params passed to a requests. ``requests`` isn't greedy about variables that can't be converted to unicode anymore + +2.7.3 (2013-04-12) +++++++++++++++++++ + +- Fixed issue where Twython Exceptions were not being logged correctly + +2.7.2 (2013-04-08) +++++++++++++++++++ + +- Fixed ``AttributeError`` when trying to decode the JSON response via ``Response.json()`` + +2.7.1 (2013-04-08) +++++++++++++++++++ + +- Removed ``simplejson`` dependency +- Fixed ``destroyDirectMessage``, ``createBlock``, ``destroyBlock`` endpoints in ``twitter_endpoints.py`` +- Added ``getProfileBannerSizes`` method to ``twitter_endpoints.py`` +- Made oauth_verifier argument required in ``get_authorized_tokens`` +- Update ``updateProfileBannerImage`` to use v1.1 endpoint + +2.7.0 (2013-04-04) +++++++++++++++++++ + +- New ``showOwnedLists`` method + +2.7.0 (2013-03-31) +++++++++++++++++++ + +- Added missing slash to ``getMentionsTimeline`` in ``twitter_endpoints.py`` + +2.6.0 (2013-03-29) +++++++++++++++++++ + +- Updated ``twitter_endpoints.py`` to better reflect order of API endpoints on the Twitter API v1.1 docs site \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 9d3d7b1..dd547fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.md README.rst +include LICENSE README.md README.rst HISTORY.rst recursive-include examples * recursive-exclude examples *.pyc diff --git a/README.md b/README.md index aa41a4c..4c8fe3b 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ Features - User information - Twitter lists - Timelines - - User avatar URL + - Direct Messages - and anything found in [the docs](https://dev.twitter.com/docs/api/1.1) * Image Uploading! - **Update user status with an image** - Change user avatar - Change user background image + - Change user banner image Installation ------------ @@ -36,11 +37,9 @@ Usage ```python from twython import Twython -t = Twython(app_key=app_key, - app_secret=app_secret, - callback_url='http://google.com/') +t = Twython(app_key, app_secret) -auth_props = t.get_authentication_tokens() +auth_props = t.get_authentication_tokens(callback_url='http://google.com') oauth_token = auth_props['oauth_token'] oauth_token_secret = auth_props['oauth_token_secret'] @@ -55,41 +54,53 @@ Be sure you have a URL set up to handle the callback after the user has allowed ```python from twython import Twython -''' -oauth_token and oauth_token_secret come from the previous step -if needed, store those in a session variable or something. oauth_verifier from the previous call is now required to pass to get_authorized_tokens -''' +# oauth_token_secret comes from the previous step +# if needed, store that in a session variable or something. +# oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens -t = Twython(app_key=app_key, - app_secret=app_secret, - oauth_token=oauth_token, - oauth_token_secret=oauth_token_secret) +# In Django, to get the oauth_verifier and oauth_token from the callback +# url querystring, you might do something like this: +# oauth_token = request.GET.get('oauth_token') +# oauth_verifier = request.GET.get('oauth_verifier') + +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens ``` -*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* +*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/endpoints.py* ###### Getting a user home timeline ```python from twython import Twython -''' -oauth_token and oauth_token_secret are the final tokens produced -from the `Handling the callback` step -''' +# oauth_token and oauth_token_secret are the final tokens produced +# from the 'Handling the callback' step -t = Twython(app_key=app_key, - app_secret=app_secret, - oauth_token=oauth_token, - oauth_token_secret=oauth_token_secret) +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) # Returns an dict of the user home timeline print t.getHomeTimeline() ``` +###### Catching exceptions +> Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError +```python +from twython import Twython, TwythonAuthError + +t = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET, + BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET) + +try: + t.verifyCredentials() +except TwythonAuthError as e: + print e +``` + ###### Streaming API *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.* @@ -136,12 +147,7 @@ from you using them by this library. Twython 3k ---------- -There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to -work in all situations, but it's provided so that others can grab it and hack on it. -If you choose to try it out, be aware of this. - -**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab -his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** +Full compatiabilty with Python 3 is now available seamlessly in the main Twython package. The Twython 3k package has been removed as of Twython 2.8.0 Questions, Comments, etc? ------------------------- @@ -150,8 +156,6 @@ at ryan@venodesigns.net. You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. -Twython is released under an MIT License - see the LICENSE file for more information. - Want to help? ------------- Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! diff --git a/README.rst b/README.rst index f39d2b7..3b0117f 100644 --- a/README.rst +++ b/README.rst @@ -9,12 +9,13 @@ Features - User information - Twitter lists - Timelines - - User avatar URL + - Direct Messages - and anything found in `the docs `_ * Image Uploading! - **Update user status with an image** - Change user avatar - Change user background image + - Change user banner image Installation ------------ @@ -37,13 +38,12 @@ Usage Authorization URL ~~~~~~~~~~~~~~~~~ :: - from twython import Twython - - t = Twython(app_key=app_key, - app_secret=app_secret, - callback_url='http://google.com/') - auth_props = t.get_authentication_tokens() + from twython import Twython + + t = Twython(app_key, app_secret) + + auth_props = t.get_authentication_tokens(callback_url='http://google.com') oauth_token = auth_props['oauth_token'] oauth_token_secret = auth_props['oauth_token_secret'] @@ -56,41 +56,59 @@ Handling the callback ~~~~~~~~~~~~~~~~~~~~~ :: - ''' - oauth_token and oauth_token_secret come from the previous step - if needed, store those in a session variable or something. oauth_verifier from the previous call is now required to pass to get_authorized_tokens - ''' from twython import Twython - t = Twython(app_key=app_key, - app_secret=app_secret, - oauth_token=oauth_token, - oauth_token_secret=oauth_token_secret) + # oauth_token_secret comes from the previous step + # if needed, store that in a session variable or something. + # oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens + + # In Django, to get the oauth_verifier and oauth_token from the callback + # url querystring, you might do something like this: + # oauth_token = request.GET.get('oauth_token') + # oauth_verifier = request.GET.get('oauth_verifier') + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens -*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/twitter_endpoints.py* +*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/endpoints.py* Getting a user home timeline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - ''' - oauth_token and oauth_token_secret are the final tokens produced - from the `Handling the callback` step - ''' from twython import Twython - - t = Twython(app_key=app_key, - app_secret=app_secret, - oauth_token=oauth_token, - oauth_token_secret=oauth_token_secret) + + # oauth_token and oauth_token_secret are the final tokens produced + # from the 'Handling the callback' step + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) # Returns an dict of the user home timeline print t.getHomeTimeline() +Catching exceptions +~~~~~~~~~~~~~~~~~~~ + + Twython offers three Exceptions currently: ``TwythonError``, ``TwythonAuthError`` and ``TwythonRateLimitError`` + +:: + + from twython import Twython, TwythonAuthError + + t = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET, + BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET) + + try: + t.verifyCredentials() + except TwythonAuthError as e: + print e + + Streaming API ~~~~~~~~~~~~~ *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) @@ -143,12 +161,7 @@ from you using them by this library. Twython 3k ---------- -There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed to -work in all situations, but it's provided so that others can grab it and hack on it. -If you choose to try it out, be aware of this. - -**OAuth is now working thanks to updates from [Hades](https://github.com/hades). You'll need to grab -his [Python 3 branch for python-oauth2](https://github.com/hades/python-oauth2/tree/python3) to have it work, though.** +Full compatiabilty with Python 3 is now available seamlessly in the main Twython package. The Twython 3k package has been removed as of Twython 2.8.0 Questions, Comments, etc? ------------------------- @@ -156,8 +169,6 @@ My hope is that Twython is so simple that you'd never *have* to ask any question You can also follow me on Twitter - `@ryanmcgrath `_ -*Twython is released under an MIT License - see the LICENSE file for more information.* - Want to help? ------------- Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! diff --git a/setup.py b/setup.py index 249035c..4ce9628 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,25 @@ +import os +import sys + +from twython.version import __version__ + from setuptools import setup -from setuptools import find_packages __author__ = 'Ryan McGrath ' -__version__ = '2.7.3' + +packages = [ + 'twython' +] + +if sys.argv[-1] == 'publish': + os.system('python setup.py sdist upload') + sys.exit() setup( # Basic package information. name='twython', version=__version__, - packages=find_packages(), + packages=packages, # Packaging options. include_package_data=True, diff --git a/twython/__init__.py b/twython/__init__.py index 97be55a..fc493ae 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -1,2 +1,23 @@ -from twython import Twython +# ______ __ __ +# /_ __/_ __ __ __ / /_ / /_ ____ ____ +# / / | | /| / // / / // __// __ \ / __ \ / __ \ +# / / | |/ |/ // /_/ // /_ / / / // /_/ // / / / +# /_/ |__/|__/ \__, / \__//_/ /_/ \____//_/ /_/ +# /____/ + +""" +Twython +------- + +Twython is a library for Python that wraps the Twitter API. + +It aims to abstract away all the API endpoints, so that additions to the library +and/or the Twitter API won't cause any overall problems. + +Questions, comments? ryan@venodesigns.net +""" + +__author__ = 'Ryan McGrath ' + +from .twython import Twython from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError diff --git a/twython/compat.py b/twython/compat.py new file mode 100644 index 0000000..48caa46 --- /dev/null +++ b/twython/compat.py @@ -0,0 +1,38 @@ +import sys + +_ver = sys.version_info + +#: Python 2.x? +is_py2 = (_ver[0] == 2) + +#: Python 3.x? +is_py3 = (_ver[0] == 3) + +try: + import simplejson as json +except ImportError: + import json + +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + +if is_py2: + from urllib import urlencode, quote_plus + + builtin_str = str + bytes = str + str = unicode + basestring = basestring + numeric_types = (int, long, float) + + +elif is_py3: + from urllib.parse import urlencode, quote_plus + + builtin_str = str + str = str + bytes = bytes + basestring = (str, bytes) + numeric_types = (int, float) diff --git a/twython/twitter_endpoints.py b/twython/endpoints.py similarity index 99% rename from twython/twitter_endpoints.py rename to twython/endpoints.py index 1c0d8a0..8fabb36 100644 --- a/twython/twitter_endpoints.py +++ b/twython/endpoints.py @@ -15,9 +15,6 @@ https://dev.twitter.com/docs/api/1.1 """ -# Base Twitter API url, no need to repeat this junk... -base_url = 'http://api.twitter.com/{{version}}' - api_table = { # Timelines 'getMentionsTimeline': { diff --git a/twython/exceptions.py b/twython/exceptions.py index 7c939af..17736e4 100644 --- a/twython/exceptions.py +++ b/twython/exceptions.py @@ -1,4 +1,4 @@ -from twitter_endpoints import twitter_http_status_codes +from .endpoints import twitter_http_status_codes class TwythonError(Exception): diff --git a/twython/twython.py b/twython/twython.py index f6d0fc8..02ba05e 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,40 +1,19 @@ -""" - Twython is a library for Python that wraps the Twitter API. - It aims to abstract away all the API endpoints, so that additions to the library - and/or the Twitter API won't cause any overall problems. - - Questions, comments? ryan@venodesigns.net -""" - -__author__ = "Ryan McGrath " -__version__ = "2.7.3" - -import urllib import re import warnings +warnings.simplefilter('default') # For Python 2.7 > import requests from requests_oauthlib import OAuth1 -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - -# 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 .compat import json, urlencode, parse_qsl, quote_plus +from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError - -try: - import simplejson as json -except ImportError: - import json +from .version import __version__ class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, - headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'): + headers=None, proxies=None, version='1.1', callback_url=None, twitter_token=None, twitter_secret=None): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -46,46 +25,55 @@ class Twython(object): :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. """ - # Needed for hitting that there API. + # API urls, OAuth urls and API version; needed for hitting that there API. self.api_version = version 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.authenticate_url = self.api_url % 'oauth/authenticate' - # Enforce unicode on keys and secrets - self.app_key = app_key and unicode(app_key) or twitter_token and unicode(twitter_token) - self.app_secret = app_key and unicode(app_secret) or twitter_secret and unicode(twitter_secret) - - self.oauth_token = oauth_token and u'%s' % oauth_token - self.oauth_token_secret = oauth_token_secret and u'%s' % oauth_token_secret + self.app_key = app_key or twitter_token + self.app_secret = app_secret or twitter_secret + self.oauth_token = oauth_token + self.oauth_token_secret = oauth_token_secret self.callback_url = callback_url - # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers or {'User-Agent': 'Twython v' + __version__} + if twitter_token or twitter_secret: + warnings.warn( + 'Instead of twitter_token or twitter_secret, please use app_key or app_secret (respectively).', + DeprecationWarning, + stacklevel=2 + ) - # Allow for unauthenticated requests - self.client = requests.Session() - self.client.proxies = proxies + if callback_url: + warnings.warn( + 'Please pass callback_url to the get_authentication_tokens method rather than Twython.__init__', + DeprecationWarning, + stacklevel=2 + ) + + self.headers = {'User-Agent': 'Twython v' + __version__} + if headers: + self.headers.update(headers) + + # Generate OAuth authentication object for the request + # If no keys/tokens are passed to __init__, self.auth=None allows for + # unauthenticated requests, although I think all v1.1 requests need auth self.auth = None + if self.app_key is not None and self.app_secret is not None and \ + self.oauth_token is None and self.oauth_token_secret is None: + self.auth = OAuth1(self.app_key, self.app_secret) if self.app_key is not None and self.app_secret is not None and \ - self.oauth_token is None and self.oauth_token_secret is None: + self.oauth_token is not None and self.oauth_token_secret is not None: self.auth = OAuth1(self.app_key, self.app_secret, - signature_type='auth_header') + self.oauth_token, self.oauth_token_secret) - if self.app_key is not None and self.app_secret is not None and \ - self.oauth_token is not None and self.oauth_token_secret is not None: - self.auth = OAuth1(self.app_key, self.app_secret, - self.oauth_token, self.oauth_token_secret, - signature_type='auth_header') - - if self.auth is not None: - self.client = requests.Session() - self.client.headers = self.headers - self.client.auth = self.auth - self.client.proxies = proxies + self.client = requests.Session() + self.client.headers = self.headers + self.client.proxies = proxies + self.client.auth = self.auth # register available funcs to allow listing name when debugging. def setFunc(key): @@ -101,8 +89,8 @@ class Twython(object): fn = api_table[api_call] url = re.sub( '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), self.api_version), - base_url + fn['url'] + lambda m: "%s" % kwargs.get(m.group(1)), + self.api_url % self.api_version + fn['url'] ) content = self._request(url, method=fn['method'], params=kwargs) @@ -114,15 +102,7 @@ class Twython(object): code twice, right? ;) ''' method = method.lower() - if not method in ('get', 'post'): - raise TwythonError('Method must be of GET or POST') - params = params or {} - # requests doesn't like items that can't be converted to unicode, - # so let's be nice and do that for the user - for k, v in params.items(): - if isinstance(v, (int, bool)): - params[k] = u'%s' % v func = getattr(self.client, method) if method == 'get': @@ -176,7 +156,7 @@ class Twython(object): error_code=response.status_code, retry_after=response.headers.get('retry-after')) - # if we have a json error here, then it's not an official TwitterAPI error + # if we have a json error here, then it's not an official Twitter API error if json_error and not response.status_code in (200, 201, 202): raise TwythonError('Response was not valid JSON, unable to decode.') @@ -228,23 +208,23 @@ class Twython(object): return self._last_call['headers'][header] return self._last_call - def get_authentication_tokens(self, force_login=False, screen_name=''): - """Returns an authorization URL for a user to hit. + def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''): + """Returns a dict including an authorization URL (auth_url) to direct a user to + :param callback_url: (optional.. for now) Url the user is returned to after they authorize your app :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value """ - request_args = {} - if self.callback_url: - request_args['oauth_callback'] = self.callback_url - + callback_url = callback_url or self.callback_url + request_args = {'oauth_callback': callback_url} response = self.client.get(self.request_token_url, params=request_args) + if response.status_code == 401: raise TwythonAuthError(response.content, error_code=response.status_code) elif response.status_code != 200: raise TwythonError(response.content, error_code=response.status_code) - request_tokens = dict(parse_qsl(response.content)) + request_tokens = dict(parse_qsl(response.content.decode('utf-8'))) if not request_tokens: raise TwythonError('Unable to decode request tokens.') @@ -261,18 +241,20 @@ class Twython(object): }) # Use old-style callback argument if server didn't accept new-style - if self.callback_url and not oauth_callback_confirmed: + if callback_url and not oauth_callback_confirmed: auth_url_params['oauth_callback'] = self.callback_url - request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params) + request_tokens['auth_url'] = self.authenticate_url + '?' + urlencode(auth_url_params) return request_tokens def get_authorized_tokens(self, oauth_verifier): """Returns authorized tokens after they go through the auth_url phase. + + :param oauth_verifier: (required) The oauth_verifier retrieved from the callback url querystring """ response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) - authorized_tokens = dict(parse_qsl(response.content)) + authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) if not authorized_tokens: raise TwythonError('Unable to decode authorized tokens.') @@ -308,7 +290,7 @@ class Twython(object): @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()]) + return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) def searchGen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. @@ -331,7 +313,7 @@ class Twython(object): yield tweet if 'page' not in kwargs: - kwargs['page'] = '2' + kwargs['page'] = 2 else: try: kwargs['page'] = int(kwargs['page']) @@ -416,7 +398,7 @@ class Twython(object): only API version for Twitter that supports this call **params - You may pass items that are taken in this doc - (https://dev.twitter.com/docs/api/1/post/account/update_profile_banner) + (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner) """ url = 'https://api.twitter.com/%s/account/update_profile_banner.json' % version return self._media_update(url, diff --git a/twython/version.py b/twython/version.py new file mode 100644 index 0000000..66eabed --- /dev/null +++ b/twython/version.py @@ -0,0 +1 @@ +__version__ = '2.7.3' diff --git a/twython3k/__init__.py b/twython3k/__init__.py deleted file mode 100644 index 710c742..0000000 --- a/twython3k/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .twython import Twython diff --git a/twython3k/twitter_endpoints.py b/twython3k/twitter_endpoints.py deleted file mode 100644 index 1c90aa8..0000000 --- a/twython3k/twitter_endpoints.py +++ /dev/null @@ -1,334 +0,0 @@ -""" - 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: - - {{version}}, etc - - 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. - - 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). -""" - -# Base Twitter API url, no need to repeat this junk... -base_url = 'http://api.twitter.com/{{version}}' - -api_table = { - 'getRateLimitStatus': { - 'url': '/account/rate_limit_status.json', - 'method': 'GET', - }, - - 'verifyCredentials': { - 'url': '/account/verify_credentials.json', - 'method': 'GET', - }, - - 'endSession' : { - 'url': '/account/end_session.json', - 'method': 'POST', - }, - - # Timeline methods - 'getPublicTimeline': { - 'url': '/statuses/public_timeline.json', - 'method': 'GET', - }, - 'getHomeTimeline': { - 'url': '/statuses/home_timeline.json', - 'method': 'GET', - }, - 'getUserTimeline': { - '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', - }, - 'destroyFriendship': { - 'url': '/friendships/destroy.json', - 'method': 'POST', - }, - 'getFriendsIDs': { - 'url': '/friends/ids.json', - 'method': 'GET', - }, - 'getFollowersIDs': { - 'url': '/followers/ids.json', - 'method': 'GET', - }, - 'getIncomingFriendshipIDs': { - 'url': '/friendships/incoming.json', - 'method': 'GET', - }, - 'getOutgoingFriendshipIDs': { - 'url': '/friendships/outgoing.json', - 'method': 'GET', - }, - - # Retweets - 'reTweet': { - 'url': '/statuses/retweet/{{id}}.json', - 'method': 'POST', - }, - 'getRetweets': { - 'url': '/statuses/retweets/{{id}}.json', - 'method': 'GET', - }, - 'retweetedOfMe': { - 'url': '/statuses/retweets_of_me.json', - 'method': 'GET', - }, - 'retweetedByMe': { - 'url': '/statuses/retweeted_by_me.json', - 'method': 'GET', - }, - 'retweetedToMe': { - 'url': '/statuses/retweeted_to_me.json', - 'method': 'GET', - }, - - # User methods - 'showUser': { - 'url': '/users/show.json', - 'method': 'GET', - }, - 'searchUsers': { - 'url': '/users/search.json', - 'method': 'GET', - }, - - 'lookupUser': { - 'url': '/users/lookup.json', - 'method': 'GET', - }, - - # Status methods - showing, updating, destroying, etc. - 'showStatus': { - 'url': '/statuses/show/{{id}}.json', - 'method': 'GET', - }, - 'updateStatus': { - 'url': '/statuses/update.json', - 'method': 'POST', - }, - 'destroyStatus': { - 'url': '/statuses/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Direct Messages - getting, sending, effing, etc. - 'getDirectMessages': { - 'url': '/direct_messages.json', - 'method': 'GET', - }, - 'getSentMessages': { - 'url': '/direct_messages/sent.json', - 'method': 'GET', - }, - 'sendDirectMessage': { - 'url': '/direct_messages/new.json', - 'method': 'POST', - }, - 'destroyDirectMessage': { - 'url': '/direct_messages/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Friendship methods - 'checkIfFriendshipExists': { - 'url': '/friendships/exists.json', - 'method': 'GET', - }, - 'showFriendship': { - 'url': '/friendships/show.json', - 'method': 'GET', - }, - - # Profile methods - 'updateProfile': { - 'url': '/account/update_profile.json', - 'method': 'POST', - }, - 'updateProfileColors': { - 'url': '/account/update_profile_colors.json', - 'method': 'POST', - }, - 'myTotals': { - 'url' : '/account/totals.json', - 'method': 'GET', - }, - - # Favorites methods - 'getFavorites': { - 'url': '/favorites.json', - 'method': 'GET', - }, - 'createFavorite': { - 'url': '/favorites/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyFavorite': { - 'url': '/favorites/destroy/{{id}}.json', - 'method': 'POST', - }, - - # Blocking methods - 'createBlock': { - 'url': '/blocks/create/{{id}}.json', - 'method': 'POST', - }, - 'destroyBlock': { - 'url': '/blocks/destroy/{{id}}.json', - 'method': 'POST', - }, - 'getBlocking': { - 'url': '/blocks/blocking.json', - 'method': 'GET', - }, - 'getBlockedIDs': { - 'url': '/blocks/blocking/ids.json', - 'method': 'GET', - }, - 'checkIfBlockExists': { - 'url': '/blocks/exists.json', - 'method': 'GET', - }, - - # Trending methods - 'getCurrentTrends': { - 'url': '/trends/current.json', - 'method': 'GET', - }, - 'getDailyTrends': { - 'url': '/trends/daily.json', - 'method': 'GET', - }, - 'getWeeklyTrends': { - 'url': '/trends/weekly.json', - 'method': 'GET', - }, - 'availableTrends': { - 'url': '/trends/available.json', - 'method': 'GET', - }, - 'trendsByLocation': { - 'url': '/trends/{{woeid}}.json', - 'method': 'GET', - }, - - # Saved Searches - 'getSavedSearches': { - 'url': '/saved_searches.json', - 'method': 'GET', - }, - 'showSavedSearch': { - 'url': '/saved_searches/show/{{id}}.json', - 'method': 'GET', - }, - 'createSavedSearch': { - 'url': '/saved_searches/create.json', - 'method': 'GET', - }, - 'destroySavedSearch': { - 'url': '/saved_searches/destroy/{{id}}.json', - 'method': 'GET', - }, - - # List API methods/endpoints. Fairly exhaustive and annoying in general. ;P - 'createList': { - 'url': '/{{username}}/lists.json', - 'method': 'POST', - }, - 'updateList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'POST', - }, - 'showLists': { - 'url': '/{{username}}/lists.json', - 'method': 'GET', - }, - 'getListMemberships': { - 'url': '/{{username}}/lists/memberships.json', - 'method': 'GET', - }, - 'getListSubscriptions': { - 'url': '/{{username}}/lists/subscriptions.json', - 'method': 'GET', - }, - 'deleteList': { - 'url': '/{{username}}/lists/{{list_id}}.json', - 'method': 'DELETE', - }, - 'getListTimeline': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'getSpecificList': { - 'url': '/{{username}}/lists/{{list_id}}/statuses.json', - 'method': 'GET', - }, - 'addListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'POST', - }, - 'getListMembers': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'GET', - }, - 'deleteListMember': { - 'url': '/{{username}}/{{list_id}}/members.json', - 'method': 'DELETE', - }, - 'getListSubscribers': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'GET', - }, - 'subscribeToList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'POST', - }, - 'unsubscribeFromList': { - 'url': '/{{username}}/{{list_id}}/subscribers.json', - 'method': 'DELETE', - }, - - # The one-offs - 'notificationFollow': { - 'url': '/notifications/follow/follow.json', - 'method': 'POST', - }, - 'notificationLeave': { - 'url': '/notifications/leave/leave.json', - 'method': 'POST', - }, - 'updateDeliveryService': { - 'url': '/account/update_delivery_device.json', - 'method': 'POST', - }, - 'reportSpam': { - 'url': '/report_spam.json', - 'method': 'POST', - }, -} diff --git a/twython3k/twython.py b/twython3k/twython.py deleted file mode 100644 index c8f03b9..0000000 --- a/twython3k/twython.py +++ /dev/null @@ -1,513 +0,0 @@ -#!/usr/bin/python - -""" - Twython is a library for Python that wraps the Twitter API. - It aims to abstract away all the API endpoints, so that additions to the library - and/or the Twitter API won't cause any overall problems. - - Questions, comments? ryan@venodesigns.net -""" - -__author__ = "Ryan McGrath " -__version__ = "1.4.7" - -import cgi -import urllib.request, urllib.parse, urllib.error -import urllib.request, urllib.error, urllib.parse -import urllib.parse -import http.client -import httplib2 -import mimetypes -import re -import inspect -import email.generator - -import oauth2 as oauth - -# 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 urllib.error import HTTPError - -# There are some special setups (like, oh, a Django application) where -# 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 -except ImportError: - # Seriously wtf is wrong with you if you get this Exception. - raise Exception("Twython3k requires a json library to work. http://www.undefined.org/python/") - -# Try and gauge the old OAuth2 library spec. Versions 1.5 and greater no longer have the callback -# url as part of the request object; older versions we need to patch for Python 2.5... ugh. ;P -OAUTH_CALLBACK_IN_URL = False -OAUTH_LIB_SUPPORTS_CALLBACK = False -if not hasattr(oauth, '_version') or float(oauth._version.manual_verstr) <= 1.4: - OAUTH_CLIENT_INSPECTION = inspect.getargspec(oauth.Client.request) - try: - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION.args - except AttributeError: - # Python 2.5 doesn't return named tuples, so don't look for an args section specifically. - OAUTH_LIB_SUPPORTS_CALLBACK = 'callback_url' in OAUTH_CLIENT_INSPECTION -else: - OAUTH_CALLBACK_IN_URL = True - -class TwythonError(AttributeError): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by APILimit and AuthError. - - Note: To use these, the syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: - - from twython import TwythonError, APILimit, AuthError - """ - def __init__(self, msg, error_code=None): - self.msg = msg - if error_code == 400: - raise APILimit(msg) - - def __str__(self): - return repr(self.msg) - - -class TwythonAPILimit(TwythonError): - """ - 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 __str__(self): - return repr(self.msg) - - -class APILimit(TwythonError): - """ - 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, you should be importing TwythonAPILimit instead. :) - """ - def __init__(self, msg): - self.msg = '%s\n Notice: APILimit is deprecated and soon to be removed, catch TwythonAPILimit instead!' % msg - - 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 - your authentication. - - DEPRECATED, you should be importing TwythonAuthError instead. - """ - def __init__(self, msg): - self.msg = '%s\n Notice: AuthLimit is deprecated and soon to be removed, catch TwythonAPILimit instead!' % msg - - 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, headers=None, callback_url=None, client_args={}): - """setup(self, oauth_token = None, headers = None) - - Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - - Parameters: - twitter_token - Given to you when you register your application with Twitter. - twitter_secret - Given to you when you register your application with Twitter. - oauth_token - If you've gone through the authentication process and have a token for this user, - pass it in and it'll be used for all requests going forward. - oauth_token_secret - see oauth_token; it's the other half. - headers - User agent header, dictionary style ala {'User-Agent': 'Bert'} - client_args - additional arguments for HTTP client (see httplib2.Http.__init__), e.g. {'timeout': 10.0} - - ** 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 = '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 - self.oauth_secret = oauth_token_secret - self.callback_url = callback_url - - # If there's headers, set them, otherwise be an embarassing parent for their own good. - self.headers = headers - if self.headers is None: - self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - - consumer = None - token = None - - if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) - - if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) - - # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token, **client_args) - elif consumer is not None: - self.client = oauth.Client(consumer, **client_args) - else: - # If they don't do authentication, but still want to request unprotected resources, we need an opener. - self.client = httplib2.Http(**client_args) - def setFunc(key): - return lambda **kwargs: self._constructFunc(key, **kwargs) - # register available funcs to allow listing name when debugging. - for key in api_table.keys(): - self.__dict__[key] = setFunc(key) - - def _constructFunc(self, api_call, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) - - return simplejson.loads(content.decode('utf-8')) - - def get_authentication_tokens(self): - """ - get_auth_url(self) - - Returns an authorization URL for a user to hit. - """ - callback_url = self.callback_url or 'oob' - - request_args = {} - method = 'GET' - if OAUTH_LIB_SUPPORTS_CALLBACK: - request_args['callback_url'] = callback_url - else: - # This is a hack for versions of oauth that don't support the callback URL. This is also - # done differently than the Python2 version of Twython, which uses Requests internally (as opposed to httplib2). - request_args['body'] = urllib.urlencode({'oauth_callback': callback_url}) - method = 'POST' - - resp, content = self.client.request(self.request_token_url, method, **request_args) - - if resp['status'] != '200': - raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - - try: - request_tokens = dict(urllib.parse.parse_qsl(content)) - except: - request_tokens = dict(cgi.parse_qsl(content)) - - oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' - - if not OAUTH_LIB_SUPPORTS_CALLBACK and callback_url != 'oob' and oauth_callback_confirmed: - import warnings - warnings.warn("oauth2 library doesn't support OAuth 1.0a type callback, but remote requires it") - oauth_callback_confirmed = False - - auth_url_params = { - 'oauth_token' : request_tokens['oauth_token'], - } - - if OAUTH_CALLBACK_IN_URL or (callback_url!='oob' and not oauth_callback_confirmed): - auth_url_params['oauth_callback'] = callback_url - - request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.parse.urlencode(auth_url_params) - - return request_tokens - - def get_authorized_tokens(self): - """ - get_authorized_tokens - - Returns authorized tokens after they go through the auth_url phase. - """ - resp, content = self.client.request(self.access_token_url, "GET") - try: - return dict(urllib.parse.parse_qsl(content)) - except: - return dict(cgi.parse_qsl(content)) - - # ------------------------------------------------------------------------------------------------------------------------ - # The following methods are all different in some manner or require special attention with regards to the Twitter API. - # Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able, - # but it's not high on the priority list at the moment. - # ------------------------------------------------------------------------------------------------------------------------ - - @staticmethod - def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" % (key, urllib.parse.quote_plus(Twython.unicode2utf8(value))) for (key, value) in list(params.items())]) - - @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 = urllib.request.urlopen(shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)})).read() - return content - except HTTPError as e: - raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code)) - - 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. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that - contain their respective data sets. - - Statuses for the users in question will be returned inline if they exist. Requires authentication! - """ - if ids: - kwargs['user_id'] = ','.join(map(str, ids)) - if screen_names: - kwargs['screen_name'] = ','.join(screen_names) - - 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')) - except HTTPError as e: - raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code) - - def search(self, **kwargs): - """search(search_query, **kwargs) - - Returns tweets that match a specified query. - - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - - e.g x.search(q = "jjndf", page = '2') - """ - 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')) - except HTTPError as e: - raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code) - - def searchTwitter(self, **kwargs): - """use search() ,this is a fall back method to support searchTwitter() - """ - return self.search(**kwargs) - - def searchGen(self, search_query, **kwargs): - """searchGen(search_query, **kwargs) - - Returns a generator of tweets that match a specified query. - - Parameters: - See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters. - - e.g x.searchGen("python", page="2") or - x.searchGen(search_query = "python", page = "2") - """ - 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')) - except HTTPError as e: - raise TwythonError("searchGen() failed with a %s error code." % repr(e.code), e.code) - - if not data['results']: - raise StopIteration - - for tweet in data['results']: - yield tweet - - if 'page' not in kwargs: - kwargs['page'] = '2' - else: - try: - kwargs['page'] = int(kwargs['page']) - 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" %\ - repr(e.code), e.code) - - 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): - """ isListMember(self, list_id, id, version) - - 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. - - 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. - """ - try: - 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) - - def isListSubscriber(self, username, list_id, id, version = 1): - """ isListSubscriber(self, list_id, id, version) - - Check if a specified user (id) is a subscriber 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. - - 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. - """ - try: - 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) - - # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true", version = 1): - """ updateProfileBackgroundImage(filename, tile="true") - - Updates the authenticating user's profile background image. - - Parameters: - image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" - 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: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - 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) - - def updateProfileImage(self, filename, version = 1): - """ updateProfileImage(filename) - - Updates the authenticating user's profile image (avatar). - - Parameters: - 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. - """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - 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) - - def getProfileImageUrl(self, username, size=None, version=1): - """ getProfileImageUrl(username) - - Gets the URL for the user's profile image. - - Parameters: - username - Required. User name of the user you want the image url of. - 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}) - - client = httplib2.Http() - client.follow_redirects = False - resp, content = client.request(url, 'GET') - - if resp.status in (301,302,303,307): - return resp['location'] - elif resp.status == 200: - return simplejson.loads(content.decode('utf-8')) - - raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) - - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = email.generator._make_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - @staticmethod - def unicode2utf8(text): - try: - if isinstance(text, str): - text = text.encode('utf-8') - except: - pass - return text - - @staticmethod - def encode(text): - if isinstance(text, str): - return Twython.unicode2utf8(text) - return str(text) From 4ac6436901d16394e264cd36234e55d24e4dba28 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Apr 2013 22:21:32 -0400 Subject: [PATCH 366/687] Update Examples and LICENSE Some examples were out of date (any trends example) Renamed the core_examples dir. to examples --- LICENSE | 2 +- README.md | 11 ++++++----- core_examples/current_trends.py | 7 ------- core_examples/daily_trends.py | 7 ------- core_examples/get_user_timeline.py | 7 ------- core_examples/search_results.py | 9 --------- core_examples/update_profile_image.py | 9 --------- core_examples/update_status.py | 14 -------------- core_examples/weekly_trends.py | 7 ------- examples/get_user_timeline.py | 10 ++++++++++ examples/search_results.py | 12 ++++++++++++ {core_examples => examples}/shorten_url.py | 2 +- examples/update_profile_image.py | 5 +++++ examples/update_status.py | 9 +++++++++ 14 files changed, 44 insertions(+), 67 deletions(-) delete mode 100644 core_examples/current_trends.py delete mode 100644 core_examples/daily_trends.py delete mode 100644 core_examples/get_user_timeline.py delete mode 100644 core_examples/search_results.py delete mode 100644 core_examples/update_profile_image.py delete mode 100644 core_examples/update_status.py delete mode 100644 core_examples/weekly_trends.py create mode 100644 examples/get_user_timeline.py create mode 100644 examples/search_results.py rename {core_examples => examples}/shorten_url.py (64%) create mode 100644 examples/update_profile_image.py create mode 100644 examples/update_status.py diff --git a/LICENSE b/LICENSE index cd5b253..f723943 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2009 - 2010 Ryan McGrath +Copyright (c) 2013 Ryan McGrath Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4c8fe3b..38205f9 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Installation Usage ----- -###### Authorization URL +##### Authorization URL ```python from twython import Twython @@ -49,7 +49,7 @@ print 'Connect to Twitter via: %s' % auth_props['auth_url'] Be sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date. -###### Handling the callback +##### Handling the callback ```python from twython import Twython @@ -72,7 +72,7 @@ print auth_tokens *Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/endpoints.py* -###### Getting a user home timeline +##### Getting a user home timeline ```python from twython import Twython @@ -87,8 +87,9 @@ t = Twython(app_key, app_secret, print t.getHomeTimeline() ``` -###### Catching exceptions +##### Catching exceptions > Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError + ```python from twython import Twython, TwythonAuthError @@ -101,7 +102,7 @@ except TwythonAuthError as e: print e ``` -###### Streaming API +##### Streaming API *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.* diff --git a/core_examples/current_trends.py b/core_examples/current_trends.py deleted file mode 100644 index 53f64f1..0000000 --- a/core_examples/current_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -from twython import Twython - -""" Instantiate Twython with no Authentication """ -twitter = Twython() -trends = twitter.getCurrentTrends() - -print trends diff --git a/core_examples/daily_trends.py b/core_examples/daily_trends.py deleted file mode 100644 index d4acc66..0000000 --- a/core_examples/daily_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -from twython import Twython - -""" Instantiate Twython with no Authentication """ -twitter = Twython() -trends = twitter.getDailyTrends() - -print trends diff --git a/core_examples/get_user_timeline.py b/core_examples/get_user_timeline.py deleted file mode 100644 index 9dd27e8..0000000 --- a/core_examples/get_user_timeline.py +++ /dev/null @@ -1,7 +0,0 @@ -from twython import Twython - -# We won't authenticate for this, but sometimes it's necessary -twitter = Twython() -user_timeline = twitter.getUserTimeline(screen_name="ryanmcgrath") - -print user_timeline diff --git a/core_examples/search_results.py b/core_examples/search_results.py deleted file mode 100644 index 57b4c51..0000000 --- a/core_examples/search_results.py +++ /dev/null @@ -1,9 +0,0 @@ -from twython import Twython - -""" Instantiate Twython with no Authentication """ -twitter = Twython() -search_results = twitter.search(q="WebsDotCom", rpp="50") - -for tweet in search_results["results"]: - print "Tweet from @%s Date: %s" % (tweet['from_user'].encode('utf-8'),tweet['created_at']) - print tweet['text'].encode('utf-8'),"\n" \ No newline at end of file diff --git a/core_examples/update_profile_image.py b/core_examples/update_profile_image.py deleted file mode 100644 index 857140a..0000000 --- a/core_examples/update_profile_image.py +++ /dev/null @@ -1,9 +0,0 @@ -from twython import Twython - -""" - You'll need to go through the OAuth ritual to be able to successfully - use this function. See the example oauth django application included in - this package for more information. -""" -twitter = Twython() -twitter.updateProfileImage("myImage.png") diff --git a/core_examples/update_status.py b/core_examples/update_status.py deleted file mode 100644 index 9b7deca..0000000 --- a/core_examples/update_status.py +++ /dev/null @@ -1,14 +0,0 @@ -from twython import Twython - -""" - Note: for any method that'll require you to be authenticated (updating - things, etc) - you'll need to go through the OAuth authentication ritual. See the example - Django application that's included with this package for more information. -""" -twitter = Twython() - -# OAuth ritual... - - -twitter.updateStatus(status="See how easy this was?") diff --git a/core_examples/weekly_trends.py b/core_examples/weekly_trends.py deleted file mode 100644 index d457242..0000000 --- a/core_examples/weekly_trends.py +++ /dev/null @@ -1,7 +0,0 @@ -from twython import Twython - -""" Instantiate Twython with no Authentication """ -twitter = Twython() -trends = twitter.getWeeklyTrends() - -print trends diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py new file mode 100644 index 0000000..19fb031 --- /dev/null +++ b/examples/get_user_timeline.py @@ -0,0 +1,10 @@ +from twython import Twython, TwythonError + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) +try: + user_timeline = twitter.getUserTimeline(screen_name='ryanmcgrath') +except TwythonError as e: + print e + +print user_timeline diff --git a/examples/search_results.py b/examples/search_results.py new file mode 100644 index 0000000..6d4dc7c --- /dev/null +++ b/examples/search_results.py @@ -0,0 +1,12 @@ +from twython import Twython, TwythonError + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) +try: + search_results = twitter.search(q="WebsDotCom", rpp="50") +except TwythonError as e: + print e + +for tweet in search_results["results"]: + print "Tweet from @%s Date: %s" % (tweet['from_user'].encode('utf-8'),tweet['created_at']) + print tweet['text'].encode('utf-8'),"\n" \ No newline at end of file diff --git a/core_examples/shorten_url.py b/examples/shorten_url.py similarity index 64% rename from core_examples/shorten_url.py rename to examples/shorten_url.py index 8ca57ba..42d0f40 100644 --- a/core_examples/shorten_url.py +++ b/examples/shorten_url.py @@ -1,6 +1,6 @@ from twython import Twython # Shortening URLs requires no authentication, huzzah -shortURL = Twython.shortenURL("http://www.webs.com/") +shortURL = Twython.shortenURL('http://www.webs.com/') print shortURL diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py new file mode 100644 index 0000000..1bb9782 --- /dev/null +++ b/examples/update_profile_image.py @@ -0,0 +1,5 @@ +from twython import Twython + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) +twitter.updateProfileImage('myImage.png') diff --git a/examples/update_status.py b/examples/update_status.py new file mode 100644 index 0000000..1ecfcba --- /dev/null +++ b/examples/update_status.py @@ -0,0 +1,9 @@ +from twython import Twython, TwythonError + +# Requires Authentication as of Twitter API v1.1 +twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +try: + twitter.updateStatus(status='See how easy this was?') +except TwythonError as e: + print e From d4c19fc3a9357f642ba27cc1c08d88df80704871 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 19 Apr 2013 02:14:22 -0400 Subject: [PATCH 367/687] Version bump --- twython/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/version.py b/twython/version.py index 66eabed..f2df444 100644 --- a/twython/version.py +++ b/twython/version.py @@ -1 +1 @@ -__version__ = '2.7.3' +__version__ = '2.8.0' From a451db43c16e57f29e9f2d892e9f615dcac339e9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 22 Apr 2013 21:29:07 -0400 Subject: [PATCH 368/687] Removed bulkUserLookup & getProfileImageUrl, deprecating shortenUrl, raise TwythonDepWarnings in Python 2.7 > --- HISTORY.rst | 3 +++ twython/advisory.py | 5 +++++ twython/compat.py | 11 +++++----- twython/endpoints.py | 18 +++++++-------- twython/twython.py | 52 ++++++++++++++++---------------------------- 5 files changed, 41 insertions(+), 48 deletions(-) create mode 100644 twython/advisory.py diff --git a/HISTORY.rst b/HISTORY.rst index 015f38d..fc82b5e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -22,6 +22,9 @@ just define it - Headers now always include the User-Agent as Twython vXX unless User-Agent is overwritten - Removed senseless TwythonError thrown if method is not GET or POST, who cares -- if the user passes something other than GET or POST just let Twitter return the error that they messed up - Removed conversion to unicode of (int, bool) params passed to a requests. ``requests`` isn't greedy about variables that can't be converted to unicode anymore +- Removed `bulkUserLookup` (please use `lookupUser` instead), removed `getProfileImageUrl` (will be completely removed from Twitter API on May 7th, 2013) +- Updated shortenUrl to actually work for those using it, but it is being deprecated since `requests` makes it easy for developers to implement their own url shortening in their app (see https://github.com/ryanmcgrath/twython/issues/184) +- Twython Deprecation Warnings will now be seen in shell when using Python 2.7 and greater 2.7.3 (2013-04-12) ++++++++++++++++++ diff --git a/twython/advisory.py b/twython/advisory.py new file mode 100644 index 0000000..edff80e --- /dev/null +++ b/twython/advisory.py @@ -0,0 +1,5 @@ +class TwythonDeprecationWarning(DeprecationWarning): + """Custom DeprecationWarning to be raised when methods/variables are being deprecated in Twython. + Python 2.7 > ignores DeprecationWarning so we want to specifcally bubble up ONLY Twython Deprecation Warnings + """ + pass diff --git a/twython/compat.py b/twython/compat.py index 48caa46..8da417e 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -13,13 +13,12 @@ try: except ImportError: import json -try: - from urlparse import parse_qsl -except ImportError: - from cgi import parse_qsl - if is_py2: from urllib import urlencode, quote_plus + try: + from urlparse import parse_qsl + except ImportError: + from cgi import parse_qsl builtin_str = str bytes = str @@ -29,7 +28,7 @@ if is_py2: elif is_py3: - from urllib.parse import urlencode, quote_plus + from urllib.parse import urlencode, quote_plus, parse_qsl builtin_str = str str = str diff --git a/twython/endpoints.py b/twython/endpoints.py index 8fabb36..d63fc03 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -1,18 +1,18 @@ """ - 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 - will be replaced with the keyword that gets passed in to the function at call time. +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. - i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced - with 47, instead of defaulting to 1.1 (said defaulting takes place at conversion time). +i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced +with 47, instead of defaulting to 1.1 (said defaulting takes place at conversion time). - This map is organized the order functions are documented at: - https://dev.twitter.com/docs/api/1.1 +This map is organized the order functions are documented at: +https://dev.twitter.com/docs/api/1.1 """ api_table = { diff --git a/twython/twython.py b/twython/twython.py index 02ba05e..19f2f68 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,15 +1,17 @@ import re import warnings -warnings.simplefilter('default') # For Python 2.7 > import requests from requests_oauthlib import OAuth1 +from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .version import __version__ +warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > + class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, @@ -42,14 +44,14 @@ class Twython(object): if twitter_token or twitter_secret: warnings.warn( 'Instead of twitter_token or twitter_secret, please use app_key or app_secret (respectively).', - DeprecationWarning, + TwythonDeprecationWarning, stacklevel=2 ) if callback_url: warnings.warn( 'Please pass callback_url to the get_authentication_tokens method rather than Twython.__init__', - DeprecationWarning, + TwythonDeprecationWarning, stacklevel=2 ) @@ -267,7 +269,7 @@ class Twython(object): # ------------------------------------------------------------------------------------------------------------------------ @staticmethod - def shortenURL(url_to_shorten, shortener='http://is.gd/api.php'): + def shortenURL(url_to_shorten, shortener='http://is.gd/create.php'): """Shortens url specified by url_to_shorten. Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, but we keep this here for anyone who was previously using it for alternative purposes. ;) @@ -276,11 +278,18 @@ class Twython(object): :param shortener: (optional) In case you want to use a different URL shortening service """ + warnings.warn( + 'With requests it\'s easy enough for a developer to implement url shortenting themselves. Please see: https://github.com/ryanmcgrath/twython/issues/184', + TwythonDeprecationWarning, + stacklevel=2 + ) + if shortener == '': raise TwythonError('Please provide a URL shortening service.') request = requests.get(shortener, params={ - 'query': url_to_shorten + 'format': 'json', + 'url': url_to_shorten }) if request.status_code in [301, 201, 200]: @@ -290,7 +299,7 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): - return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) + return base_url + '?' + '&'.join(['%s=%s' % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) def searchGen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. @@ -312,29 +321,14 @@ class Twython(object): for tweet in content['results']: yield tweet - if 'page' not in kwargs: - kwargs['page'] = 2 - else: - try: - kwargs['page'] = int(kwargs['page']) - kwargs['page'] += 1 - kwargs['page'] = str(kwargs['page']) - except TypeError: - raise TwythonError("searchGen() exited because page takes type str") + try: + kwargs['page'] = 2 if not 'page' in kwargs else (int(kwargs['page']) + 1) + except (TypeError, ValueError): + raise TwythonError('Unable to generate next page of search results, `page` is not a number.') for tweet in self.searchGen(search_query, **kwargs): yield tweet - def bulkUserLookup(self, **kwargs): - """Stub for a method that has been deprecated, kept for now to raise errors - properly if people are relying on this (which they are...). - """ - warnings.warn( - "This function has been deprecated. Please migrate to .lookupUser() - params should be the same.", - DeprecationWarning, - stacklevel=2 - ) - # The following methods are apart from the other Account methods, # because they rely on a whole multipart-data posting function set. @@ -407,14 +401,6 @@ class Twython(object): ########################################################################### - def getProfileImageUrl(self, username, size='normal', version='1'): - warnings.warn( - "This function has been deprecated. Twitter API v1.1 will not have a dedicated endpoint \ - for this functionality.", - DeprecationWarning, - stacklevel=2 - ) - @staticmethod def stream(data, callback): """A Streaming API endpoint, because requests (by Kenneth Reitz) From 776e02b0715e9f059a1152593755ee6c0e834699 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 23 Apr 2013 19:43:05 -0400 Subject: [PATCH 369/687] Updated AUTHORS, HISTORY; added ssl_verify; removed _media_update - Added @jvanasco to the AUTHORS.rst - Updated History - Removed _media_update internal function - Twython now takes ssl_verify param --- AUTHORS.rst | 1 + HISTORY.rst | 4 +++- setup.py | 2 +- twython/twython.py | 46 +++++++++++++++++++++++++--------------------- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index b6a6dfb..2db7027 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -38,3 +38,4 @@ Patches and Suggestions - `Virendra Rajput `_, Fixed unicode (json) encoding in twython.py 2.7.2. - `Paul Solbach `_, fixed requirement for oauth_verifier - `Greg Nofi `_, fixed using built-in Exception attributes for storing & retrieving error message +- `Jonathan Vanasco `_, Debugging support, error_code tracking, Twitter error API tracking, other fixes diff --git a/HISTORY.rst b/HISTORY.rst index fc82b5e..99ce0b9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,7 @@ History ------- -2.8.0 (2013-xx-xx) +2.8.0 (2013-04-29) ++++++++++++++++++ - Added a ``HISTORY.rst`` to start tracking history of changes @@ -25,6 +25,8 @@ just define it - Removed `bulkUserLookup` (please use `lookupUser` instead), removed `getProfileImageUrl` (will be completely removed from Twitter API on May 7th, 2013) - Updated shortenUrl to actually work for those using it, but it is being deprecated since `requests` makes it easy for developers to implement their own url shortening in their app (see https://github.com/ryanmcgrath/twython/issues/184) - Twython Deprecation Warnings will now be seen in shell when using Python 2.7 and greater +- Twython now takes ``ssl_verify`` parameter, defaults True. Set False if you're having development server issues +- Removed internal ``_media_update`` function, we could have always just used ``self.post`` 2.7.3 (2013-04-12) ++++++++++++++++++ diff --git a/setup.py b/setup.py index 4ce9628..dc9e006 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['requests>=1.0.0, <2.0.0', 'requests_oauthlib==0.3.0'], + install_requires=['requests==1.2.0', 'requests_oauthlib==0.3.0'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index 19f2f68..5f0b631 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -14,8 +14,10 @@ warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > class Twython(object): - def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, - headers=None, proxies=None, version='1.1', callback_url=None, twitter_token=None, twitter_secret=None): + def __init__(self, app_key=None, app_secret=None, oauth_token=None, + oauth_token_secret=None, headers=None, proxies=None, + version='1.1', callback_url=None, ssl_verify=True, + twitter_token=None, twitter_secret=None): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -25,6 +27,7 @@ class Twython(object): :param headers: (optional) Custom headers to send along with the request :param callback_url: (optional) If set, will overwrite the callback url set in your application :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. + :param ssl_verify: (optional) Turns off ssl verification when False. Useful if you have development server issues. """ # API urls, OAuth urls and API version; needed for hitting that there API. @@ -76,6 +79,7 @@ class Twython(object): self.client.headers = self.headers self.client.proxies = proxies self.client.auth = self.auth + self.client.verify = ssl_verify # register available funcs to allow listing name when debugging. def setFunc(key): @@ -334,9 +338,6 @@ class Twython(object): ## Media Uploading functions ############################################## - def _media_update(self, url, file_, **params): - return self.post(url, params=params, files=file_) - def updateProfileBackgroundImage(self, file_, version='1.1', **params): """Updates the authenticating user's profile background image. @@ -348,10 +349,11 @@ class Twython(object): **params - You may pass items that are stated in this doc (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image) """ - url = 'https://api.twitter.com/%s/account/update_profile_background_image.json' % version - return self._media_update(url, - {'image': (file_, open(file_, 'rb'))}, - **params) + + return self.post('account/update_profile_background_image', + params=params, + files={'image': (file_, open(file_, 'rb'))}, + version=version) def updateProfileImage(self, file_, version='1.1', **params): """Updates the authenticating user's profile image (avatar). @@ -363,10 +365,11 @@ class Twython(object): **params - You may pass items that are stated in this doc (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image) """ - url = 'https://api.twitter.com/%s/account/update_profile_image.json' % version - return self._media_update(url, - {'image': (file_, open(file_, 'rb'))}, - **params) + + return self.post('account/update_profile_image', + params=params, + files={'image': (file_, open(file_, 'rb'))}, + version=version) def updateStatusWithMedia(self, file_, version='1.1', **params): """Updates the users status with media @@ -379,10 +382,10 @@ class Twython(object): (https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media) """ - url = 'https://api.twitter.com/%s/statuses/update_with_media.json' % version - return self._media_update(url, - {'media': (file_, open(file_, 'rb'))}, - **params) + return self.post('statuses/update_with_media', + params=params, + files={'media': (file_, open(file_, 'rb'))}, + version=version) def updateProfileBannerImage(self, file_, version='1.1', **params): """Updates the users profile banner @@ -394,10 +397,11 @@ class Twython(object): **params - You may pass items that are taken in this doc (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner) """ - url = 'https://api.twitter.com/%s/account/update_profile_banner.json' % version - return self._media_update(url, - {'banner': (file_, open(file_, 'rb'))}, - **params) + + return self.post('account/update_profile_banner', + params=params, + files={'banner': (file_, open(file_, 'rb'))}, + version=version) ########################################################################### From 32432bcac963bf76618e9a53da773cfd583850cd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 11:42:34 -0400 Subject: [PATCH 370/687] Update perms --- setup.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 setup.py diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 From b6e820d792759a3f67f352e84f60618b0a4b34ba Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 12:04:22 -0400 Subject: [PATCH 371/687] Remove version.py for now Was causing conflicts on pip install --- .gitignore | 3 ++- setup.py | 3 +-- twython/__init__.py | 1 + twython/twython.py | 2 +- twython/version.py | 1 - 5 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 twython/version.py diff --git a/.gitignore b/.gitignore index 5684153..4382a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ nosetests.xml # Mr Developer .mr.developer.cfg .project -.pydevproject \ No newline at end of file +.pydevproject +twython/.DS_Store diff --git a/setup.py b/setup.py index dc9e006..8b7702b 100755 --- a/setup.py +++ b/setup.py @@ -1,11 +1,10 @@ import os import sys -from twython.version import __version__ - from setuptools import setup __author__ = 'Ryan McGrath ' +__version__ = '2.8.0' packages = [ 'twython' diff --git a/twython/__init__.py b/twython/__init__.py index fc493ae..76ce26c 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,6 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' +__version__ = '2.8.0' from .twython import Twython from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError diff --git a/twython/twython.py b/twython/twython.py index 5f0b631..1297436 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -4,11 +4,11 @@ import warnings import requests from requests_oauthlib import OAuth1 +from . import __version__ from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError -from .version import __version__ warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > diff --git a/twython/version.py b/twython/version.py deleted file mode 100644 index f2df444..0000000 --- a/twython/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '2.8.0' From e18bff97d3299b66f4a4208a123c89aa021ceba2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 14:30:30 -0300 Subject: [PATCH 372/687] Update HISTORY.rst --- HISTORY.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 99ce0b9..76b4c86 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,11 +10,9 @@ History - Added ``compat.py`` for compatability with Python 2.6 and greater - Added some ascii art, moved description of Twython and ``__author__`` to ``__init__.py`` - Added ``version.py`` to store the current Twython version, instead of repeating it twice -- it also had to go into it's own file because of dependencies of ``requests`` and ``requests-oauthlib``, install would fail because those libraries weren't installed yet (on fresh install of Twython) -- Removed ``find_packages()`` from ``setup.py``, only one package -- we can -just define it +- Removed ``find_packages()`` from ``setup.py``, only one package (we can just define it) - added quick publish method for Ryan and I: ``python setup.py publish`` is faster to type and easier to remember than ``python setup.py sdist upload`` -- Removed ``base_url`` from ``endpoints.py`` because we're just repeating it in -``Twython.__init__`` +- Removed ``base_url`` from ``endpoints.py`` because we're just repeating it in ``Twython.__init__`` - ``Twython.get_authentication_tokens()`` now takes ``callback_url`` argument rather than passing the ``callback_url`` through ``Twython.__init__``, ``callback_url`` is only used in the ``get_authentication_tokens`` method and nowhere else (kept in init though for backwards compatability) - Updated README to better reflect current Twython codebase - Added ``warnings.simplefilter('default')`` line in ``twython.py`` for Python 2.7 and greater to display Deprecation Warnings in console @@ -60,4 +58,4 @@ just define it 2.6.0 (2013-03-29) ++++++++++++++++++ -- Updated ``twitter_endpoints.py`` to better reflect order of API endpoints on the Twitter API v1.1 docs site \ No newline at end of file +- Updated ``twitter_endpoints.py`` to better reflect order of API endpoints on the Twitter API v1.1 docs site From c3e84bc8eee44f61e4b255e1642592bbcd0faf49 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 2 May 2013 15:12:36 -0400 Subject: [PATCH 373/687] Fixing streaming --- .gitignore | 2 + HISTORY.rst | 5 + README.md | 27 ++--- README.rst | 29 +++--- examples/stream.py | 20 ++++ setup.py | 2 +- twython/__init__.py | 3 +- twython/exceptions.py | 5 + twython/streaming.py | 224 ++++++++++++++++++++++++++++++++++++++++++ twython/twython.py | 54 ---------- 10 files changed, 290 insertions(+), 81 deletions(-) create mode 100644 examples/stream.py create mode 100644 twython/streaming.py diff --git a/.gitignore b/.gitignore index 4382a8d..7146bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ nosetests.xml .project .pydevproject twython/.DS_Store + +test.py diff --git a/HISTORY.rst b/HISTORY.rst index 76b4c86..3a2853c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ History ------- +2.9.0 (2013-05-xx) +++++++++++++++++++ + +- Fixed streaming issue #144, added ``TwythonStreamer`` and ``TwythonStreamHandler`` to aid users in a friendly streaming experience + 2.8.0 (2013-04-29) ++++++++++++++++++ diff --git a/README.md b/README.md index 38205f9..6d8bdc2 100644 --- a/README.md +++ b/README.md @@ -103,23 +103,26 @@ except TwythonAuthError as e: ``` ##### Streaming API -*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.* ```python -from twython import Twython +from twython import TwythonStreamer, TwythonStreamHandler -def on_results(results): - """A callback to handle passed results. Wheeee. - """ - print results +class MyHandler(TwythonStreamHandler): + def on_success(self, data): + print data -Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' -}, on_results) + def on_error(self, status_code, data): + print status_code, data + +handler = MyHandler() + +# Requires Authentication as of Twitter API v1.1 +stream = TwythonStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, + handler) + +stream.statuses.filter(track='twitter') ``` Notes diff --git a/README.rst b/README.rst index 3b0117f..9c5f58e 100644 --- a/README.rst +++ b/README.rst @@ -111,24 +111,27 @@ Catching exceptions Streaming API ~~~~~~~~~~~~~ -*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.* :: - from twython import Twython - - def on_results(results): - """A callback to handle passed results. Wheeee. - """ + from twython import TwythonStreamer, TwythonStreamHandler - print results - Twython.stream({ - 'username': 'your_username', - 'password': 'your_password', - 'track': 'python' - }, on_results) + class MyHandler(TwythonStreamHandler): + def on_success(self, data): + print data + + def on_error(self, status_code, data): + print status_code, data + + handler = MyHandler() + + # Requires Authentication as of Twitter API v1.1 + stream = TwythonStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, + handler) + + stream.statuses.filter(track='twitter') Notes diff --git a/examples/stream.py b/examples/stream.py new file mode 100644 index 0000000..0fee30c --- /dev/null +++ b/examples/stream.py @@ -0,0 +1,20 @@ +from twython import TwythonStreamer, TwythonStreamHandler + + +class MyHandler(TwythonStreamHandler): + def on_success(self, data): + print data + + def on_error(self, status_code, data): + print status_code, data + +handler = MyHandler() + +# Requires Authentication as of Twitter API v1.1 +stream = TwythonStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET, + handler) + +stream.statuses.filter(track='twitter') +#stream.user(track='twitter') +#stream.site(follow='twitter') diff --git a/setup.py b/setup.py index 8b7702b..14c5439 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '2.8.0' +__version__ = '2.9.0' packages = [ 'twython' diff --git a/twython/__init__.py b/twython/__init__.py index 76ce26c..481750e 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,8 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '2.8.0' +__version__ = '2.9.0' from .twython import Twython +from .streaming import TwythonStreamer, TwythonStreamHandler from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError diff --git a/twython/exceptions.py b/twython/exceptions.py index 17736e4..265356a 100644 --- a/twython/exceptions.py +++ b/twython/exceptions.py @@ -46,3 +46,8 @@ class TwythonRateLimitError(TwythonError): if isinstance(retry_after, int): msg = '%s (Retry after %d seconds)' % (msg, retry_after) TwythonError.__init__(self, msg, error_code=error_code) + + +class TwythonStreamError(TwythonError): + """Test""" + pass diff --git a/twython/streaming.py b/twython/streaming.py new file mode 100644 index 0000000..0666367 --- /dev/null +++ b/twython/streaming.py @@ -0,0 +1,224 @@ +from . import __version__ +from .compat import json +from .exceptions import TwythonStreamError + +import requests +from requests_oauthlib import OAuth1 + +import time + + +class TwythonStreamHandler(object): + def on_success(self, data): + """Called when data has been successfull received from the stream + + Feel free to override this in your own handler. + See https://dev.twitter.com/docs/streaming-apis/messages for messages + sent along in stream responses. + + :param data: dict of data recieved from the stream + """ + + if 'delete' in data: + self.on_delete(data.get('delete')) + elif 'limit' in data: + self.on_limit(data.get('limit')) + elif 'disconnect' in data: + self.on_disconnect(data.get('disconnect')) + + def on_error(self, status_code, data): + """Called when stream returns non-200 status code + + :param status_code: Non-200 status code sent from stream + :param data: Error message sent from stream + """ + return + + def on_delete(self, data): + """Called when a deletion notice is received + + Twitter docs for deletion notices: http://spen.se/8qujd + + :param data: dict of data from the 'delete' key recieved from + the stream + """ + return data + + def on_limit(self, data): + """Called when a limit notice is received + + Twitter docs for limit notices: http://spen.se/hzt0b + + :param data: dict of data from the 'limit' key recieved from + the stream + """ + return data + + def on_disconnect(self, data): + """Called when a disconnect notice is received + + Twitter docs for disconnect notices: http://spen.se/xb6mm + + :param data: dict of data from the 'disconnect' key recieved from + the stream + """ + return data + + def on_timeout(self): + return + + +class TwythonStreamStatuses(object): + """Class for different statuses endpoints + + Available so TwythonStreamer.statuses.filter() is available. + Just a bit cleaner than TwythonStreamer.statuses_filter(), + statuses_sample(), etc. all being single methods in TwythonStreamer + """ + def __init__(self, streamer): + self.streamer = streamer + + def filter(self, **params): + """Stream statuses/filter + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/post/statuses/filter + """ + url = 'https://stream.twitter.com/%s/statuses/filter.json' \ + % self.streamer.api_version + self.streamer._request(url, 'POST', params=params) + + def sample(self, **params): + """Stream statuses/sample + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/statuses/sample + """ + url = 'https://stream.twitter.com/%s/statuses/sample.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + def firehose(self, **params): + """Stream statuses/filter + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/statuses/firehose + """ + url = 'https://stream.twitter.com/%s/statuses/firehose.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + +class TwythonStreamTypes(object): + """Class for different stream endpoints + + Not all streaming endpoints have nested endpoints. + User Streams and Site Streams are single streams with no nested endpoints + Status Streams include filter, sample and firehose endpoints + """ + def __init__(self, streamer): + self.streamer = streamer + self.statuses = TwythonStreamStatuses(streamer) + + def user(self, **params): + """Stream user + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/user + """ + url = 'https://userstream.twitter.com/%s/user.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + def site(self, **params): + """Stream site + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/site + """ + url = 'https://sitestream.twitter.com/%s/site.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + +class TwythonStreamer(object): + def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, + handler, timeout=300, retry_count=None, retry_in=10, + headers=None): + """Streaming class for a friendly streaming user experience + + :param app_key: (required) Your applications key + :param app_secret: (required) Your applications secret key + :param oauth_token: (required) Used with oauth_token_secret to make + authenticated calls + :param oauth_token_secret: (required) Used with oauth_token to make + authenticated calls + :param handler: (required) Instance of TwythonStreamHandler to handle + stream responses + :param headers: (optional) Custom headers to send along with the + request + """ + + self.auth = OAuth1(app_key, app_secret, + oauth_token, oauth_token_secret) + + self.headers = {'User-Agent': 'Twython Streaming v' + __version__} + if headers: + self.headers.update(headers) + + self.client = requests.Session() + self.client.auth = self.auth + self.client.headers = self.headers + self.client.stream = True + + self.timeout = timeout + + self.api_version = '1.1' + + self.handler = handler + + self.retry_in = retry_in + self.retry_count = retry_count + + # Set up type methods + StreamTypes = TwythonStreamTypes(self) + self.statuses = StreamTypes.statuses + self.__dict__['user'] = StreamTypes.user + self.__dict__['site'] = StreamTypes.site + + def _request(self, url, method='GET', params=None): + """Internal stream request handling""" + retry_counter = 0 + + method = method.lower() + func = getattr(self.client, method) + + def _send(retry_counter): + try: + if method == 'get': + response = func(url, params=params, timeout=self.timeout) + else: + response = func(url, data=params, timeout=self.timeout) + except requests.exceptions.Timeout: + self.handler.on_timeout() + else: + if response.status_code != 200: + self.handler.on_error(response.status_code, + response.content) + + if self.retry_count and (self.retry_count - retry_counter) > 0: + time.sleep(self.retry_in) + retry_counter += 1 + _send(retry_counter) + + return response + + response = _send(retry_counter) + + for line in response.iter_lines(): + if line: + try: + self.handler.on_success(json.loads(line)) + except ValueError: + raise TwythonStreamError('Response was not valid JSON, \ + unable to decode.') diff --git a/twython/twython.py b/twython/twython.py index 1297436..7e1cbf1 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -405,60 +405,6 @@ class Twython(object): ########################################################################### - @staticmethod - def stream(data, callback): - """A Streaming API endpoint, because requests (by Kenneth Reitz) - makes this not stupidly annoying to implement. - - In reality, Twython does absolutely *nothing special* here, - but people new to programming expect this type of function to - exist for this library, so we provide it for convenience. - - Seriously, this is nothing special. :) - - For the basic stream you're probably accessing, you'll want to - pass the following as data dictionary keys. If you need to use - OAuth (newer streams), passing secrets/etc - as keys SHOULD work... - - This is all done over SSL (https://), so you're not left - totally vulnerable by passing your password. - - :param username: (required) Username, self explanatory. - :param password: (required) The Streaming API doesn't use OAuth, - so we do this the old school way. - :param callback: (required) Callback function to be fired when - tweets come in (this is an event-based-ish API). - :param endpoint: (optional) Override the endpoint you're using - with the Twitter Streaming API. This is defaulted - to the one that everyone has access to, but if - Twitter <3's you feel free to set this to your - wildest desires. - """ - endpoint = 'https://stream.twitter.com/1/statuses/filter.json' - if 'endpoint' in data: - endpoint = data.pop('endpoint') - - needs_basic_auth = False - if 'username' in data and 'password' in data: - needs_basic_auth = True - username = data.pop('username') - password = data.pop('password') - - if needs_basic_auth: - stream = requests.post(endpoint, - data=data, - auth=(username, password)) - else: - stream = requests.post(endpoint, data=data) - - for line in stream.iter_lines(): - if line: - try: - callback(json.loads(line)) - except ValueError: - raise TwythonError('Response was not valid JSON, unable to decode.') - @staticmethod def unicode2utf8(text): try: From 97a33ce8dd5cb32334d454889d44ed1d828622bd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 3 May 2013 16:57:59 -0400 Subject: [PATCH 374/687] Merge Handling into TwythonStreamer, update examples --- README.md | 11 ++-- README.rst | 11 ++-- examples/stream.py | 11 ++-- twython/__init__.py | 2 +- twython/streaming.py | 151 +++++++++++++++++++++++-------------------- 5 files changed, 94 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 6d8bdc2..3504336 100644 --- a/README.md +++ b/README.md @@ -105,22 +105,19 @@ except TwythonAuthError as e: ##### Streaming API ```python -from twython import TwythonStreamer, TwythonStreamHandler +from twython import TwythonStreamer -class MyHandler(TwythonStreamHandler): +class MyStreamer(TwythonStreamer): def on_success(self, data): print data def on_error(self, status_code, data): print status_code, data -handler = MyHandler() - # Requires Authentication as of Twitter API v1.1 -stream = TwythonStreamer(APP_KEY, APP_SECRET, - OAUTH_TOKEN, OAUTH_TOKEN_SECRET, - handler) +stream = MyStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) stream.statuses.filter(track='twitter') ``` diff --git a/README.rst b/README.rst index 9c5f58e..ab806cb 100644 --- a/README.rst +++ b/README.rst @@ -114,22 +114,19 @@ Streaming API :: - from twython import TwythonStreamer, TwythonStreamHandler + from twython import TwythonStreamer - class MyHandler(TwythonStreamHandler): + class MyStreamer(TwythonStreamer): def on_success(self, data): print data def on_error(self, status_code, data): print status_code, data - handler = MyHandler() - # Requires Authentication as of Twitter API v1.1 - stream = TwythonStreamer(APP_KEY, APP_SECRET, - OAUTH_TOKEN, OAUTH_TOKEN_SECRET, - handler) + stream = MyStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) stream.statuses.filter(track='twitter') diff --git a/examples/stream.py b/examples/stream.py index 0fee30c..f5c5f1a 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -1,19 +1,16 @@ -from twython import TwythonStreamer, TwythonStreamHandler +from twython import TwythonStreamer -class MyHandler(TwythonStreamHandler): +class MyStreamer(TwythonStreamer): def on_success(self, data): print data def on_error(self, status_code, data): print status_code, data -handler = MyHandler() - # Requires Authentication as of Twitter API v1.1 -stream = TwythonStreamer(APP_KEY, APP_SECRET, - OAUTH_TOKEN, OAUTH_TOKEN_SECRET, - handler) +stream = MyStreamer(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) stream.statuses.filter(track='twitter') #stream.user(track='twitter') diff --git a/twython/__init__.py b/twython/__init__.py index 481750e..23f2eef 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -21,5 +21,5 @@ __author__ = 'Ryan McGrath ' __version__ = '2.9.0' from .twython import Twython -from .streaming import TwythonStreamer, TwythonStreamHandler +from .streaming import TwythonStreamer from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError diff --git a/twython/streaming.py b/twython/streaming.py index 0666367..2c04cd3 100644 --- a/twython/streaming.py +++ b/twython/streaming.py @@ -8,66 +8,6 @@ from requests_oauthlib import OAuth1 import time -class TwythonStreamHandler(object): - def on_success(self, data): - """Called when data has been successfull received from the stream - - Feel free to override this in your own handler. - See https://dev.twitter.com/docs/streaming-apis/messages for messages - sent along in stream responses. - - :param data: dict of data recieved from the stream - """ - - if 'delete' in data: - self.on_delete(data.get('delete')) - elif 'limit' in data: - self.on_limit(data.get('limit')) - elif 'disconnect' in data: - self.on_disconnect(data.get('disconnect')) - - def on_error(self, status_code, data): - """Called when stream returns non-200 status code - - :param status_code: Non-200 status code sent from stream - :param data: Error message sent from stream - """ - return - - def on_delete(self, data): - """Called when a deletion notice is received - - Twitter docs for deletion notices: http://spen.se/8qujd - - :param data: dict of data from the 'delete' key recieved from - the stream - """ - return data - - def on_limit(self, data): - """Called when a limit notice is received - - Twitter docs for limit notices: http://spen.se/hzt0b - - :param data: dict of data from the 'limit' key recieved from - the stream - """ - return data - - def on_disconnect(self, data): - """Called when a disconnect notice is received - - Twitter docs for disconnect notices: http://spen.se/xb6mm - - :param data: dict of data from the 'disconnect' key recieved from - the stream - """ - return data - - def on_timeout(self): - return - - class TwythonStreamStatuses(object): """Class for different statuses endpoints @@ -143,8 +83,7 @@ class TwythonStreamTypes(object): class TwythonStreamer(object): def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, - handler, timeout=300, retry_count=None, retry_in=10, - headers=None): + timeout=300, retry_count=None, retry_in=10, headers=None): """Streaming class for a friendly streaming user experience :param app_key: (required) Your applications key @@ -153,8 +92,12 @@ class TwythonStreamer(object): authenticated calls :param oauth_token_secret: (required) Used with oauth_token to make authenticated calls - :param handler: (required) Instance of TwythonStreamHandler to handle - stream responses + :param timeout: (optional) How long (in secs) the streamer should wait + for a response from Twitter Streaming API + :param retry_count: (optional) Number of times the API call should be + retired + :param retry_in: (optional) Amount of time (in secs) the previous + API call should be tried again :param headers: (optional) Custom headers to send along with the request """ @@ -175,8 +118,6 @@ class TwythonStreamer(object): self.api_version = '1.1' - self.handler = handler - self.retry_in = retry_in self.retry_count = retry_count @@ -200,11 +141,10 @@ class TwythonStreamer(object): else: response = func(url, data=params, timeout=self.timeout) except requests.exceptions.Timeout: - self.handler.on_timeout() + self.on_timeout() else: if response.status_code != 200: - self.handler.on_error(response.status_code, - response.content) + self.on_error(response.status_code, response.content) if self.retry_count and (self.retry_count - retry_counter) > 0: time.sleep(self.retry_in) @@ -218,7 +158,78 @@ class TwythonStreamer(object): for line in response.iter_lines(): if line: try: - self.handler.on_success(json.loads(line)) + self.on_success(json.loads(line)) except ValueError: raise TwythonStreamError('Response was not valid JSON, \ unable to decode.') + + def on_success(self, data): + """Called when data has been successfull received from the stream + + Feel free to override this to handle your streaming data how you + want it handled. + See https://dev.twitter.com/docs/streaming-apis/messages for messages + sent along in stream responses. + + :param data: dict of data recieved from the stream + """ + + if 'delete' in data: + self.on_delete(data.get('delete')) + elif 'limit' in data: + self.on_limit(data.get('limit')) + elif 'disconnect' in data: + self.on_disconnect(data.get('disconnect')) + + def on_error(self, status_code, data): + """Called when stream returns non-200 status code + + Feel free to override this to handle your streaming data how you + want it handled. + + :param status_code: Non-200 status code sent from stream + :param data: Error message sent from stream + """ + return + + def on_delete(self, data): + """Called when a deletion notice is received + + Feel free to override this to handle your streaming data how you + want it handled. + + Twitter docs for deletion notices: http://spen.se/8qujd + + :param data: dict of data from the 'delete' key recieved from + the stream + """ + return + + def on_limit(self, data): + """Called when a limit notice is received + + Feel free to override this to handle your streaming data how you + want it handled. + + Twitter docs for limit notices: http://spen.se/hzt0b + + :param data: dict of data from the 'limit' key recieved from + the stream + """ + return + + def on_disconnect(self, data): + """Called when a disconnect notice is received + + Feel free to override this to handle your streaming data how you + want it handled. + + Twitter docs for disconnect notices: http://spen.se/xb6mm + + :param data: dict of data from the 'disconnect' key recieved from + the stream + """ + return + + def on_timeout(self): + return From cea0852a42200553a8dc5d04db7c7ab919d49475 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 3 May 2013 17:33:17 -0400 Subject: [PATCH 375/687] Updating file structure and HISTORY.rst --- HISTORY.rst | 4 +- twython/streaming/__init__.py | 1 + twython/{streaming.py => streaming/api.py} | 86 ++-------------------- twython/streaming/types.py | 71 ++++++++++++++++++ 4 files changed, 81 insertions(+), 81 deletions(-) create mode 100644 twython/streaming/__init__.py rename twython/{streaming.py => streaming/api.py} (66%) create mode 100644 twython/streaming/types.py diff --git a/HISTORY.rst b/HISTORY.rst index 3a2853c..a0df760 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,10 +1,10 @@ History ------- -2.9.0 (2013-05-xx) +2.9.0 (2013-05-04) ++++++++++++++++++ -- Fixed streaming issue #144, added ``TwythonStreamer`` and ``TwythonStreamHandler`` to aid users in a friendly streaming experience +- Fixed streaming issue #144, added ``TwythonStreamer`` to aid users in a friendly streaming experience (streaming examples in ``examples`` and README's have been updated as well) 2.8.0 (2013-04-29) ++++++++++++++++++ diff --git a/twython/streaming/__init__.py b/twython/streaming/__init__.py new file mode 100644 index 0000000..b12e8d1 --- /dev/null +++ b/twython/streaming/__init__.py @@ -0,0 +1 @@ +from .api import TwythonStreamer diff --git a/twython/streaming.py b/twython/streaming/api.py similarity index 66% rename from twython/streaming.py rename to twython/streaming/api.py index 2c04cd3..541aa07 100644 --- a/twython/streaming.py +++ b/twython/streaming/api.py @@ -1,6 +1,7 @@ -from . import __version__ -from .compat import json -from .exceptions import TwythonStreamError +from .. import __version__ +from ..compat import json +from ..exceptions import TwythonStreamError +from .types import TwythonStreamerTypes import requests from requests_oauthlib import OAuth1 @@ -8,79 +9,6 @@ from requests_oauthlib import OAuth1 import time -class TwythonStreamStatuses(object): - """Class for different statuses endpoints - - Available so TwythonStreamer.statuses.filter() is available. - Just a bit cleaner than TwythonStreamer.statuses_filter(), - statuses_sample(), etc. all being single methods in TwythonStreamer - """ - def __init__(self, streamer): - self.streamer = streamer - - def filter(self, **params): - """Stream statuses/filter - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/post/statuses/filter - """ - url = 'https://stream.twitter.com/%s/statuses/filter.json' \ - % self.streamer.api_version - self.streamer._request(url, 'POST', params=params) - - def sample(self, **params): - """Stream statuses/sample - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/statuses/sample - """ - url = 'https://stream.twitter.com/%s/statuses/sample.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def firehose(self, **params): - """Stream statuses/filter - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/statuses/firehose - """ - url = 'https://stream.twitter.com/%s/statuses/firehose.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - -class TwythonStreamTypes(object): - """Class for different stream endpoints - - Not all streaming endpoints have nested endpoints. - User Streams and Site Streams are single streams with no nested endpoints - Status Streams include filter, sample and firehose endpoints - """ - def __init__(self, streamer): - self.streamer = streamer - self.statuses = TwythonStreamStatuses(streamer) - - def user(self, **params): - """Stream user - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/user - """ - url = 'https://userstream.twitter.com/%s/user.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - def site(self, **params): - """Stream site - - Accepted params found at: - https://dev.twitter.com/docs/api/1.1/get/site - """ - url = 'https://sitestream.twitter.com/%s/site.json' \ - % self.streamer.api_version - self.streamer._request(url, params=params) - - class TwythonStreamer(object): def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, timeout=300, retry_count=None, retry_in=10, headers=None): @@ -122,10 +50,10 @@ class TwythonStreamer(object): self.retry_count = retry_count # Set up type methods - StreamTypes = TwythonStreamTypes(self) + StreamTypes = TwythonStreamerTypes(self) self.statuses = StreamTypes.statuses - self.__dict__['user'] = StreamTypes.user - self.__dict__['site'] = StreamTypes.site + self.user = StreamTypes.user + self.site = StreamTypes.site def _request(self, url, method='GET', params=None): """Internal stream request handling""" diff --git a/twython/streaming/types.py b/twython/streaming/types.py new file mode 100644 index 0000000..fd02f81 --- /dev/null +++ b/twython/streaming/types.py @@ -0,0 +1,71 @@ +class TwythonStreamerTypes(object): + """Class for different stream endpoints + + Not all streaming endpoints have nested endpoints. + User Streams and Site Streams are single streams with no nested endpoints + Status Streams include filter, sample and firehose endpoints + """ + def __init__(self, streamer): + self.streamer = streamer + self.statuses = TwythonStreamerTypesStatuses(streamer) + + def user(self, **params): + """Stream user + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/user + """ + url = 'https://userstream.twitter.com/%s/user.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + def site(self, **params): + """Stream site + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/site + """ + url = 'https://sitestream.twitter.com/%s/site.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + +class TwythonStreamerTypesStatuses(object): + """Class for different statuses endpoints + + Available so TwythonStreamer.statuses.filter() is available. + Just a bit cleaner than TwythonStreamer.statuses_filter(), + statuses_sample(), etc. all being single methods in TwythonStreamer + """ + def __init__(self, streamer): + self.streamer = streamer + + def filter(self, **params): + """Stream statuses/filter + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/post/statuses/filter + """ + url = 'https://stream.twitter.com/%s/statuses/filter.json' \ + % self.streamer.api_version + self.streamer._request(url, 'POST', params=params) + + def sample(self, **params): + """Stream statuses/sample + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/statuses/sample + """ + url = 'https://stream.twitter.com/%s/statuses/sample.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) + + def firehose(self, **params): + """Stream statuses/filter + + Accepted params found at: + https://dev.twitter.com/docs/api/1.1/get/statuses/firehose + """ + url = 'https://stream.twitter.com/%s/statuses/firehose.json' \ + % self.streamer.api_version + self.streamer._request(url, params=params) From b7d68c61365f17d794ac5c770bc4b54e20b771fb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 15:12:21 -0400 Subject: [PATCH 376/687] requests_oauthlib==0.3.1, fixes #154 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14c5439..1281882 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['requests==1.2.0', 'requests_oauthlib==0.3.0'], + install_requires=['requests==1.2.0', 'requests_oauthlib==0.3.1'], # Metadata for PyPI. author='Ryan McGrath', From 0e258fe1a19dd40ce5150d1e4fc6fb49580ed9cb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 15:13:29 -0400 Subject: [PATCH 377/687] Update HISTORY --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index a0df760..607f9ee 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ History ++++++++++++++++++ - Fixed streaming issue #144, added ``TwythonStreamer`` to aid users in a friendly streaming experience (streaming examples in ``examples`` and README's have been updated as well) +- ``Twython`` now requires ``requests-oauthlib`` 0.3.1, fixes #154 (unable to upload media when sending POST data with the file) 2.8.0 (2013-04-29) ++++++++++++++++++ From 84e4c5fe138a002c819a0a048b02fad2a0120046 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:28:47 -0400 Subject: [PATCH 378/687] Update all endpoints in the api_table, examples, READMEs Also updated functions in twython.py to use pep8 functions instead of camelCase functions --- HISTORY.rst | 5 + README.md | 4 +- README.rst | 4 +- examples/get_user_timeline.py | 2 +- examples/search_results.py | 8 +- examples/shorten_url.py | 2 +- examples/update_profile_image.py | 2 +- examples/update_status.py | 2 +- twython/endpoints.py | 162 +++++++++++++++---------------- twython/twython.py | 72 +++++++++++++- 10 files changed, 167 insertions(+), 96 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 607f9ee..a044dcf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,11 @@ History ------- +2.9.1 (2013-05-04) +++++++++++++++++++ + +- "PEP8" all the functions. Switch functions from camelCase() to underscore_funcs(). (i.e. ``updateStatus()`` is now ``update_status()``) + 2.9.0 (2013-05-04) ++++++++++++++++++ diff --git a/README.md b/README.md index 3504336..a9b2052 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ t = Twython(app_key, app_secret, oauth_token, oauth_token_secret) # Returns an dict of the user home timeline -print t.getHomeTimeline() +print t.get_home_timeline() ``` ##### Catching exceptions @@ -97,7 +97,7 @@ t = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET, BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET) try: - t.verifyCredentials() + t.verify_credentials() except TwythonAuthError as e: print e ``` diff --git a/README.rst b/README.rst index ab806cb..4f15fe5 100644 --- a/README.rst +++ b/README.rst @@ -88,7 +88,7 @@ Getting a user home timeline oauth_token, oauth_token_secret) # Returns an dict of the user home timeline - print t.getHomeTimeline() + print t.get_home_timeline() Catching exceptions @@ -104,7 +104,7 @@ Catching exceptions BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET) try: - t.verifyCredentials() + t.verify_credentials() except TwythonAuthError as e: print e diff --git a/examples/get_user_timeline.py b/examples/get_user_timeline.py index 19fb031..0a1f4df 100644 --- a/examples/get_user_timeline.py +++ b/examples/get_user_timeline.py @@ -3,7 +3,7 @@ from twython import Twython, TwythonError # Requires Authentication as of Twitter API v1.1 twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) try: - user_timeline = twitter.getUserTimeline(screen_name='ryanmcgrath') + user_timeline = twitter.get_user_timeline(screen_name='ryanmcgrath') except TwythonError as e: print e diff --git a/examples/search_results.py b/examples/search_results.py index 6d4dc7c..96109a4 100644 --- a/examples/search_results.py +++ b/examples/search_results.py @@ -3,10 +3,10 @@ from twython import Twython, TwythonError # Requires Authentication as of Twitter API v1.1 twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) try: - search_results = twitter.search(q="WebsDotCom", rpp="50") + search_results = twitter.search(q='WebsDotCom', rpp='50') except TwythonError as e: print e -for tweet in search_results["results"]: - print "Tweet from @%s Date: %s" % (tweet['from_user'].encode('utf-8'),tweet['created_at']) - print tweet['text'].encode('utf-8'),"\n" \ No newline at end of file +for tweet in search_results['results']: + print 'Tweet from @%s Date: %s' % (tweet['from_user'].encode('utf-8'), tweet['created_at']) + print tweet['text'].encode('utf-8'), '\n' diff --git a/examples/shorten_url.py b/examples/shorten_url.py index 42d0f40..61eb105 100644 --- a/examples/shorten_url.py +++ b/examples/shorten_url.py @@ -1,6 +1,6 @@ from twython import Twython # Shortening URLs requires no authentication, huzzah -shortURL = Twython.shortenURL('http://www.webs.com/') +shortURL = Twython.shorten_url('http://www.webs.com/') print shortURL diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py index 1bb9782..9921f0e 100644 --- a/examples/update_profile_image.py +++ b/examples/update_profile_image.py @@ -2,4 +2,4 @@ from twython import Twython # Requires Authentication as of Twitter API v1.1 twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) -twitter.updateProfileImage('myImage.png') +twitter.update_profile_image('myImage.png') diff --git a/examples/update_status.py b/examples/update_status.py index 1ecfcba..1acf174 100644 --- a/examples/update_status.py +++ b/examples/update_status.py @@ -4,6 +4,6 @@ from twython import Twython, TwythonError twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) try: - twitter.updateStatus(status='See how easy this was?') + twitter.update_status(status='See how easy this was?') except TwythonError as e: print e diff --git a/twython/endpoints.py b/twython/endpoints.py index d63fc03..365a0a2 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -17,38 +17,38 @@ https://dev.twitter.com/docs/api/1.1 api_table = { # Timelines - 'getMentionsTimeline': { + 'get_mentions_timeline': { 'url': '/statuses/mentions_timeline.json', 'method': 'GET', }, - 'getUserTimeline': { + 'get_user_timeline': { 'url': '/statuses/user_timeline.json', 'method': 'GET', }, - 'getHomeTimeline': { + 'get_home_timeline': { 'url': '/statuses/home_timeline.json', 'method': 'GET', }, - 'retweetedOfMe': { + 'retweeted_of_me': { 'url': '/statuses/retweets_of_me.json', 'method': 'GET', }, # Tweets - 'getRetweets': { + 'get_retweets': { 'url': '/statuses/retweets/{{id}}.json', 'method': 'GET', }, - 'showStatus': { + 'show_status': { 'url': '/statuses/show/{{id}}.json', 'method': 'GET', }, - 'destroyStatus': { + 'destroy_status': { 'url': '/statuses/destroy/{{id}}.json', 'method': 'POST', }, - 'updateStatus': { + 'update_status': { 'url': '/statuses/update.json', 'method': 'POST', }, @@ -57,7 +57,7 @@ api_table = { 'method': 'POST', }, # See twython.py for update_status_with_media - 'getOembedTweet': { + 'get_oembed_tweet': { 'url': '/statuses/oembed.json', 'method': 'GET', }, @@ -71,321 +71,321 @@ api_table = { # Direct Messages - 'getDirectMessages': { + 'get_direct_messages': { 'url': '/direct_messages.json', 'method': 'GET', }, - 'getSentMessages': { + 'get_sent_messages': { 'url': '/direct_messages/sent.json', 'method': 'GET', }, - 'getDirectMessage': { + 'get_direct_message': { 'url': '/direct_messages/show.json', 'method': 'GET', }, - 'destroyDirectMessage': { + 'destroy_direct_message': { 'url': '/direct_messages/destroy.json', 'method': 'POST', }, - 'sendDirectMessage': { + 'send_direct_message': { 'url': '/direct_messages/new.json', 'method': 'POST', }, # Friends & Followers - 'getUserIdsOfBlockedRetweets': { + 'get_user_ids_of_blocked_retweets': { 'url': '/friendships/no_retweets/ids.json', 'method': 'GET', }, - 'getFriendsIDs': { + 'get_friends_ids': { 'url': '/friends/ids.json', 'method': 'GET', }, - 'getFollowersIDs': { + 'get_followers_ids': { 'url': '/followers/ids.json', 'method': 'GET', }, - 'lookupFriendships': { + 'lookup_friendships': { 'url': '/friendships/lookup.json', 'method': 'GET', }, - 'getIncomingFriendshipIDs': { + 'get_incoming_friendship_ids': { 'url': '/friendships/incoming.json', 'method': 'GET', }, - 'getOutgoingFriendshipIDs': { + 'get_outgoing_friendship_ids': { 'url': '/friendships/outgoing.json', 'method': 'GET', }, - 'createFriendship': { + 'create_friendship': { 'url': '/friendships/create.json', 'method': 'POST', }, - 'destroyFriendship': { + 'destroy_friendship': { 'url': '/friendships/destroy.json', 'method': 'POST', }, - 'updateFriendship': { + 'update_friendship': { 'url': '/friendships/update.json', 'method': 'POST', }, - 'showFriendship': { + 'show_friendship': { 'url': '/friendships/show.json', 'method': 'GET', }, - 'getFriendsList': { + 'get_friends_list': { 'url': '/friends/list.json', 'method': 'GET', }, - 'getFollowersList': { + 'get_followers_list': { 'url': '/followers/list.json', 'method': 'GET', }, # Users - 'getAccountSettings': { + 'get_account_settings': { 'url': '/account/settings.json', 'method': 'GET', }, - 'verifyCredentials': { + 'verify_credentials': { 'url': '/account/verify_credentials.json', 'method': 'GET', }, - 'updateAccountSettings': { + 'update_account_settings': { 'url': '/account/settings.json', 'method': 'POST', }, - 'updateDeliveryService': { + 'update_delivery_service': { 'url': '/account/update_delivery_device.json', 'method': 'POST', }, - 'updateProfile': { + 'update_profile': { 'url': '/account/update_profile.json', 'method': 'POST', }, # See twython.py for update_profile_background_image - 'updateProfileColors': { + 'update_profile_colors': { 'url': '/account/update_profile_colors.json', 'method': 'POST', }, # See twython.py for update_profile_image - 'listBlocks': { + 'list_blocks': { 'url': '/blocks/list.json', 'method': 'GET', }, - 'listBlockIds': { + 'list_block_ids': { 'url': '/blocks/ids.json', 'method': 'GET', }, - 'createBlock': { + 'create_block': { 'url': '/blocks/create.json', 'method': 'POST', }, - 'destroyBlock': { + 'destroy_block': { 'url': '/blocks/destroy.json', 'method': 'POST', }, - 'lookupUser': { + 'lookup_user': { 'url': '/users/lookup.json', 'method': 'GET', }, - 'showUser': { + 'show_user': { 'url': '/users/show.json', 'method': 'GET', }, - 'searchUsers': { + 'search_users': { 'url': '/users/search.json', 'method': 'GET', }, - 'getContributees': { + 'get_contributees': { 'url': '/users/contributees.json', 'method': 'GET', }, - 'getContributors': { + 'get_contributors': { 'url': '/users/contributors.json', 'method': 'GET', }, - 'removeProfileBanner': { + 'remove_profile_banner': { 'url': '/account/remove_profile_banner.json', 'method': 'POST', }, # See twython.py for update_profile_banner - 'getProfileBannerSizes': { + 'get_profile_banner_sizes': { 'url': '/users/profile_banner.json', 'method': 'GET', }, # Suggested Users - 'getUserSuggestionsBySlug': { + 'get_user_suggestions_by_slug': { 'url': '/users/suggestions/{{slug}}.json', 'method': 'GET', }, - 'getUserSuggestions': { + 'get_user_suggestions': { 'url': '/users/suggestions.json', 'method': 'GET', }, - 'getUserSuggestionsStatusesBySlug': { + 'get_user_suggestions_statuses_by_slug': { 'url': '/users/suggestions/{{slug}}/members.json', 'method': 'GET', }, # Favorites - 'getFavorites': { + 'get_favorites': { 'url': '/favorites/list.json', 'method': 'GET', }, - 'destroyFavorite': { + 'destroy_favorite': { 'url': '/favorites/destroy.json', 'method': 'POST', }, - 'createFavorite': { + 'create_favorite': { 'url': '/favorites/create.json', 'method': 'POST', }, # Lists - 'showLists': { + 'show_lists': { 'url': '/lists/list.json', 'method': 'GET', }, - 'getListStatuses': { + 'get_list_statuses': { 'url': '/lists/statuses.json', 'method': 'GET' }, - 'deleteListMember': { + 'delete_list_member': { 'url': '/lists/members/destroy.json', 'method': 'POST', }, - 'getListMemberships': { + 'get_list_memberships': { 'url': '/lists/memberships.json', 'method': 'GET', }, - 'getListSubscribers': { + 'get_list_subscribers': { 'url': '/lists/subscribers.json', 'method': 'GET', }, - 'subscribeToList': { + 'subscribe_to_list': { 'url': '/lists/subscribers/create.json', 'method': 'POST', }, - 'isListSubscriber': { + 'is_list_subscriber': { 'url': '/lists/subscribers/show.json', 'method': 'GET', }, - 'unsubscribeFromList': { + 'unsubscribe_from_list': { 'url': '/lists/subscribers/destroy.json', 'method': 'POST', }, - 'createListMembers': { + 'create_list_members': { 'url': '/lists/members/create_all.json', 'method': 'POST' }, - 'isListMember': { + 'is_list_member': { 'url': '/lists/members/show.json', 'method': 'GET', }, - 'getListMembers': { + 'get_list_members': { 'url': '/lists/members.json', 'method': 'GET', }, - 'addListMember': { + 'add_list_member': { 'url': '/lists/members/create.json', 'method': 'POST', }, - 'deleteList': { + 'delete_list': { 'url': '/lists/destroy.json', 'method': 'POST', }, - 'updateList': { + 'update_list': { 'url': '/lists/update.json', 'method': 'POST', }, - 'createList': { + 'create_list': { 'url': '/lists/create.json', 'method': 'POST', }, - 'getSpecificList': { + 'get_specific_list': { 'url': '/lists/show.json', 'method': 'GET', }, - 'getListSubscriptions': { + 'get_list_subscriptions': { 'url': '/lists/subscriptions.json', 'method': 'GET', }, - 'deleteListMembers': { + 'delete_list_members': { 'url': '/lists/members/destroy_all.json', 'method': 'POST' }, - 'showOwnedLists': { + 'show_owned_lists': { 'url': '/lists/ownerships.json', 'method': 'GET' }, # Saved Searches - 'getSavedSearches': { + 'get_saved_searches': { 'url': '/saved_searches/list.json', 'method': 'GET', }, - 'showSavedSearch': { + 'show_saved_search': { 'url': '/saved_searches/show/{{id}}.json', 'method': 'GET', }, - 'createSavedSearch': { + 'create_saved_search': { 'url': '/saved_searches/create.json', 'method': 'POST', }, - 'destroySavedSearch': { + 'destroy_saved_search': { 'url': '/saved_searches/destroy/{{id}}.json', 'method': 'POST', }, # Places & Geo - 'getGeoInfo': { + 'get_geo_info': { 'url': '/geo/id/{{place_id}}.json', 'method': 'GET', }, - 'reverseGeocode': { + 'reverse_geocode': { 'url': '/geo/reverse_geocode.json', 'method': 'GET', }, - 'searchGeo': { + 'search_geo': { 'url': '/geo/search.json', 'method': 'GET', }, - 'getSimilarPlaces': { + 'get_similar_places': { 'url': '/geo/similar_places.json', 'method': 'GET', }, - 'createPlace': { + 'create_place': { 'url': '/geo/place.json', 'method': 'POST', }, # Trends - 'getPlaceTrends': { + 'get_place_trends': { 'url': '/trends/place.json', 'method': 'GET', }, - 'getAvailableTrends': { + 'get_available_trends': { 'url': '/trends/available.json', 'method': 'GET', }, - 'getClosestTrends': { + 'get_closest_trends': { 'url': '/trends/closest.json', 'method': 'GET', }, # Spam Reporting - 'reportSpam': { + 'report_spam': { 'url': '/users/report_spam.json', 'method': 'POST', }, diff --git a/twython/twython.py b/twython/twython.py index 7e1cbf1..8af784a 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -82,15 +82,20 @@ class Twython(object): self.client.verify = ssl_verify # register available funcs to allow listing name when debugging. - def setFunc(key): - return lambda **kwargs: self._constructFunc(key, **kwargs) + def setFunc(key, deprecated_key=None): + return lambda **kwargs: self._constructFunc(key, deprecated_key, **kwargs) for key in api_table.keys(): self.__dict__[key] = setFunc(key) + # Allow for old camelCase functions until Twython 3.0.0 + deprecated_key = key.title().replace('_', '') + deprecated_key = deprecated_key[0].lower() + deprecated_key[1:] + self.__dict__[deprecated_key] = setFunc(key, deprecated_key) + # create stash for last call intel self._last_call = None - def _constructFunc(self, api_call, **kwargs): + def _constructFunc(self, api_call, deprecated_key, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] url = re.sub( @@ -99,6 +104,14 @@ class Twython(object): self.api_url % self.api_version + fn['url'] ) + if deprecated_key: + # Until Twython 3.0.0 and the function is removed.. send deprecation warning + warnings.warn( + '`%s` is deprecated, please use `%s` instead.' % (deprecated_key, api_call), + TwythonDeprecationWarning, + stacklevel=2 + ) + content = self._request(url, method=fn['method'], params=kwargs) return content @@ -274,6 +287,10 @@ class Twython(object): @staticmethod def shortenURL(url_to_shorten, shortener='http://is.gd/create.php'): + return Twython.shorten_url(url_to_shorten, shortener) + + @staticmethod + def shorten_url(url_to_shorten, shortener='http://is.gd/create.php'): """Shortens url specified by url_to_shorten. Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, but we keep this here for anyone who was previously using it for alternative purposes. ;) @@ -303,9 +320,26 @@ class Twython(object): @staticmethod def constructApiURL(base_url, params): + warnings.warn( + 'This method is deprecated, please use `Twython.construct_api_url` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return Twython.construct_api_url(base_url, params) + + @staticmethod + def construct_api_url(base_url, params): return base_url + '?' + '&'.join(['%s=%s' % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) def searchGen(self, search_query, **kwargs): + warnings.warn( + 'This method is deprecated, please use `search_gen` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.search_gen(search_query, **kwargs) + + def search_gen(self, search_query, **kwargs): """ Returns a generator of tweets that match a specified query. Documentation: https://dev.twitter.com/doc/get/search @@ -339,6 +373,14 @@ class Twython(object): ## Media Uploading functions ############################################## def updateProfileBackgroundImage(self, file_, version='1.1', **params): + warnings.warn( + 'This method is deprecated, please use `update_profile_background_image` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.update_profile_background_image(file_, version, **params) + + def update_profile_background_image(self, file_, version='1.1', **params): """Updates the authenticating user's profile background image. :param file_: (required) A string to the location of the file @@ -356,6 +398,14 @@ class Twython(object): version=version) def updateProfileImage(self, file_, version='1.1', **params): + warnings.warn( + 'This method is deprecated, please use `update_profile_image` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.update_profile_image(file_, version, **params) + + def update_profile_image(self, file_, version='1.1', **params): """Updates the authenticating user's profile image (avatar). :param file_: (required) A string to the location of the file @@ -372,6 +422,14 @@ class Twython(object): version=version) def updateStatusWithMedia(self, file_, version='1.1', **params): + warnings.warn( + 'This method is deprecated, please use `update_status_with_media` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.update_status_with_media(file_, version, **params) + + def update_status_with_media(self, file_, version='1.1', **params): """Updates the users status with media :param file_: (required) A string to the location of the file @@ -388,6 +446,14 @@ class Twython(object): version=version) def updateProfileBannerImage(self, file_, version='1.1', **params): + warnings.warn( + 'This method is deprecated, please use `update_profile_banner_image` instead.', + TwythonDeprecationWarning, + stacklevel=2 + ) + return self.update_profile_banner_image(file_, version, **params) + + def update_profile_banner_image(self, file_, version='1.1', **params): """Updates the users profile banner :param file_: (required) A string to the location of the file From 828d355ad280d643e159f4b7072a3fb551edd632 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:29:55 -0400 Subject: [PATCH 379/687] Update version --- setup.py | 2 +- twython/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1281882..2cd3652 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '2.9.0' +__version__ = '2.9.1' packages = [ 'twython' diff --git a/twython/__init__.py b/twython/__init__.py index 23f2eef..4d888fa 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '2.9.0' +__version__ = '2.9.1' from .twython import Twython from .streaming import TwythonStreamer From 498bd9e557040ba9c1e935fb0d7e815077726ff4 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:35:21 -0400 Subject: [PATCH 380/687] Update more old function names in README --- README.md | 2 +- README.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9b2052..f0de1b2 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens ``` -*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/endpoints.py* +*Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py* ##### Getting a user home timeline diff --git a/README.rst b/README.rst index 4f15fe5..c91a4a9 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Handling the callback auth_tokens = t.get_authorized_tokens(oauth_verifier) print auth_tokens -*Function definitions (i.e. getHomeTimeline()) can be found by reading over twython/endpoints.py* +*Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py* Getting a user home timeline ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 600f36c6bab6607bff81f9891bb3588d6512e581 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:46:23 -0400 Subject: [PATCH 381/687] Catch the four methods that won't get caught with our deprecation fix --- twython/twython.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 8af784a..a3f055f 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -88,8 +88,18 @@ class Twython(object): self.__dict__[key] = setFunc(key) # Allow for old camelCase functions until Twython 3.0.0 - deprecated_key = key.title().replace('_', '') - deprecated_key = deprecated_key[0].lower() + deprecated_key[1:] + if key == 'get_friend_ids': + deprecated_key = 'getFriendIDs' + elif key == 'get_followers_ids': + deprecated_key = 'getFollowerIDs' + elif key == 'get_incoming_friendship_ids': + deprecated_key = 'getIncomingFriendshipIDs' + elif key == 'get_outgoing_friendship_ids': + deprecated_key = 'getOutgoingFriendshipIDs' + else: + deprecated_key = key.title().replace('_', '') + deprecated_key = deprecated_key[0].lower() + deprecated_key[1:] + self.__dict__[deprecated_key] = setFunc(key, deprecated_key) # create stash for last call intel From 60b2e14befd777530f51927c7d346def02a65981 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 20:15:02 -0400 Subject: [PATCH 382/687] Update READMEs, fixed streaming pkg error Removed Twython 1.3 note from READMEs, explained dynamic function arguments in another place Fixed error that caused users to not be able to install 2.9.0 --- README.md | 66 ++++++++++++++++++++++++++++++++---------------------- README.rst | 66 +++++++++++++++++++++++++++++++++--------------------- setup.py | 3 ++- 3 files changed, 82 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index f0de1b2..eb0a073 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Features - Change user avatar - Change user background image - Change user banner image +* Seamless Python 3 support! Installation ------------ @@ -102,6 +103,38 @@ except TwythonAuthError as e: print e ``` +#### Dynamic function arguments +> Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. + +> https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes "status" amongst other arguments + +```python +from twython import Twython, TwythonAuthError + +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + +try: + t.update_status(status='Hey guys!') +except TwythonError as e: + print e +``` + +> https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments + +```python +from twython import Twython, TwythonAuthError + +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + +try: + t.search(q='Hey guys!') + t.search(q='Hey guys!', result_type='popular') +except TwythonError as e: + print e +``` + ##### Streaming API ```python @@ -126,36 +159,15 @@ Notes ----- Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! -Development of Twython (specifically, 1.3) ------------------------------------------- -As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored -in a separate Python file, and the class itself catches calls to methods that match up in said table. - -Certain functions require a bit more legwork, and get to stay in the main file, but for the most part -it's all abstracted out. - -As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main -Twython class to import and use. If you need to catch exceptions, import those from twython as well. - -Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that -whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped -as a named argument to any Twitter function. - -For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). - -Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up -from you using them by this library. - -Twython 3k ----------- -Full compatiabilty with Python 3 is now available seamlessly in the main Twython package. The Twython 3k package has been removed as of Twython 2.8.0 - Questions, Comments, etc? ------------------------- -My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up -at ryan@venodesigns.net. +My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. -You can also follow me on Twitter - **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**. +Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well. + +Follow us on Twitter: +* **[@ryanmcgrath](http://twitter.com/ryanmcgrath)** +* **[@mikehelmick](http://twitter.com/mikehelmick)** Want to help? ------------- diff --git a/README.rst b/README.rst index c91a4a9..10d11bc 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,7 @@ Features - Change user avatar - Change user background image - Change user banner image +* Seamless Python 3 support! Installation ------------ @@ -108,6 +109,40 @@ Catching exceptions except TwythonAuthError as e: print e +Dynamic function arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~ + Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. + + https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes "status" amongst other arguments + +:: + + from twython import Twython, TwythonAuthError + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + + try: + t.update_status(status='Hey guys!') + except TwythonError as e: + print e + +and + https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments + +:: + + from twython import Twython, TwythonAuthError + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + + try: + t.search(q='Hey guys!') + t.search(q='Hey guys!', result_type='popular') + except TwythonError as e: + print e + Streaming API ~~~~~~~~~~~~~ @@ -139,35 +174,16 @@ Twython && Django ----------------- If you're using Twython with Django, there's a sample project showcasing OAuth and such **[that can be found here](https://github.com/ryanmcgrath/twython-django)**. Feel free to peruse! -Development of Twython (specifically, 1.3) ------------------------------------------- -As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored -in a separate Python file, and the class itself catches calls to methods that match up in said table. - -Certain functions require a bit more legwork, and get to stay in the main file, but for the most part -it's all abstracted out. - -As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main -Twython class to import and use. If you need to catch exceptions, import those from twython as well. - -Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that -whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped -as a named argument to any Twitter function. - -For example: the search API looks for arguments under the name "q", so you pass q="query_here" to search(). - -Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up -from you using them by this library. - -Twython 3k ----------- -Full compatiabilty with Python 3 is now available seamlessly in the main Twython package. The Twython 3k package has been removed as of Twython 2.8.0 - Questions, Comments, etc? ------------------------- My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. -You can also follow me on Twitter - `@ryanmcgrath `_ +Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well. + +Follow us on Twitter: + +- `@ryanmcgrath `_ +- `@mikehelmick `_ Want to help? ------------- diff --git a/setup.py b/setup.py index 2cd3652..dd44b6d 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,8 @@ __author__ = 'Ryan McGrath ' __version__ = '2.9.1' packages = [ - 'twython' + 'twython', + 'twython.streaming' ] if sys.argv[-1] == 'publish': From bd0bd2748c44d79a109fe92d62b2bee5d23c203f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 20:33:11 -0400 Subject: [PATCH 383/687] Updated docs to fix verbage and note PEP8 swapover for anyone who misses it --- README.md | 6 +++--- README.rst | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eb0a073..630d34e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Twython ======= -```Twython``` is library providing an easy (and up-to-date) way to access Twitter data in Python +```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today! Features -------- @@ -140,7 +140,6 @@ except TwythonError as e: ```python from twython import TwythonStreamer - class MyStreamer(TwythonStreamer): def on_success(self, data): print data @@ -157,7 +156,8 @@ stream.statuses.filter(track='twitter') Notes ----- -Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! +- Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! +- As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time. Questions, Comments, etc? ------------------------- diff --git a/README.rst b/README.rst index 10d11bc..19fab1f 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,6 @@ Twython ======= -``Twython`` is library providing an easy (and up-to-date) way to access Twitter data in Python +``Twython`` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today! Features -------- @@ -168,11 +168,8 @@ Streaming API Notes ----- -* Twython (as of 2.7.0) is currently in the process of ONLY supporting Twitter v1.1 endpoints and deprecating all v1 endpoints! Please see the `Twitter API Documentation `_ to help migrate your API calls! - -Twython && Django ------------------ -If you're using Twython with Django, there's a sample project showcasing OAuth and such **[that can be found here](https://github.com/ryanmcgrath/twython-django)**. Feel free to peruse! +* Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! +* As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time. Questions, Comments, etc? ------------------------- From f0f0d12a60d5334846741ac4730c1afc32ca9348 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 20:35:13 -0400 Subject: [PATCH 384/687] One more thing worth noting --- README.md | 1 + README.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 630d34e..29e4d3b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Features - Change user avatar - Change user background image - Change user banner image +* Support for Twitter's Streaming API * Seamless Python 3 support! Installation diff --git a/README.rst b/README.rst index 19fab1f..9e6ec44 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,7 @@ Features - Change user avatar - Change user background image - Change user banner image +* Support for Twitter's Streaming API * Seamless Python 3 support! Installation From 7095a894a99496812b45cdf53a20f02fee541c00 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 21:12:48 -0700 Subject: [PATCH 385/687] Create gh-pages branch via GitHub --- fonts/copse-regular-webfont.eot | Bin 0 -> 123680 bytes fonts/copse-regular-webfont.svg | 247 ++++ fonts/copse-regular-webfont.ttf | Bin 0 -> 123504 bytes fonts/copse-regular-webfont.woff | Bin 0 -> 46152 bytes fonts/quattrocentosans-bold-webfont.eot | Bin 0 -> 54776 bytes fonts/quattrocentosans-bold-webfont.svg | 247 ++++ fonts/quattrocentosans-bold-webfont.ttf | Bin 0 -> 54564 bytes fonts/quattrocentosans-bold-webfont.woff | Bin 0 -> 27880 bytes fonts/quattrocentosans-bolditalic-webfont.eot | Bin 0 -> 62100 bytes fonts/quattrocentosans-bolditalic-webfont.svg | 248 ++++ fonts/quattrocentosans-bolditalic-webfont.ttf | Bin 0 -> 61860 bytes .../quattrocentosans-bolditalic-webfont.woff | Bin 0 -> 31096 bytes fonts/quattrocentosans-italic-webfont.eot | Bin 0 -> 66152 bytes fonts/quattrocentosans-italic-webfont.svg | 247 ++++ fonts/quattrocentosans-italic-webfont.ttf | Bin 0 -> 65932 bytes fonts/quattrocentosans-italic-webfont.woff | Bin 0 -> 32504 bytes fonts/quattrocentosans-regular-webfont.eot | Bin 0 -> 54444 bytes fonts/quattrocentosans-regular-webfont.svg | 247 ++++ fonts/quattrocentosans-regular-webfont.ttf | Bin 0 -> 54220 bytes fonts/quattrocentosans-regular-webfont.woff | Bin 0 -> 27408 bytes images/background.png | Bin 0 -> 4559 bytes images/body-background.png | Bin 0 -> 1097 bytes images/bullet.png | Bin 0 -> 993 bytes images/hr.gif | Bin 0 -> 1349 bytes images/octocat-logo.png | Bin 0 -> 3085 bytes index.html | 235 ++++ javascripts/main.js | 53 + params.json | 1 + stylesheets/normalize.css | 459 ++++++++ stylesheets/pygment_trac.css | 70 ++ stylesheets/styles.css | 1010 +++++++++++++++++ 31 files changed, 3064 insertions(+) create mode 100644 fonts/copse-regular-webfont.eot create mode 100644 fonts/copse-regular-webfont.svg create mode 100644 fonts/copse-regular-webfont.ttf create mode 100644 fonts/copse-regular-webfont.woff create mode 100644 fonts/quattrocentosans-bold-webfont.eot create mode 100644 fonts/quattrocentosans-bold-webfont.svg create mode 100644 fonts/quattrocentosans-bold-webfont.ttf create mode 100644 fonts/quattrocentosans-bold-webfont.woff create mode 100644 fonts/quattrocentosans-bolditalic-webfont.eot create mode 100644 fonts/quattrocentosans-bolditalic-webfont.svg create mode 100644 fonts/quattrocentosans-bolditalic-webfont.ttf create mode 100644 fonts/quattrocentosans-bolditalic-webfont.woff create mode 100644 fonts/quattrocentosans-italic-webfont.eot create mode 100644 fonts/quattrocentosans-italic-webfont.svg create mode 100644 fonts/quattrocentosans-italic-webfont.ttf create mode 100644 fonts/quattrocentosans-italic-webfont.woff create mode 100644 fonts/quattrocentosans-regular-webfont.eot create mode 100644 fonts/quattrocentosans-regular-webfont.svg create mode 100644 fonts/quattrocentosans-regular-webfont.ttf create mode 100644 fonts/quattrocentosans-regular-webfont.woff create mode 100644 images/background.png create mode 100644 images/body-background.png create mode 100644 images/bullet.png create mode 100644 images/hr.gif create mode 100644 images/octocat-logo.png create mode 100644 index.html create mode 100644 javascripts/main.js create mode 100644 params.json create mode 100644 stylesheets/normalize.css create mode 100644 stylesheets/pygment_trac.css create mode 100644 stylesheets/styles.css diff --git a/fonts/copse-regular-webfont.eot b/fonts/copse-regular-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..af1f5e6e27f634cda8e2f704ddfdd2f9e3f439bc GIT binary patch literal 123680 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 {Z5B|9Ui(j;WPEnUCmG)@8M8UZtuP1X!aSG{ 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 literal 0 HcmV?d00001 diff --git a/fonts/copse-regular-webfont.svg b/fonts/copse-regular-webfont.svg new file mode 100644 index 0000000..1e920b5 --- /dev/null +++ b/fonts/copse-regular-webfont.svg @@ -0,0 +1,247 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2010 Daniel Rhatigansparkyultrasparkyorg with Reserved Font Name Copse +Designer : Daniel Rhatigan +Foundry : Daniel Rhatigan + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/copse-regular-webfont.ttf b/fonts/copse-regular-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..434b208ef85db371bd6a82d85cf9a128a6630062 GIT binary patch literal 123504 zcmeFa33yfIwg10&MmQNy1_B`o86XKHA&>+@AV5H529Zf7Db}fa#d6g;wAxl=P*lKK z8J%!IvFC({6huJU+JZx?5)deYfPxhbg4kO%=l@wdiB@9qD0f6smX&+||Ca8B0Q zXYY5fcfI3&-?bdqahxRni*T|B4<0{i#tkFt9mg$3nlWV1xr5y_K5^Wk{GB~y%z5MD zzh2jkzklL54YfnYO&EC9J=ZOAoX<1t^Yg})mJR*ghQIK4HGj{zc;-d3R&E-|_dj38 zciz7E`s=bIPq%u;aZb(R^YqJRT|P6RuJmdCzRPjKF1`GsYfYZ$IHynX+UoMFZn~`M zzGV~m`)kKJ(D5giUUW%VLBXAlyDOW|d;SDL@}GTA^7kbE?)a0L*WLJ!u6=Id@0%Sb z{F$qMdhtbnT(-EYRq_3LL|bmOeB_^|67cOU0-vah~q=A~V}JlF5If6d_YhFL$o z_PV%M;wk^5xFJm%BWfJcB>hYD_ULFYF6v`)s4^qdcZf{eZmX!9B zv*OZoKAO8B_b}J}_Se^?^@m*F`}NXrbzXYwzk4ZYODg!yt0o2BZ{F2;hdaK? z_4(~oX#ckzNlHdG{ie_8G`-WKo!-t*Y)VJkgZY<19qI6X3w&u0W?$m#(spFQ7=G_9 z*jG5b;NHR;3x|`k4NjWVI`oN?(0J1EH=c5mLR*})&|W7m^k=8H)4@5{>FW&R@8O}V zopGVH&cx6!oeM(mIFm!ab*6;YJ5xiiIv0i>b#4#UI*)OlRiXEspcCWNh7LMgoPpSC zWav%D2PL6nP9?9sLPwn{&d@uw%c%~1G2jD??2p)zdV48cJ?>zxe z!YX(Qo`z@OS&mr^&%yJs25OPl@%kdHgO^}EY=DjMGHilZU^Ca>;uJbtd3}|?w?TiW zV`v#^PbQzcI@L}m?r;-#*o!+{!W}-s9WEj7_j8xeJ7c)h^I#c_8;CWaBL8pwnf8SZl_3QZiMOX(f!Ft#L8~OZY*aWY@X4138>4ye> z&)qMf_DCUh>7hSRr$jcMbbOFTE~I1YyBhz2^|f;L@Vb}R9F91}5vQ7u*oS=_$DTjL zo_Ao+?_$qOu;&%n^KoYsHf0*RfY(Wsf~J!4J2diJ?EGEq{9Ww)UF`f_?EE+>JxEFq zlG1~u^dKqyEvfu1srZUiv?di_k&4#jQBvqnq~H+w_A+^vMV=*-XYZ0{7n5gil4oy| zXK#{UZ#pBKIA=7R2V?nrJfBS@_b)^jv-$pQFbC$sJeUu+qmh-I^Ks7k1Uw0=;3;?- zo`GjM*J^kUo`*G12QR`pcnQ|S2H4_Uj;`NC!?Eb1BV~Fv+y--CF3f}Z(1p4@iJIbL zYKo7YL^PWn`kT|ynS^$apxuw0KF&E#4LLE4`x)W%a7J?cXkO2QF}yz?CcsoQGmU(l zfo)xbecVK?Gn@0=26JF8%!B!`lyfhG<*))?;QPOU00dzzZ01~BVJB&-2bX&}LK_xI zsrEq{b!0j^?Z~lBHQXDd`7mjI1B-autKs@%Q`c}`vpMEAm;-ZR9?XZO9KQ^f!wRt4 z?_wv?@jLOMolZjNk7#6<wRMgBPHd;*??Rqzx%4bQ-{q-!-i2hYPASW6n~ zczqGp!Ar0nHo!*G`Z8>SS6~~mo#?$DEY}Zvb;eF|`%Uy6iS~Wa37aT@Lg)-dPz)tl zd{X1jSH-R=V<93f-VPltT}w3^j01$GE~FuJDP|hcj1`3w=YU zofmij+fmNZg_NC(4FxI1TPOh|`Tnz%u$`Ra zSxVRru5uLH%5^g717%^^d0y?cke{@CChwFX%OVL9gKpdJSLDYxsgv;8FrUq6B@>9j8@2h5kN8eGk6!np*S+X<==mUeK7yW)py&PQdB2l^ z7T-Z*+tJt#G`1a$ZO7U&(bhP$lY(|q&`t_kSdA85M+>X4tUT^+J9oF8yW7s)ZRhT` zb9dXhyX{_TH*?$;+RClGzKU!o*QtkW>a|tWYpbY1R&n(QC<|{>le|YwVkPfXb4~I* zW&JJ6*{>+;_fXE?pho!vsd$}K93d6k$%DHn$8S-N-=ZA9MLB+pa{LDOzn!bv6(8V= z4>-H%3+|@2+QaK!UXwlRvC{lq%jtpznWdPOm}QK^LMNg@vw|iY{LIV!Sy;#I92cbZ z+d_XJhWcR-XKcV08%Vp=y{Aa`VbX2&!+z?A6Vwk!sUMEg`o(gW38BZi^BsKSacTpr z0UEH}Rn&?nsTEIBE1slQJV~v1l3MX3HR4HX#FNyBC#exnQX`(EMm)*c8?fjGEc!Dn zx`A|`#)=!TV6)O4Sm!FNa~0OP3hP{jb*{oXSCRgQsVPrVQ=X)zJV{M?lA7`)HRVZa z%9GTTC#fkJCHeitrSY!hh*?>hhaQ{hIWCIr2fJHW7kqua60~XnU#WkS) z9cX_C7WX(7_c#_utw(LTg_5@0(-l1*dYE-**O{~38SdO$}2`FS9>+Y`s0} z#m%H-%p%QqaP0l?06Yi}!Nafwmh$~&upCyvO3v{r@&kO=+U~z$>j_wAV(2mM?sf9D zo>J46G|`^Z`^*e&B%e1?OT9&2*HY#hNaqRC*gzT^n)^c&$j^D?=X~;W9wVdE|-AGF%SEghkeYW6&Q$(oKJt~KI~*3b}|n;nTMS`fSt_4PUcZ_-9XKC12xwT)Lb`E za>rmF7gBo1pj)$}dDzio*wJIy(d*dJ>*#wGHP4iyO&29>jF(B zc_P|-AI*G*W=^A(FVV_rv~ild{T->R{NwTe-p(uJA|lZ$9~FWB9$~U(@)#20I!QdXf6$KJsuGdAOWBTt*&#PAVQG4=0d^ z6Uf5}%SP zL1z|u`7U|6jJ#Y%UM?dqmywst$jfEqM2e2l%{%0Q$3|=J-M)+Tv$&o>?9Y~lMCy~h4tjZ8ggL` zxv+*@SVJzXAs6z-UZ3c0X{Tv$UctRWZHkPBB@*Hge%9a^Wd*VH3IVn%5VoZ?@|_-?8f$w0RMl%SCg!Xf7A6 zyoy%LE;peK|9@(4LHcl;Ic|${1*zEN*}_gzQV*GoKx4_Vw&eY1eE?YZX0=foYERYuEmhYT}aU*$z zW{%mytie`ZU*+$eoVOmT7^`>Y?1wn}A^HjVT;UK`IK(Ge++8L@Wge%^FogT$T zkD}pT%=C>y%e~NYFYMB6@E9pNNQw@TqQ5b|@67nVGvoWtoj5Gg%G zN)M6JL!|T&DLq6=50TQp(~mE~u8v|?N3pA;*ws<&>L_+}6gxVK9UaAvj$%hg9hHRnEDM<5KC#ZJ{T(h29$@BT8r`rLn1>nZbrTpc2lqJg&6vU&vC}TayG_Jp{1PZDOzjmmFA&~qnyn~lLh3?huEI= zzU&(*XvyZB&Ai|0*~0hO52UI#kv8y2*F&8U*5Gx#q6)tA} z-A5V8=Y9rYjVGuzPhycLxbwebg(s*PPjYV~sr7aFN*v)S2W{+3L*jZcg?IRrV z1xNgyBThBj`Z10;#C5-9Zm2hRV0qFD9V|izi}=n1=wMy*RjF&R%u0IOO*6)ia^)xK z?V26$B4>|~v&YEUBb?jtTklZ^$?j0fb zj*xpt$h{+^&7Kk*A@`1udq>ErtO?Iemm*&V%V7nq@1G^YZ--Ck^@zwp*OmoN!`4QZ|wb3aR&uZ2%Vt_ilK!1p)0SY&<(mnIrMa2_<(c@4A;hq(X4^zwGo%iB#aZ#TWX-SqNy)61hSA|#4?`}$A~3?eDGr{Nz_Iu-aLzG)J8C$oprXMgqG`UGY*xZE1Na?AsYP*jUGXx zyU^%|X!I2I%PBN^3LF0tjUGp%$I<9S-TXk024PrK8u%(4E-OUheo~?($vkayNJRu6LJXc^%K+&+@t&o`dIM z4b(ELT*vE+unt~=^{@f9!A{O!54)(Z_JH+ftTb#R-!_wPo106*Ti=!hv-V0x_Eq$Q znk@Pa&zgr*Qq5Xx(Y5tcp5-2%==}{z7UemGtroPeVoWY)WeL*SxfP476*COuZ zn0l|QA0sWtycVsJHnps|A8U15>390EooS8w@S9DWT17wB##=4@*nafI>z;+(g^9GDC9U_RW=H_qON{f73p3N4@6hkb$haovy2q*nj_6PGPq zt)>4OL)~G$%->_tHaD~ljs6j9K16x_;atxlX7x+Rp-Qw_MIGN8ZRSymUi(j;WPEnk zCmG)@8M8UZZ7>Js!aSG{w{x}s?$Zo=auG%O{tLOhgPQOlW$yrG?*L`*0DZfrl&>P? zFOu>N%t*dRU-%d`w2dwfkSiZBYC7iiF^6%Fub~l}d)>g%o6!jU66X@M^251S8(SvQ zbNS(1{S10r-<_|wXDI)@Ins0K@y@^^|J9M9`#&-=tfb`7JBL_GXB@Vg5PE`g_ypz9 zo-J%7w|CGA?VuIfK`XR_R%i#U&<-@bhuk*Z@1PahK`XR_R%i#U&<^yu5q%y+pT9?+ z?@(txgHD6w!tW`&_T1c>>U?Wzr5i8h;6>P)`HevdZ*T}3ie$#m7h23!{`=!N*6=Wk>Vr5&V>OIsR8M#YBbt`pust!|4SQfO z#B$x!T=z6*aOJnZ`%U{+!gsD2&zP$7S-xeXzb~C1Ue89qf8lz6!B+NhuWw>2j7QO~ z_5ZA&XLJ5GgFAvcdo-K}Q~BGT1MH#~wVPJsJ-)Mt_j`H2kN4K2w4UBRy;4{#k@MfMusIK(&J zz@A^{8@4iKJtb-#D}oxB8M2_TO-Jo|9rCsxtDJh1OW$47GnsT+8LTCp|F_b%hqU?6NE>UGC>4>cm1#{ntu!Xk^7)ZQ zGYUyV){c3?I9mN|ql6}OxtMvck<+Bk#oQ~$Vl7WJd|rW%u?Lw{v-Nu+?(->{i{>IZu3g|lNleT zex2X3^P43%RxGTp=(l~qmLV&K{c+@y32mpxU!HMA?Y|xU_kq_|U-!W;dj0a1+fL1Y z^^Ut1WZl*6?%5B_TK4(MKR>m?Poib!GW zDiyZYQsDmFTP0~LC4Y#moK)CqN`D$t=Lr9Qy_({>{QrlPlc@j zRoJRfg{>4-451cO*osnxtuj^E%2S1{Mpf7fRfVluRoF^ag{^K?*po4Z&HXEEWvs$h z%PQ>2nZj1pDr}{#!dBlZY(=i(VdRP>$Q8DxRbeZ96}IYEVJm?ZwmMj0D~1)eidbPQ zixsxoSYa!W6}Bo_VJnptwt87%bCrrZj#t>qXN9eXR@e$@#d_q54agPtG*w|Mt`)Wl zTVX4+6}DPiVJo;5wyIlUPj(fy`ddIvZ!5wTwn|)KE5{YKnp|Nk%oVokTwyEG6}CEE zVJp@Zwu)V0E87*e+FfBQ;1#wiUSUtJ6}Ea_VJqqtw#r^%EAJJy8ed^6^cB{hRoF^? zg{|&a*ouFJ?E+BP&H#n&7Ess@0)_1=P}oibh3!62*p38+s?AOIIQFct-3$ub;h?Y) ziNbb5C~S9x!gfq3Y!`*Xc2+2Cw}rxXU?^-?hQfAgC~WtJ!gh2hY?p_^c77;qH;BS^ zh$w8=h{AS~C~S9$!gib}Y!`~ccBUw7w~E4cuqbR-i^6ugD7txjUTojX53zkP3fmi_ zu>CTMU5)?n_R;+C)%MjW_Hewy_TMP};O)t=eK|kG_UR~8>YH}_?Bfp3#P;|oY;Hhd zdw~@8v{zw!h7`7sNMU=66gD@ZQ0VUFxi65Bhb zu>E8T+heA%eP;^Wi>9#sX$sr3rm%f%3ftSJu>Ecd+XJVteQ^rgE2psia|+v2r?53| z3fp_9u>E)n+oPwjeR~Sq%crpYeG1$2r?7p13fmi~u>FDx+e4_ZeT53!YpAgOhYH)1 zsIYyC3fsG=SdCm^dmI(E?@?iUAr-biQek^06}FF3VS6hTw%<}=doUHYFH>QAH5J`> zLh)CL?deq5K2L@1{Z!0BuCP6#3fniTu)U;;|M2#g+8)y%VygxfwhvWddsADmJ*o=Z zx2mwctO{EJqp&@%3fmc>u)VPg+b^rIJ+unjSF5nSwhG&StFS$}3frfvu)Vtq+s~`8 z-60Cw_p7kIzzW+Rtgt=93fo7lu)W0!+i$F}J;(~%m#nb8$_m@Rtgt=J3ft$bu)WU; z+YhaMP?)z1g{`Mlm{$yi?T1pB_Y8%3)KHk04TX8$P?$Fkg?Z>u znAZ-4dGb)0cMrv#$Q9-VL}8vm6bq3n%!7!+yoxBy(}=>nk0{I|iNd^;D9m$-!n~O% z%)^Poyq+k`6N1<-8-;nqQP@a& zzr;M{D9n3~!aV9I%*&3#JntyX8;`;~^eAl2xxzg8D9pQ$q8oCBc>z+GXCQ@n3sRT| zA%%GrQkbV9g?S%R*sc+Uc_~tu=OTr9Gg6p`BgNgw73K*^VcwAxi;*kLi;}`TD=Ey| zlEOSNDauwvSa|KCu+$Cre?zvlM+;v!gH{TMF~L zrKlnO3iHpUuw5<+^W&v3-(Cvy_oXl&U<&gKrdW(zVgADu=2J{ze#R8$drV>e$Q0(I zOksY@6z0oJ@hIsLxW8w1;aQ1!ENj6$mnqDfnZi7rDa`Ac!aSiV%sZOGJfH>ImHO(YZT^j zPGMf?6y}*uVczN#=D|*3UhNd-=}uwZ?-b?{Phnp26y`ZkVczr<=3!4^UiTE{iBDnP z`4r}{PhnpC6z17aVcz}}<^fP)UI7*6DNteF0~O{`Q1K*kg?S!Sm^VU&c_>tv*FuGP zGE|s%Lxp)fRG1e;g?UC)n72fQc~DfCS4D++T2z?#Ma4_V73+~J%yXl{yg4e&!=u8y zJ}S%;q{6&ID$HY~!n{Z-%(JAzyiE#R_Jc7JQJCM!7R-yF!aNHq%%_pUd{-*WpQXZl zTq?}(rNVq+D$GBo!u+Qx%#WtRcHSw>-=@NRa4O6%r@}lkD$IYU!hCuv%+II7e19s; zAE?57geuH$sKUHRD$KvA!hDV@%nzx;e3L57U#VgyrBGphO%=Cryu$pSD$FOU!u+Hv z%y+87{HZF;$Ew2ot}4tItHS)VD$HlA!u+@@%(tt;{Jkp72du*U!Ya&Htit@qD$J*> z!u-rC%=fIq{Lw1RN3FvA)+)@Gt-}1>MG3FuEPB9 zD$FOZ!u<3q%y+NC{P`-($FIWt{wmBDu)_QUE6iuG!u$v;%(t+@{0%GiAy=4RVukrC zR+tCjcfQ5^7%R-TvBLZvE6fM7!u%pD%vZ9){3k2Sr?SHQEGx|SvcmSYE6hi;!u&QX z%$Kvm{5vbm=d;56Kr75Qw8H#FE6j(qLX~`znXhSu`JYypPilqvsaBZpYK8f;R&3%4 z#VU#UVo;bbY=!y9ZoxcOD$Hw_!u(Ju%*VFEJoqar*}WtuTMz z3iAQ3Fu&jm^A)Zz|KSSrbE7an;|lXVt}uV(3iDB}Fu&yr^JT6u|K#R5ERZIM_!2ZhDMP*~gtg~ftUsGX1|vp5?H zi@l++cpQp8j4c!vw?k1wTndH7s!&+`3WdeAP*|J`g~h&5SUe1c#mG=t+zf@q(ok4@ z4TZ(rP}I>&{Xk-|ITRMJLt!yI6c*P*VX;0G7XL$GejgPUCq!Z1brlv*MA4sFONH85 z(-MnB?KZLaBnpdJqOdq7iu;i(EZ&L2VxTB2E{eipr6??Zio*PCDlE>5!eXx|EFO!( zVzekMZi~WVxd_5K(0hGdV$oh&u&6Hziv**v=r9V47^AQ#G75_8k+l>SZA)PhxD*zZOJR|^6c)WpVG+F)7UfG}k-roc4NPGX!W0%YOkt746c$}f zVG+j^7KKbEY(Is?`%_p9K!wExR9LJ)g~bn4SWH2M#Tiss z>_LUaBUC6at0q%kR!ye7teQ-DSvA>1XW?PwiY3Su7B5j@F%%URS5aZH78MqMQDHF| z6&9yaVX+$(7SB;(F&-5b_fcW7AQcuLQeiP86&6QQVX-9@7H?8vF(?%lmr`M|Dis#L zQeiPI6&tY=h4RX3GUb)kWXdb6$u?7OC@hwy!s2Txlvh@hSsYG<#pYC4yiSG1@Kjh_ zPld($R9O5^LD)H7Oi+sp`a>*EsKR20wqP+t6&6=iVX;OP7JpP>F-a8`r&M9FOBEK+ zRADhr6&CkYVX;sZ79Uk%F;f*5M^$05RTUO*Rbeq$6&5E*VX;~j7Qa6&5#EVXm6R#>cHg~bn6 zSWIEXgUA&Ydst!dh!snaD=coY!eSXKEWWYAVje3j4zj{xBP%Rkvch60D=eVlpc%PO~CFtDvxW&I*h1tgyJx3X27;u=vmliy5u3IMND>Ev?u{y`iue)QU~W z6&9;nVezXK7Smc`ajq2>`&wb~uoV^~TVZjt6&6cdVez#U7IRx+akv$>&b9tq&OXOs z^BiaLug`Hx#3D4J8ZY*-ex2UzLEm77j()tgv$m-8uc${Z za3|v5u(KqeQI(<#azzQzz!ha2uc$z-sAAobqBnCGiax~KQdE-~#ktN6Sm>Ep#8kyl zeaLvr|}fTE+dy6%Qa+JcwNJ5OT%C$Q4V7l&V;X9V#q(u3|NE#dF9N&m&i? zL9Vdqyowi*E7l=byo6k_9=T!zqe#U@>{;XX(idT>;He=_CEqG;AY~^@`MJiTo z<9-#tWY5VKi4VCVo&K_-gn2AQCG$xNwTiAOs`AUMq&t(|q(?D|o{wTOZLnerEBh4F zI9_o(_oYy)&|0EE|4*z$+e~^CTahbv^VCzZ7rEjJa{XWPG9JhAibT9NwNOL(QB08qZBFaVmyT$`wO7UNM~Bh+-_qE5>tv#SH9OF_U=8iUs7C;!fm>yO1mHMy^_DS9)nspv!GAw@NN zo)k0bn=4d|1Cyy32PRW74os$E9GFbSI53%tabU7L(3|3Z z$Q3Kmv!a$(Me!=Fej{-gj_M1y(5Y#$Q9F&D`w&gK%t@-noLD8G?|KGXfhSW&}1vgFU7Oy zO|cre;yL7s=aDPcAXliE+$O8zc*Tpz73+{IUP7)|k6f_{JcnHIJaWYv)NU zdpKUPm*W)&xURtc3T^&tr9PJNog$9eBSivpTZ%;LrxyIIT2ds@8Yxo9ZACh>T#8)Q z;ww5*eiWUsdqn|qMImxUXXJ_^dLGdW*QK)A+CR5LJOs1ac zm`pv>F`0U%W3pPF{wvfo9h0eNIwn)kbWEn6>6lDC(=nNPreiYoOvhw9Y0DJ#$Q8TD z7sY$Dm5Mzauh_@&iUX8)fqQ_~euKn^ToFs3NTGVuCQHDNmcmbOSdm1|D^l=$t5B`G z$#Ur(DmpS-tWbVIO{V;UnoRixHJS1YYBJ>))MUypsL4uL&!SNNLQSUpg_=zH3pJVY z7iu!)FVtkpU#Q6{Lsv>v>u$0t*2O4#lP`)s%o-?E+(47naDK&L=H(PakSm5FR}5$V zN-=_dzhX3U#d*jTW4W$kJaWYhdPj=c=u>eUa>X3vin+)Y^N=g%BUdPYrzTVWPEDr# zotjMfJ2jc|cWN@_@6=?<->J!zzf+Sbf2Sr>{!UG%T5*%9R@`K&6*rk`#Z9K-HkwTN zSvA?S*pFg0a>aAV70)AAtU<1*r8QL4alGP1 zt||6#ykZ~6D_)}Y-6ruNSH#hBDiUcqTS%w9S5$`Xq_#ejDrQ9#y%~on26LB+A@t)F zLpffdYEqMp<#@$-&aZfu;}xrsE1pBHcpkZ84RS>-J_;3e9Itp0xndo1#Y@N)>yaxq zAXjWduGopqDC&_bcJVYxu?M-r)+9eG@gY~lVsVPNP#so(CW*{rwUEwifFhT@g9_EZ zGFc^k4n-BCdqsa{6%=YEq{-BpA(QRoUKI7n6>6Qb$<%selZiDeCUb|g6RS+3c4C>V z3wNw2VO6d|?Zh&f+KFYdDpnOMdb2)D(Z|W=lQXI2EDGg+!enYEmdVskER&63uYzKf za{+r8&SW&lE6zi%P&=_qHi=TAm`uJXl%EEZ%_iR!w;@-|L9UpKTrm&1Vm@-k?Py1# zc3hcE?YJ_T+Hqwvwd2ZUYR8qy)Q&5YEeU-iS&6+V)Q&5YsU24)Q#-CqrgmJJOzpTb z*$YITRj8d!ebB{rpal=LXxqyACs zL$0`wTJj@_54j?anoN;M550x6%`A?h9#@Q`T~;W+(I%V3ULwV0RxT-~(8E_uIJ2 z3oXW)&Ll4M9M*Iu>5T6bmH4etRE3sOkDp0z{J$ysQ-3LH@E@p9y97)&1Zcj^m$Q5y%N%6m>>HjH>Dy^ApinLF&TVr}jl-V}Q{k76(9QS9S93fsYRwp|vp$@w#h zWn`v^qlHtX(6>>jJt8Jk&+bfCLLWv^8M>Vo`b<>oY%pUTNy?0Hk{ zL9TG|Qq&qR7jZ=8_2U5|nJANKc!xt(2~&B&_UjBbpDecRv&vmp)2f#6{i2i4=suvcNG2_I!q}Q zKeR^P4|D$i#ZTx^(=pgBKRiJ_>(94G9Oz-OV~vJ!Y| zXl+I?Z#fZ-3~2f9Ho-0{nz- zusaIbxbdt%O*u`Dd-`Wx(OG|JWgJrdZ_Ven)OtDa-7lC9kgtXhLmx1Q%nvF(|8S(q(J}XqE;;iL^i3_y4|TNn<7ufr^pntQp<|&Jzi)F*_s+<(>BtxCTcp!o zgO-~uKTSDjf1x`s7kOv4ncs4R_t{EHX=>6H>4Td`xiI|46*}RJ42!8Gy73!kZ~ zR`_2XL>+c6zkX*hzdrn~4W&*S!EYk|!A4TIjUj$wqB9n+X?|xCzipgp{H8fG`Hf{g z$1T)=xAPm0|C>8V$pXB4h2zQV9wlNdGbrfzv&JsDPEfp?`b?4jgRHi@%$#zN=zWdru`&O`{ADUTYK8~v$|wHDKzbe zq5b=~uIW0=({-4q>sFqwBWY6}<w5Z)@$}o;)32WtwZje~v3Ijy*VAv9r{8e& zT8O=vPNT8gGAzRM80%>;&eLE#8XQcDO>0SLZ6q2QgH@(_dTHnBCEe3YdrvPJ=;dM7 zLM-9lE4b$%xw{rS8qOVVR>Z0qeCf8_bDS;vTpP%rE zAGwPy?UCCetWIfu4cqHn7RFy;PGop1dx`ip$0GRlCz~Iq#>xIFBI>fn%N!^AwXhgI zwCg$H$0DsZ&gOef-?DFJG=1j0&Ux&p3{n!{wUFZ?NY^i&kUP%3G3*av`?7u6DcR}S zS=qVS`Prq}y|aIm{d`XD0q3g-FWuSBqwd(|Bc1HT?6%n%%|}*!d!!o*(c-=uniU%P zP3W7C54?8Z)dQOkymDaUf%OLh2cAA~+kwjWU(G%ed89d2B;>#Taif}VkVD-j^?d(J z+*f!+WK?vk*1nk7xcG!be^PQvYMZpS?b6$4WM*~9&dJT|*eSoDuydE9;*zeV-MW{R zSM=yv*{iB|pX$E-`qvB?c+Q}62M-xKZ1{+gqeh=M=KQha#!r}d!KBGkrcS$X`j2K@ zj&u19UNWz`*?DHetruTyf6TmP|Bq+SedyI~JL~uC-L?C-&eq>KhYlS4 z@FVA@_x3q+?z-*n1&bEnv+#ijo%@$9U+Vn+5C6`We(s&7aaeN>-;E8vgU!9c|Hts+ z^@2l&76+W<5rHmaCIyCHKRMv!^=})9Dw@=HvM0M`a`w9cH@R!ul7L&By(bV`R1yd) z9x-;(xp|XwN&?}M8+5LFz^b#LeD*|$%d);{2Hn&{$SM^9Aqt*BLTl0Unu zv?S2FIQtg6$nQ9Rb|Addu)OR*ME>xAGiK6)OBY;}ZEt$D&&iqGeu4Mb*rvbig1)BI zCALq@;UY1`*>8Kd6I+~J8i+2MHYq!MNZ#O!uFRg4eaVlTPGQHz*_F9;_JZso3kF}5 zw;+2#o_D#tfHQVdpoSy4Kf7$8=2H8E@5Fmo>RaD7C#QXO{Q}g$mxhtH3C(FEeH@lh zoR?kSeDS>ONh8L!&k4AbCoSNHhUG2D%U&>SLEc4{61yGy-_nz4*^opw`0WPlP0|l; zfQ04^&b#Q!8Q;AV`)YDAH?d&8scrZrc?+Tg*<&VEx8K4iDaC7@8nMQl7nF?4-OOllr`Y?RjzY2BP_zv6E`42hO>8V6B_&@)F3tIMD9W3@@deF|{~= z6ut0YiCLlRP{OG-!>Bc#h)*MLrB;ciCamR?qPmC(r#zx45M5g5dhgsotI~i|Ul$SW zBp{DS3`V*|b>ZG$!Pah()2(~CKgZuG$DcDa;;FA*58wKA|Hxba@vCbimU68yViSGh zu8aJF`Zy4Sbv~by#ud_9-mT5@IjxH7+D1FM5k)%byfXO{l7Jf~isdq(DYhx9*h{l|A`wlh!6RB`P{9I=3J{xuU!*H9RFcDlfOk z6Ta3-`MrC0>Cz^pOYbQ;zOZ?7%PJG%Cf=CYE$9>f55suo{7J+W=ykGlp*z&69fMv2gUUUf z2C~a*6T_UMwf^=Q`5n{BgRLWvt!-64?ZS@ec4E|+#JHlh(G^ps+D8-lsAl+x34G)R zrk4hPQ(aD^6B|*qwlt@pRZ(C_d9Xv|u|T`B+76|@qO~<0 zGFlhaCUaPzrZmvIJea_T!^&!F670j8zO9Sa4os+_G>3a9aRcK^11a^vK~cxn#-}*( zMRn0WC!UfuC^6XIEvijuH<)5o*C*O(M?H~MI*h*peG-GCC|MOz#{zj}fr`YsqNeW@ zl?EpuACy9k5MC9C_6Opsf}@J~tE4JW;SUs0U4$q2lWLucQB_q{fdc>9#BPJfbxf-Y zlqS`tLlMBNNFH!U;z0A1E$mrC@v0*3Xcx!J?6J}4C>qC@-;(-JaWTb#YF=L6qjUH zp7;7aBeQdQ&Dt_#$P+i+eR}O$cfcbhiHZKq(ktBJfm2Gl4rt{|jY)|vigf=m<`=Us zn0RZ~DQR;vGJe&1@cB2_bS*9}DKG1t9nqLK{hFI6UNB^E#pP+wWMn)MbMBb%4L8nk z7iE7{n|W2IE?u&+(z|r0d+MAM`XFL!luJ*#JtLMX#&~O-+6t?Y`+0S}*8w5OeWbk~R9q1s$BQSb%WmWSmF@lI1_qY~eD!r1UQPuv66rz|)g^g-e18XWLSoeENWF1$n^)~+f-ErYkE_=R}_;&-TY*v6CBi!1G-fO^RszP zB9F>l@+j3M7p$PRlsn6lg8a&qv^M2sruw{G_dosgobUrFBl~?_*yjBH@e?M_yLH&W z2@~fJD(jXzaM`(Sb7v1}m-{Ps?)N`f-ts}@!R+jRbjXT$`L3zcrjGq3|F>(``TPg( z>+aule>eYEtG*|Gq*+XT9nS96Pa=z`uY2JKe4KOCsqKN@gMA~9)poNtMG?npV^DT* zbmXzRb{%@e#1#eGMH~xsq*|t|(7065xKt$u24>g$gVC*y(Lx6Y@=}!;9ELIzc$<(I zY=eqBc@1W5r#4pfI+e683Uo?b>+6(DDa&M(MJdZHt;_Y^c?D-+73z(s3JmkF?a{X; z*~&#`QlNiTAi-ao(yrW!Oq--&+XAvEw-bjJku`n0kyj;r7HmgBt94TP+hHC2!Q>iV zM<>-rBn)XTRum!148@|nxeP_Sln}3$q-f=orKS|QQPF8x)U&kUCihC2wdmz#m2OmW z3h&Z-_OK$Amuq$I@al@i#gpAju3EqC$0PdpS>}X(7I!nXVY<7pU0&Zdb*UYu4sM(2 zTRn2xbANv2#-AT^`}G<3*o}>gem1Q`dY37y+-X@w5uI~8w(aiTc+2IJ7Y}{tuG^>n z*u8qf%{R`x?9s*-p1mTmXMB90!9#w#C#QSifYQv%e=+l?@82@;_hf(cQ_tRG}wPfRwA(y<5&k4lY7#rXO4 zx;zD8;rY`po3hlGlil^t9b?_N*sMN_O5;%DXJazLcDWtgs1KiQUDUpDS>vCtygoAG znxFjJRd1gvX%iLey6%H_*|kS|wVUfWuhL9PZj+iA zPR2Bq#F2K@*to3Ti@Ub%>^m_!V`u*%ZIV;s;(TqNaxZeLuezc2zh8dk6|bLaU(~ws ztH$dVJkoex<1?3yh>eQ$F@w$Z2S#)a`*BnabL4%TKvrp>XL&G<_9T+B(5p1is=h9{ zsTQfi@RMoiYok-TU~Nt?jSgb16P?m*dZwZdZb4IRKr5Z;Mte%L_Bz3}|Gh+)x|Ve8 zt4BqDw;+FDt5!*EBk%K9#Kp!WB*tc@+!NNSRhw2x@sC{-o}N^klpHa6R8&lCL2}pd znakoM&rK;zDRa+Rk=lyZx>ch4=s9j?O6QEssGRE}Tl;e2bEEHB810YGNso;Dd6vx} zxZKs<;X5OK#FOv|j&&VV%Y#weUtC#jW|S4$%yu-UQJMBO%Ga7ca$aemeSILhtS+so zGL0>(O>1u-rzLS@`!svo-f3OLXsyY(D0Dr`bNtp3_UBlCGbJi7$4^Vu!=GbKSbm@I zu*ldX_rjq=-Rain$n-TnK6Gf~lNr|kSmcY1i*4`zdunRqBDxDDd5ssRrn<{fh|NB@ z&hW<1-9^sV%+|DV0`aAF(UDH82$Jb94JI|22*s7w)9X2z*IDaCH% zmzf>jxnX3%ls;qHBo+1vyp_!&KVi$epo%ew#EG^keZS$a{`6Za*UDt*jUKh~o_IZ|b<@of>%y#XP3dXj}%qmYR zy!J=AK3^>NH!a7Pl4P^7xuJ>SKZ&}GIY=kXnlUS|UQ?Ez8}?ZD^mcJ{;J&%6u&Qh3 z-BIskyKY3Qr2i<%Od-u-&UE_vW4$p{Cw9PiV<;;SUh5ppi9F^8^1Ui1nIfby4CjHY1 z^XXZi|CpQPzV!31jrU)1ljZH_j5B+X{z7KGYOx_QkftrFtTx;m0d_78w5<9EKhjB|TjXzN>GY?PPU|5B^BF~TYZuvyGEm`} zO-t7_F_=dt_{bMuVjzvC+o~=bM<|1^2L}{6{-nA@f4lTTYkl+lYvZ#L7`asi zlC9$t@cDzOZLNOm=JzM9jZe$Uw$JkYK_{=d>q>biKQmogfCO689M3?*b9y9J_Uzx( zB_&g%v)twyuONTX`8Br%Ui!AkJoL!qfL3^))8*nzJ9%~zW%DdlPCXVNaDzX zf?lcgNzvOq*lQko8^Zi^?I6qVIIJ~pkmYDxBuXnYr7?b}Ym+*tjIL#y2&WB0i_U|} zOoc;B110qVr>w4HQwP6wS)gNL(2t&KP-MDwfGTN?tlQJE1oMu`q0f=-uZv6V&}R^u zae{49G4(!G!494IIK9ME65V#M*wJloL&u`2mu~%%9u-vm-`7r4&%Z^Zj0T!iYwg=T zCB*}(OS%rNzU5~_hYuMwcTG)tZuU8|n$+cW>`k+;UN>ubap^6USKW5!TTjkj=A|r{+d_YQEyK=%ekRW{M9Kj(Auh- zq^4U=Dy?hny$kl`fF$d)cj#PdefDyHAf+nMIw=s(eOH#E3a69Tl@GM93Z})Qht{SC zDv~~K#J620`r?d&>8wXb`K=N58WYn+UN*zvf0nOYI#@H;Eq^=AEt-(gHnpUtrnuN0 zUj4JMUrxRB-1*(lA2VS>-(N4g_qX5t$FaeqZ@zBCsKFy=^`6#oP}m>b`}b{jUmsLm zU6oqGNcf`sA@@Am7#f=4%kI$c*8OkJy*X^cGq)|CH1*zJOq%ZIW%RK6dMf&xg$<># zYQk%0yuwkJ%=1wy?PS+%BN^Lp18u!pI;E*DPDuqXgQ}e@L&%1xYFS2XWuKQUJx6Pz0 zzPYVaRQ~vg&2H|1#>OujU%Tdm#t&0c!iwGPiyyu@nQ`HTq#YBMa? zJDTlVuJgFo4pk*jVt4oE+ zAs4!>F31~l^RG6&@#|~v%Fn;^qR!_Jy891}-`q9#*vQedZyIxMRmJExA1mzHb;zoF zh7G#xrvv-CbHhJ=duXp&H_!Rek1w72>rE?)JC3+!)`W@6DzCk6=AVBt>b!}=hYY!- z>!6=LA9hKUix75+~%5N!L4Ja)ZEp5%=xSP{`%Ot!>{e#wWQ;fRCTjZKy8Y)DOixP8=sEQFH{Ie+ntG4vYoXK5eJuQL z%0m}4MjyJluNx>Vt;_Iwy>6h$bQ5S>Ru|JW{hn7=7u)o%V_C5H+x~7Jn*nQS2DxLK za`9s|&9m8F&m%fFe_=s>v0K8_dO<;cNmy7H>k=1q>Kf6dttaX_a^&OHg@ta6FR7q+ zRIh@3*B6si7`Ci;q4%z!mnS>V(tb5#jJ2dakx^1NCy-rQm)e|S2160`w0xFMtJvF@ z*{~-V8$sO_7Ed>+Tlcgdjz3!Fi<<9%lwSS45lN@9?Xv8N0Lkt;eAM^HC*hw@&+(DY z=G2X%xuUM=<;)0wE#g6*nme4_(@tx;Uv7S{9^q+S6C>Pj=7inc@9TqQVYgOixf>ID z`x6@{HVz!w)_s%leVB8-J2xUF{C>tE1*{MYbSw?FwMM%%7;Y9@SQ_lyGR9$!q`94G z8ssojiRJx3czsq(_~@1i2wjSt1Nk8R&F zA6&tma9>w(UtcxdS7mGKUUP3$>~8*5VK;v>CqLF7p4Ox6>x2D%=f2{uuF8ptY&f;eS zUSW3TkTzD`wxl3_)=XULqJA1P7{M3k(J%&{l#@B=^Z+5OTlOX4H?yYV?$ z)x)}9IQ`Oo)ox7Z{Cq}~j3x`d+F)Z#H->>FbH7Q2sh!F_oeye!JnBHCtRMKtN8hkEi~sCl51s2^4KKY)ZOXw7Oz|4RYH!LgP@Nb|M};G4N=7CI z$MLpH^szt}uUAnQ(=_KY73sK<3|FGsW(}=Dt${8~6K2x|r!SFITR3pAHxpAc6kQLl z3bgeH237^mPqI$NL_b}*^m6-7R#Gs#(5pr1SXkRb*TPQ@G(9}=S|6)j=vR2-JF1>& zzt^weV6VTP6G6`-DXmRTcBNHJQ8~0$uFVwEwfE=SJmH}KSa$HIOP9LkSAB5nZGQ;A zyfEH3?$=$rH{QEo&TE&A4iH4NJ z_uSd3JglQHZr3-P^I9jAm0kbXqhCz@-T%Yfo4~huRq4aJ+9k`fBwMmJYqho5vSmxQ zWm(?iEl%P%&Yn73o1{&bbl=jnlv3!vLQ5A)3$(NhQ1r@5DP<{8wxJZJ6v8kJ!#+%* zlzl0U)$cj?O1A7YiBsVB|E9l*W!VXO&pqedvpnZH?2FGA9xD8F_LG;?3|LI>`SyRZ z{Qf(dH|~*V2A>{v_{}Sb0IoAOHxvz9&E6C(f7#dDDd?}LONV$9y&8L zJjgL40f3CvX-OBs*GAmtqSBS(^hJQatumlssGe9Y<66;?ag@7X)QkZ!kY(<)aDGUc9xH`N}n>rAYp@IZIF zSAHA&8}yHA+ka911aVgs6}urIZ4heVDa=V;!ER3EsvLc=h z9FZE4n{*{|fktu)0VMpSaS5ndB`)k<3TdcpI*d1ZVa<{b=^P?e2SV6NkC)W0%ZwN` z8?t0o6w#oj>?2j8tj$WdbC_g6YQ*nl=#N=cpP??v1lOl9BH0d+6Vequ;grke-s!a$ ze`L=Yg)g1^?$-<7cDS{*N~N;aUT1Pweuz46A6vU)`?b-v*I)R7bMJqqr?#te^G{SC z&J^yv=(NpV`<^}b6~4IJuTQtRwG5Ruon3h=ipsCwy>^v5F!taHYvhM)gkUk`KtQb;W8C zN?h4$Q7w|1@Y;>kHWtr1j!1qKjU0Yb97h!t$8VBCczP}Lr5RsCemt%L&+Rwhg{`C= z@X1!xy;MRk=-kVcg1$AI8lVv?o$^t3%ab2uzxaCLrMd69oEn$2&SP>Lc#-s@?>=(x z6NUfYaplKUAMF2}RJdMIxc;l3U}^Fj{J?KFwi{np-trJLv+rCf&Kb|9{63y-h3rvT zu9?o+r+QgRcEZSg&GZnydK@;l78Fdf{lanxE>Vg6#Y#E8(c4U!uaWeF=PF(o>B4Y4 z+jB(n!IDr!?(xyT)kBtqntk%0-9@Lm;jw)TuuC)XE#f^ZNS}FkstYj}~+`-|kwsuCO8MdhguN z@2OU+DymiT!mF92y<}M=XqXY~h~)bT$hQ|5(_EP3+X|=SD>@(&Y5Gk?8h$g8_Mz`E zFH&D8Kb|0tr;|&!a~aY_CtD_Y(J9L&i>h}*X_mb6Dai4APEW<$bITXt_j+4>$4HLJ z_MkUuhyNKMz08o@0?;otd2K2)*YZ+qEetH**qTv8{Td}QVP*oM(}XCJ@ghr>37?xFYGd-`*$)?Lec z?|r=uLGH_x1?}4#S(N!rtkYPDzG2~saqqy6yIE|H&`C9ePEN}{BxIZQC#4bG?yg8X z-R@-V*_x!Z7H?@P9Rz#OB{OtI0=2`k>*Lu~M>ysI<-7J(B+hnJQ1NF9@?F(VZ>%C+ zY&S`5uylPwa^VbIZS(_IE&Zm;gWqi9?V>ici=+*ZcUwDTSr6fkWcUjz1C1Azly_tS z=~~jGh5d`@Vaa7mjzRpOSdCwD&BBdNIf8ll>4ryfZ@T1b@y!!1uYT?~c308v-6r?k z`=20Peg!qdd0$ZZAp};cM|(%d=8m#InL3P>c=>v|x6PYQI*ZoN3Uj2?X+_*s@&~u# zH-$f#z=iSxWFNv@P0mDmO)Yg@(H{iyS|f_2-XoF@Qqy@!YC0}oodHtw>gWwGe;S{?fj&FB z5%Ya23z?+>pPdj}==*4oNDjIMwe(RP)RNHh?u5314qt1+XGbFlg#QB(1v@90?Xs9V)IE)0P`#h%e~yW*{sBo|&pduv7WGpgZYr~L1~`}BQhHQDq? z>(jWyA$Qd`xZGFDb_>Y^n?}Nybp0irZ%Ilxft)7N0i9>riFkGyr?9H*6woIDwrAOA zp<{HDQQ|C8Jv^#L$I4&_p)iii?Fe3boa17@l~-Pm$e0~=*tI5ez))i;q}bEJ%^Ykn zGFoFg?t!8f(1n1RVrh+9Uo-djx#wD%fGp^<*h=@-`ysC+GPs8V!JYqhI{SpEPIU z`Bq^TLEv6&jmf0chvPTZwaj}#G%m*Rct$+l>UeheNWO(fDY+JE91XOzK?cJu^b!*) zP1zQ4WQMAO%SmyQ&!lyP%T9Yz9CQkz;GlBE+{ zGQo|)qUk}o({b#aHM%S14eTd6GrPQG@voL=4TXM#yVF`|e;jVE5Gnr&H!-MA-Rp;x zx78fu5fqQV{sDtQ@y5~H$rTp~&&8B}4mnzK|} zZlZg-KAr6_OG;-+!t9u91hL~X#5tpI6kU(a5uBYEt2p|Bva@GO;;oboKyWr%akn8!pPop+*b2yl4O7@;^r8IE!)3j!M8)!Y;vBb8{t zSGNTZN2!F*23^vM3NR&6RJcGgLFfb2i00eqX5L2EsP}z-mNm2Z2Os`gw(#TYFS=;c zGynVA)XZrsS9TpdcN2A9(d=E=7 z-BVBB^5)0+P|d_K`st|kCY~s1@RaT3JalxVp0w0}#%Welv=W|3pX{2aF0*J1taQm^zr69L zx$}NRH~Lfaw>(ds319NjZ|^vG#>}sw^6bF*UnnFCc4&5&qIKSL+*Z|ToV%PQ1|L&= zxHnCAxmQA6%A_XgXdd#EDD#{fdX(@&d2>_(1^NYq8wa`n4tg%oIRBCq71%iT=k5Q+j?BwOh zmQa7Po>@BjKTK?X>}TiRs;ST$O$K+&?{D6FzNT8k=>uh(QT`q_t``Wky-iH6+!YAJW#2pvfy!@RLrg+a7g3FCJ~ zi}$Y72}N0v$UD2MDadq^l#o3pP)UHIbSH8yDm#dvF~NgIOkQ!H&}%Fp6=he zOdb37*@YgC@A{VRf$QI2#3^T{*LQYCrrGAkz8ikH_58w*fB0{A zeS|5g1=Z1!2wF_O6$jWgmj(PQ@BICz_MJAe?p-`T8tjPHqZpJvTRz$E^Nk+Ze(?vM z{^i(uiU%yn(|-v)Z3cUF&PKBJLMf|?o-B+!9aRiUjPi}V^ufdQ7G9~2h?Xs^nY=Op zk+~{@2noehwa>!|iu zQ&foZa~ySz*D(K?iOyvNBf-@?Gr_=@U3A&WLl`19&7$I9@m(4CYJ?&UPr0FRIpJmlkf z3J>`xPsug%45g7wRA(aeUA|txf9owImU=IK$5Kx(ElwCL!2>KhJbZ|BiOiMi#tYdD zzrWzgOEuDBZUDiUO;locFp=0qo$?+>@R2X}PE8$(Ojh2B%+4uSA~^oY+-rsZJ9xoD z>UQ+i!beAK$&NFBq)_oYdZ4xL==bKnaNjK`Bv#-ZS}%?7H6m~WR@_~|e&O)y4{6!O?DG35g3mkb zI``_Df==7frl0%y0jKKDHeag4kJ|RpwBm2YP(A(kU)0p7Ht<~X4X{sZW%!K!vi-7L zH$G#wJ;`ge{Ig27hzMx&I+Hul%%^Dv;?kg)rs^?zP!t9VV7O?+N~ z!em=Z3!C)^+7dL7LPIE0AHm>_2v(OqAgfj^WH2XsW?kvY8y24YtY2JmsdM4mGgps-IJ9McvK7k;g7gFt?EUb#Rv%4b=$XI?%V75`l>AH~$stdJ5zTqRR&gN)W- zVk)wij+X=&IARH)_2V=j(uW?t6@6@F}}TKOQPv z&9(>jR9nmxwEWKH)~erL_-^4h8q0nP*w;II<4%oEYjDcb^3B}|f4akK(^v|7+1E2k zZ+FtyH1{8KFLkLIA{qKk_QS%UUQ-FF!QT;6fXWKFq%fB(>XMYkp)N`IM^ba#$@6NC zvVM}+9do`U*{zR?j!9m0Oh#eCRNnU`xe3UmP9!^R$aigBHiu_>J4*Wl3!X>gQhukVMnnBqd^&h zU9M>0HiL<3nesDuZWow?`D}>@)MVUYQ=n#`I}{D?oWelwDb#d_x@n?>;D2J|FsJ0< zs**6Egltvn@yHa3gJXrjIg&w9ltHqC6#uz z5MDCMk)o@jZSoJSN=MYAJ|CdKbwZ|^H+He_^3MY-t&TpA3ZKUim*mCIqY<(zek7Bm zM+;XE$b^aDZ}c(o*~Mogw+}dYM|*~uVM>1X@xpqK=U2b-s|QE__WiL@HF4Z!6hq1# zsz)(*@0ZaegDy!kTpEIVSH=l9h2bhf%jxMJ5uHc$7j&gSn&$_Kzz_5uNI>%hy-Nl9O9!9Vv7JE83t&qI{4ByN1X~? znVR~j>W=e56@{NQ)i&ZEmdi#E^EE2wWSz(kAC~1}_^}7JtpEDyb-rt^_^+wZ2vaHU-?aMi-3?B^ALyPCn|3tR545e4IKGka*pfy2GOt4f$Pu7}6){VWItZ=B zlB=)*FAcFZQnu~{I6r>#leoTaAl`*v=BcF^f?Gz{(7JngZo zUW!?bN-f(s_u_Q0e(2U;X@7tFJ9~_}jcU8qdRX}g8ia-wPmP~<-q1j3h_zW95hm=C9Yk@Sh zfM)~%>4Y?_b?6FfWxz-kErPHU6!(e;heq+lwG;#bIbLi4i=R*5b>mAv-R!5ihIBd- z(QQ9#)vgEIlF8|Cq<&1P*SQVOif4cF;$;`H!tZXqjZyeL1l8VZLwHC>b z`8Es?K|&~=W>aRVxqaTu;F7sYz_>_t=vzt=jPjzw%o`WPi3)+hpRB*<#_#Q2MQYKx zrFCW_Ym}p|Tov_gZf!Ff$*vyz;{LraH2V5By&cGsPmem~GR5z1d0P#;ugz#`t>Jp} z_gqhN02ZfJDs-lft#{x07XcpW8azA#xvhh)d=a`5!QBMp07p&NKwD~fN>qb~gf zz!2x7rUvTVMae;RaK$k}XwgL0Wa2333K$dNNE^}OkZK45Tw%`HIj%=0Md{6odDx?n z0oR(J9GLsP<*t@wa%v*DvJxZ7 z7;CQhdKrpGi%h5&A(uv7NAO{uZqA2g=uGgx(qSHOne>;K)U!h)Bda~5 z5$>4TI<=ftI_w(5>pyiNDElh@pkGp~JrJwa8m?i!(*nlorfr!>2tY>PM_OAyndx@x z=K3fcZS~<-*8tLF~Njo5=7^)$wV>`6K z?`U3@&c-{V$ipQy+>o>bmHDUIZ0W)Ab!SkuW3?FxX*OobMsW+8r2rg_Oj<`+vR=aZ zpi2kEY_mv)4t(8eM(CTZx8%&R4RC3a;|%;>9b8*2Atd{$`O7=QEsW1|z-$F*lH8r} z^r0}6Sa?6A3^tsA@Q0}8wy;!;c3_)TZ83W4H6N#K9#^I8n4tn|jDX zn$XhE^)%7To_5$qQ~C9bF$9`=vce5p`M`-4NY7l5HxGj7HApM)(;#LLvL>9aiO_3? zw4|YFZ*f6W+lp>z4AkQV&JB>0g_;cBKzX8tEUEn6qWMsTM$;KNUgafuj$j`k1UpLdWs#2FG+V2W05#zkL8L#Pj|l&RX20a8zz4uqJ(XYy z;w7N1<#&{5S(SVX2}eMoh4Y4+4wT(aMWI|;rJ`%=edqvpuN4kR@o z1ffAha5|WXV_Ap@U2QuQ&Th&Z#ocX;=Zt(h&`7l|JLF;~x%h-!01q#di_eHSN3C>| zQ?P3x8xCxm?|egjNDzdUB#VcZ6|XG88?Z3zH3JE(bZ*Ofv~uR${|bxB+06b=BT#1A z4NiCW+Hk06M?f#S;lEU@Wv#}lmWuF3yZh*WJo}!HOgq%ykF$4w2bSa9J!<*^ z+tITJ##MN|+>ZYKO8_;*<--l;=)LSNFh}&&nf|BnwiW#?KB$ZEN%GBHCL4rANE}&1 z0=z&pSIiv{5dX`U0gXVP(z7o-Rd62G9V8@q}>GbTZI!Qh7?KTNG9{Egz~R4vFzS>cIOdk5=T6_lRWC=IvnxN zNqV_+Gmd!D#JdEY_=V0!W8eQMoeeCue>BhaPlA z3u&iAqbYv>gcGW$bi;XHY*Le-QoJv2)oPV`rqi3uoz)w>`rEdJ4IPOspD$F6j`g-nf zWTO;PN<)YzW8yTknq)pE&N7Cx1Rcu25h;$7h!4b{%5t@zUA7u|aOkIq@Q zuIqvqs%rux2d^3FXYU7f;AyMds(F5w9PY4|tOk@*KgRywF6-8X! zV4cABw$efKI&LekP zYDzWe;RP_8a;-Y*kY>ik2M~w3eWs?|ah5?xaBv&&{GY9xIbAVc0 z83;1~dy!Pg=+Qm}Sq=gtsV&TJMe6ID(pgopH%(p)@udg^XvpN)>yf7ikXvfebP2Ni z@9uwRe{qUr@=@5$+-D)P4VRcyE3S!*joq8r`$9`_Xy3l_Kjq}u!!oleT#ZXnrAMy9|L!31uI|LO( zQy-qF1#)WPo`~b82Ke^=DaG#${DE*QP@jx8#x}HRqgrSjW0zhQips-YUv&VyeE5mE zzT4xeiT$y*&M^0kn>n>ESK8#xh86cGY#J>Ba}~M(egkHu^yfqjtjc7IG{q8@aIGa2 zcLjr<;kiE{+USRGS7F#f{%+DW;<0%}u@*8MlRYHM4UzS79)cmLjjR&AhMbq2c^jO2 z8{L(%wnpkdfPMf0Iuh8~S{L>3i-VEq;iJYT^}wckOxX|(lD3H8vn8GniJ+V)2|CDX z)l@zqt8KB&dQ)9Pl-pX&AhW`liX^yd+R%NKTs8?7E^6HLt;yA^28P-%7qP10jCJ9$ zTUUSKK*Hs0J@c`{E|gQQa_Cu;;WB1)FuSJW=+LTuLq)CDT>bV-Uno{kewZqJ zfmcj&7t~f6oNkY9m|5kMcs46)lb?kSb%TRCm&CK>D|mvOYoh0*Ic+|sPkjNp&a&=g zJ}s^SOM=L`(3YXeP7Xy%9g#G6Pz?{+(XpUfwnjihcu#;kG3p8A%+#`%5y1$*Bs3{J zH7L3O21!!ILuwLD0YYiDrKL1AU^MBh9J9&R(hUi9nje|#rv?u%f-IOVcXf*km}IEd znkR0F$@h4gW9xh#d(%zPj!-B%8CNvv4NCPBuIRq0DU>fwwui4hI&QzMLGfTh)igEt z>T@AOjj_t?laEax8Ux6cW_%s}b5(<*YLixLwDnx_^%Yy$9M=~mWIC*P8B@vV_fyqU zC`ffuib9OKO408xjk!hX$PK~|ajYiJ)#@ti4AVnUl@T3({yg}R5^c>K+L~Lw@`ra& zkN0v8*{#9{*A;RapU>2#D@YeO75BCI5CAsK{Xjld>}Ja4YMuS)w=9g+C}wc}THq_I z@mbq(U-6lpY(r9fTt`w$(#Ng#@J4n=oa(1CDT(TGj+e;OS%tY2wUvEvx^T)rKL7BX z4YUH(gJJ>_oqnxHCm!L(*L2aCw(zic5PPH_b-zDpddD@t{;j50S8FtQ6u*DFLvcUQ zq4X$^%X8J1xqkWG2yREm?AnT>-vZ&ls=33UdsG_vGX)XjUW{j2MSc1S$Uoa5`t;2` zD=5aTB1J~oRFB8Ay2AYNR>uidE5mzmTTTu@5tY{E@j z*i8NT^*_vNEEKR7?&jV3fRzoi4Wc)1(db@frebRzxP~9KEwtt>e=2q6$#T*hrf%xm^DKL}<-osYtaD&`lB{J<(@OyQgRDtkDtcQ|;bVEpJ`n@;yVq< zD@9VmaZ@~&q?SK?5+>!~mS|wEsQ%o#ksDKGy0323?7la#;!(GI?H-?Fcc3j_9XxZb z+x_S*eM4r~Se^W%y=uKJIKf_cJ!u{pZ*4fEu;Ng!&Cvew!t*oEw$ZVnv5&LC)mFb= zDdL^Og*vttysbL)(-}4WaU5!X9K|nE1xy&9RS?6W+H-gf{;!bMR=M4O{XHUdhAP!A z`Sz0RgXn2bg&iALwtQs)L4$QG=iy5DqT8CSujm zLv{^vNJ~qy^O^BR)`B{|i)2>|1Of-7Ur0>zgkI?@W7K`jatKb5;T?DpF8 z`I?435ZDv%>>pxg_h_9pX0sctT&7z`oBVrN>|-6^OaJs9Pv`i^*u;~Crs0~PUftVR zr`Bps&e6ite{Zf5vOAAGqueI{6Y`QosXbyA7deliDZ!)c7Oj2CdigB}1&X7?0q2S~B)&Os@S@h{ z*u?r+EV3&YOf&|JAzP^PqSn^7>84n0!;YXL;BYm1DjIA-dGqC6cAKxd+7$9OH5sh+ z!7DS48l%zT3oxUxz5zTIsDdC@yR)#8eMo5nuB%RVx~S#y+$>*JEYyOV5#%8jz|KV^ z?3{m9k(hPph`t+qXCz0A`(Xl=h`hnMA~`^$ldDouffhI(H}r#((Qsl5dP1@q+#4a6 zsK{EY@y(q@25%60H)Ogus1u#Ws!j2>(`FQx5f=go_&g40b!6&wpW?^F;(gSQ;FP$G zf1!9xbsuv09^_IF%g{B?rv%42+JU`-=GMm17&ZdO_j20QUKFW;KgkcOWYtuWMlyv~ zyAvs>lhf@Gf;!DD3sbrj=7F88)PAR-+D>wEXj!Grd8H@e>qGo6X+hqWv(_Uk#Y5W= z(dD!r`UW)GIPd-yB`-pGKryMJXg^url}}+To44gjA?91H$hpjrkRsU;f<6q2Hoesn z>84~rwCP(Dj3uWh>&MZjH&#*;>DizB2Rik4-R6ijyKbwCHrC0%@cUb@L(iV_s&J@* ziJtw-6Lw|`qGxYF_tmbjQP;lE&ccQ7yOdAeOqH9=%ibX@Z~>!b_(Wf%;@_(HZ5;J_W!2*}zJ z#P^m3kwA(MxzT9ptSsfjzog{@rdlKnS>S7M6Xz$y^|O;MWh(vmpf#8sGE04vB=){Z zyd|Tz6y{lV&(H)VW+1;J*{6uW$ICiJtAG9~CLRjziiF)23n*o@n%pzNI;m+zXNUhp z915!pi_v+!>fJWb9%zaF9}Ped_h$9~U3U5{%EuXd`mV)I2J*>&*T%Ba1!p`pv#{H~#GHO{k`JPBq_Igxg{ne_O! zRV~Kmk;Y16u(|79g%5)7rNtjUt^MEyUDbh|wMvE7?EH3 z*uhmJm$k%_mtKAQZC4atyKH(xb9m~CLm(}i=6Os8ar75}Z#-LePqDtR5|<>PVkKPX zeu^16d^tZgGo*lWHz%bjD0e!M-?x*$(Ug?-;SDbYdQLn$c_h0%A!UTNXSl_1>_L5E zb|8aNe{PZk6eeLs(zy_I2Kt=S8POGoOn1(}=@`?}{(bl=h(5~!(&ae^PnkG#J$E7` zQVli7i{J5Se-W&g3i^a*pdqJHjGFZ0?x7|cj04$91~KXpjwAiqZ#PlUzJnmvW9IkW zO&ZM^?O)u=8t1<$y!2TrnLS77(tbjhJ~eL`Y;5N%^o^(DBTp|tPL-CqKgvf-kki6- z^NjXC0D+NG&$;b?kLn{$0uu`L=fqYEn*goq4+Jg#NBEugG-9M_#7Jr&I)jLkID!+X zAzEpHxD`Q7@l^>V3Ezr93BY%h2F*`DAOa@E^G7?}%KH^}yWK|*@PBtj;U;B0`dI5^ zyJTNOR!c4peE~4$XR%wvEVbj1fps?E%M{g)N<{d(iTpHMmyo7$=;_3)(Oxz;GR@h8wlSljF|*6o+9pZ9{X}OZ#dZwH1NX>>J@zaq*gUpnm;N0vGCiF zwI|v*{H~WCyz!ypc)kZSC0+L_`_}IvrJDYG8gEiHGl?T{m=iGSb%iFfLowon1vGMo}8=bmd&Sz5+iJtm8ch6Z<13u5_ zfvxAgXTt-hov~;2Waq_W>lJ^fUsK<|;XHd&)B2UZkGkO-bIK1ZZcx#tF+cJ|kaSU% zN6BL#Tu#MTlDvlS>;WEi7i)$U0+;6$i6TG|yg^^OCW^qrP=hE!m-xFwxhXp+`-_BW z6YR@V4i6Ee0oemKVO3mE3y0Hcw3R2}!1R$&FBrX{&Igjt zb=W*YuD+;D{Q4ARa-4vW%I+R{(kG*NNuZ3rSw}wO!Aj2Ud(-h{;f~sUevyr`}mpre{%a*cOAItRdiVof%n+-?B~zS zy?Vp%Z@ZoYASsw?=r*fC1s>h?xk4$Ns_M2FEC!B%RLCxuZ&6rrW)?7*@H1T1 z+?<=2?A@3GAa*!4E*J!(nPh$)KSrr@EmmV}jpyyfeGE9X>LU~d5z8>IQu{;%K4FqJ zBX@Bm@}2yLxz5dqyaqZ^g@xbYNUezwjwZ;{v*C1hGZNjka1EVisTXq@z+nyJ#{(R9 zRZV9m?4Pa;aa9nu1Rf#7+HoLc_3#OB2^=#y-z}IgI|4K*4ye>xnfEYg-?7W(V8AxX zPp{U>Yc+EZgIwJZ_O00V)TVWH)tc6ZP@B7@&h6>ymUlHbyW7t;IU|=??S|9^0YFXb zYh!OW8fpS^h1cnJwfjDhXt&7k)mK#LYomI7*wv_Kmkdt3yLYcsn%wTTHoreor_-Uy zP-6}BpH+C)-L2Bq2V=DwW0gI=z1FLxAWmtC}#cjO;yb{1H4)c-|w+)my@)oZ5;D-c>O5P>j z4kfb9@$5u0-zL5vi02hW^8nExiXy^UWsN3cS)^fjGL~V?zD7xyoG^vwB?D11JIuqQ zL!0jqzlJ^#f?O)75PSD(_`4{CCTbqWUO^Y@M z#lywj7!>uYsEE=Y9A>p&`4X$W_2w^s_2yg8J@=gZZ`jw==y;pcwLxF?-?|EwQ?F{Q z(*Jz(nHQZ`omt1uJ#g=P&pVf8HaZg@P<-dhthVs-m%nuD&5YfA>z5zA;K0`K*W^!P zv`?+8s;XsL9eYHrbUD@Q$1jdP;Z~jXj)xAMfBu1+Ds02sU>i*KdF6K%Gpf@7)(^-4 z)aLyUKAKV?Gy~Ha7#>;Jg5js90C42&s#0csFovWnKlkB}#ywnr{%#=|Sy%Qu& zsYB8AlmN>szIp#6`}Tb8k+tt=J@XNEMSYC1#`*?VS6BCz4c%$wB@cXi|5*<{^p*Db z-Un}PHL@z;D`&QL_w;1&nb$#S|ESys&ebNGdWuj54IH(=9%sSB07(#t(k3T8v(fSf zIj=XM=>Yo$r&-OBtRj&&iAMtEkBJvuiQ0m|1~n%7do{3WFt$=Did)yhq6d;B=a_#)s`GrT|}OE~KK*7ed)Q>x@elWIqO!1^L9;Sqrd6L1G@k42_$Qohu0W68B=v|iM}#@*&Rx5i}I zHyCQd$z*%2&!FsXiuI^GtyAu9c4fF%-oJZgZ(wTgWM$odG=1@ry?a6A=`#B)wUw<# zL%bW=A7rsreo+1`$YPc3Dqzp)-u4Mu_=ti@<|C-bFmUJmO-^iridifnX%W!FPicAS zh&XHq2!Kzh29E18gSv^+m}nszNgseX0A&~kxh4U(dLR-X`o}p%4{m1eJ)qPg=&|uy zDhDZtiAs#*fzX@NAr^0Byi0||VzMWG!tY{A=t5ij{r-BRrzN^oo@r>XMmDXejYT8P z@~siCQQ6(xoJlutKlqJ43%c<3`1l#VvCnk($>js7&e$Hsszka9e>EP6wzaNNukdv1 zIs?w+L}#cWQq^S8b@ueOCnueq>}{b&MbzhWCY(`s-|nYcP0U#3?14Pl)5=E`JNRsu zPsXNG&j(p#X{^Q!s!3eI9)>m#`FkvEfR5r$ z6!DO)nwmk6KDRT_(Xrh>a#vfxTfL#KNmCOW3Wd|w7GGrQ;HNUi3GGnxro#2Ay@k6A z?_TX59e*zi|1~mqaDUpM3r!>SRjKXH?(_CU)<67I;d4IpMHFC_m#Kb%dPM`UxoMih z;w&Ogbg1>M1Z{)%N@;wjje$ud&}2rzf|%e+2%HdTQ9>r#K$LAtNUc!dRuf-e0g92F zzJp9(4?l)KQAp)D93KUg#GLL#?GrDgH1$yghg*G=`hmsCNP%4Aoi9;H6A)d2xiKRi z1U?n2Ojln9t8NG6g(At$(QC>HVeSL($jigs2R{A%JNo4Zt1%%_d8e}G|9)`{sSf)r zs7GGiav7NBdq(!Z{wF{HmA9J6o)58)vD@XJ!R8~xSSB9d;GU=W247gVhQ|+LRb^=_ zj27Ts$yy%j@cTv7u*0E4gv;m#YM2jB;j6seLz8ps9G>D=();@hqxoE1)$R<{TvSj}{7x~k!}Dr>=Ouy5+u>8;ysTAf*IQFSwu`ccE2Dv1ck!Hx!X zVO@14>dy-74%(t{;WJhPfs&GKM!rsdxLth6U|fQR(@LSnVhqTMQ}bbQ$A&p}mQW^C z{qZJpE&Q{$pn*lB)-tJ`CNKF81mruS5X?)E+*iI?4n$)jEi56zO z)&}-RdIx90J{)Z<%=oPJpJHunr^0Iu7e3~T4EqYR$U^8XzYXsM!$bTX(hbDsZ~Ro9 z!#=?Pa2)Gp-;w3UsouXv$bAUR)A*Lh2e}9x$aHdCg%W6YGCwU;dmxcZPLtb60??eV zs^&k&fN@pvps@JvZy za(GVp6v<*liLTUjUI;MTd4RbPU&5i1-|AW3iN0#HArSrUfGxQ;(b{@ZXdw>$$~7x% z5rww5*ZK_#9)-TSx0^p+`Qg?`)ZKkB($}|prNv0aO_w+>WoGa_l!wBz!=tM=&{^|nSh{im$G>*d<{5hH9J93UG_vQJt6>bJACeh-r*k%#OEx_@FIleo>1Jb_&cz1D=E6hoPQDdNq~1* zEgFURlRzQeRI?c=hEQPoL>gF)@DW&l5IjhgfZEzr9e~>Q^OX*>?n8@B8(Od2ug&gP*SvJ%J~I~gooSM z*FsjlfT4v|uP<$INcHq2F2(|fU~{atqJpn*08lD69&2vdhHW+2^)M)wI5c1{vn7me z4As%8_Z;j@?0t|HION(Iqpj!Nv9GU(wl`2IpYT~7ohwr{R)f8}?F{A0nAqQd5D=gR zYjqU&sn`Xe`6@UP62L2*Xd6IsVH-eaTuM=#GQSM~ZjV=#mc0!C5-L(-U`N2R76YtV zpwM{a#5)2s(GtVP(Y4qXQ2vaKCwa!QjR(g(Bh8GS;>Lq&G2%wq;REvDY6RLhlDZ0MraWOC)`SioO|T6J91+SZn7kJ~!yInv6jkJJRdd=e>s z^3t@uIo0fKjFnJWbwT3-21Sopl*2d~eyA zbmL5V(oz#)(??F_9C&z4^(S8bfHQ{FDUlLoSv_vAImenh-gRtL* zUa25KK&$iw7OVcDyqJ(@@avPW|C;?xzGZPM6>|Rr>Lnl4)Isizko!1o6*!LEvja)0 zoa8zYE#-PBWLj9*LMo0%#1w@_E92RwBl%vDZ1$RH62911kR~Ag6}?!jqiNKo+bE6! zkuLGzFNi!X+@3Uw1Yc?b1;QyPzrUh9u7!VqcR+ass2OCc=-C^WyAbAClgZt;b7x;q z5C8L)crX~J|387WX;BP&E*1*c_4oH~-O|$s>b342!M3&_{vU|bA7GBwkGj;fY@6%{ zvfLDT4a`MPC|{mcg!@Qua}zwLn;_Z&+J_^7<|MnNxElwY9@vkOU%7q?D{_cVZ6$u# zkjSlEPvPTA3P9Gcq?hY4Yss>w!7h$SVZ#w*&@jjXzKCWi)d4V^2ac@b z&ZBIM02@jE6KMv=G`11)9|wv|3e)aLE2`B}1&y3lf=mV-Zg?w61{1 z3$P6G2inh5wpXLt&#ML0%_;8Hz&q44<+X-dD?*l~tka8E`Jm$y&@}jTtz8E8TVtbg ztJt@((#CA_8#g{-VeUJ2WsJ(Oy3*3ejQv&EyFqI=RBBsoovF@Txblk^0Jc-O+96M} z(OPr5CY=m0rPZVSYkhGS$K2P;c5xJffu2;2LRY8V_g*VsDIv$!69;>?U;{=+VU~BW zN-U?6gSU5F`DxTd#cZP%g3QMInNvJcEN-|Vf+N)3 z0Ct(3&|456_KChWh$_(SrixhK1RZi8unW>UoDvb!9=7?DLn|m~r12{>wI}!{D!n;p zEUv^!!i`BofWt_(6>xptFm1{qKmzgpa*tGOQ^RYKoglQF#~F~z5={nC>w`%hujF=S zX>m>w(L$SZ9_~K4bZ<^5(sjdaxBqn0r`R*cl<4ID1!CaEyL2XxU8Uo2)~2>@|M$7t zH?8OK{W{9;YrcwZ>ZI(MfASWZwQp?;4XQViuLm}Q^QkjSD}mI^IxNz}Dty(N`EHsb zg~LC0Hw`(&V&MCh*3__d3t{l7udHG3E=9(O7Z;yP{{KJcQb$eeVLAzi8Mk~^3#P0I zeD|-}7F5>Jd21(9C=1x%!jaUJ$MB^yIS5->FZ;|t`AksTd@CE9sESFpA`MUB6ksi5 z1krI=vXf*h#k2X>Sj&>s;;kJJc-tG@^_ZP3B^qzxlxi2{9;k1D1PngyES%Fu*qiHt_69=Er&QLfW#8Fn;@vb9R^u9;Q%Xtw6>hh`gprfU2r{iRIkpGjEgk=90Z09OP-MlkIf%h?3#J6m<$GXF~_$bVeFP~KSG8ZFK$2LuvOZA zRAf|%b4HaoZ9nS5b2@0TxuSxeGm0AxWka0j5{Fj1yhH89r&&}gsXXDyE~%9K__)Ul zKZdzn`QPBj%;1L_mwi~aXq8J1X)^Y>KnjFFUcq^IDYaK=GQB*tM{?Q5*EpPL{nu=T zt4%D}1&bjq*cDLa*))H9P*ega7i|OZB&VtZd;t?mumPzTLGt{T7fC_8vIlvjTuj~P z)A?lzl6NMHopJ(?2Nz}Y;S)-gy}vxsUy{uqJ&ugo$f67%eG%mMcOdWdfo&rWMntg@ zrUBVwY1q~U|z7s8>tnTnzF`SVqE}~q2qD7P~@~aBVSw%VLd*Fe&pO>wpgx}=5 zgEUt|+ji5;LM=3Vk;=)Ta)eqdZ4b(B!Ni;>)(!P;L9uQv73)s_=WG-@19EdwRc|*{ zoRpZ5=p9?TgEBX(MXE&vI1A#d&VMWYbYA&VV)@L3ag7iSN|59hF^?CtZ9LU$`8XwF61P z%JLe`S)1`xH3iL)A!FQ$ri2KSiMZGlf2G3RZ*NXkGyMJv`{t^{m=Z}XDcP+OHOsNYF8 zy6k)cwns&v#T&ksaowD^N%JL6f?5h(~m2#V!y zP8b3@#8S|N_yZ)>7&!2xvrfdr3`c?PIpLS6jLCpjqMqUW$}`%M?I&nznLeXkMxzGS ze&xS+IvkD)=j#*Wj&Nm#BT-s+_SGCV-Ti#iF^kQ@Y8@7wdyOY>G3)drGfqT+DO6;z(`|a<^Btl^ol1|6^E?)%(G!VwHlb@o2HPucnPqG}n_XQ` z{i0#<7ch*mn!n%{XeS^VTkP48jwvE@l>@>QjR9b3i&$ z*W;~8D`Jw$THU2tUjqJ9(!0F#dH15!m`?=Hce$6T=p&07-b==Y(S!X7?Wlr}O@X0x z=Hy$du;b>0TdIbk+O(xAXYA_dH~v4osjAEyoZ8YVbf3#+L$K&=SsSd_rujge-$B@E zt$ZhH1vEKvr7Y*5TR6b$ihNTcr-Riba%mvlh`uCmZgrp=Et!ks6A{GdYD`wpoP{Mq z#YYR)_u)@k*d5_e{s@hASxkH)stR{Dh)e}55XfMO`LCf|1+7U`5bBu8FJQ(TUrgmm z8l37@gqk|e@|Tsx=LY;VGYf%^a? zVk|quhif)sUVv>RF)(`SjhwrMCTy~1Q?`|A=ncYJ8{))uK&2|PiE|@k)?_xVA4Ayh z0#tZA)NCW>ak5*|QY&fzvrX#$VR%w$3#-u6D;`c0c>jkPuu$?r?-gdQI&trOwWlk6QNF!!-buO`gW(75Qz^BO$leJV?@id6~nxD0G!u#i*U ze|4LP#%k&t3$LC2R#suDRHf+b=-cMEVo|=kAp0T6{{F?XKb5UxLiQ)zI>u$6g6zvz zjY0026`TjD-aH#?8(xVc_?PS(D{C0Mwbf(ymHF|vvW86Y#%CHziVsAiGzQ`4s#Dh|{ zd+DV>kEZ4=LLp1zE@@Q#4D13sW#`DA7kd@5Gf8O;DnEM?x%K3WCIPkMGhSX%2hu6M zxDW_FG)DJtJAT|wQ!KLf9*ifnoV9TbS21=qv+TL?Z1jk<3kNkow@jzFTXvafMM5^7 zkk;T*IS&W5YXApT--A6^7%{*?`*wWYXL(9v)49fG_RwLiG0#>SH*VvHwPrmIXInaZ zmRZ`h4~Iqjg(VsOvZSQ=(unF43e(Fg9T7V6l#jQ_7r^~mdYOf#inp}?PgQraVpHW? zSP-Zbeh?fxo#=dRz~uZ(i!E|ZnMJ;-F|I2aBUPt%Ci59lu-d$p3Rc*#QF|ntO3)%W ziY36(&lgv%gdSiO7_OwEk!I^n+3lnl8^tefjOVxWSA2!WN^ZRw3}Kh7cOz|Tgn6c= z?dV11+P3oM1ybKwnF+50rU@twXuw-K+hd#b<;Gj;q7g3|B^0OPTALTu#2#9(Z zM+)9}Vu?52l)zc%Qv%?L)-eciXl_s}0;`t%L??GLu;*BX)PQc2s zo^!@bvC1gcyQHNdLZ)Lc=aM6zHUkCv4;(q?>ZXckks}HvxH5+g60LdM?x~#jXT0vd zUAy`+8UCkn{=Ba)VR%pJq(7pFtp=dCXR~^9rmrt^bi8!b-zy<|DZUxQZtq`$KYa=| zeSz#nS#Av+cy>HV#8Wh^xpfWs>8<@pgMu`G4}v_<^JdRZsxiqO*sk68g7`uXX6cz8#6yFm!=VH(%G1=_zvG5gY3e2mfLr zsJDQ$YF$GkG=lobNTiF0BTfe1o0q;z)gMm8x^R48zY>Gr zrD=OBAGTli*3`e%-PUg5L-`j)jC?46`Fm97uIx;hY+`!f?h(WL3b7v)d0hlt2K}-N zPp~1h8x@Oou^|+BJIfAmfzyW2cD^B$8j7?b6btPo@B#Dvj2&jsFSo>#WIv#4DeVev zN1Nf4cZKqphqDNR{2PQNB70ue+R*ne6v_*~K7|#c%6F79vbj&qFB2`7SvzEQ?g?bJ zbgSxu%ocA|C7IDyRY#?SF4s~Rm&1r*_PGAW%<-8Y+RM6=F=$n zcfNxbc~k@_l3^Kk)x6mj+8E_fd=)Jkx%sqEv@|K&$h@MZxuRi%9apr`GDYhqMT@m} z2t~_7bX@6VQo5vS*f9#7OqQ%xkj?QD>+#pqE=`C|Rqx)pG!q&>73I^Gjg&u(O_b#pAcd0xDk#B9_=gxu}o zs>Iq~bY)9W)wQN<1$?R01B>qgK|V}<3r>J+IcIL}qU&(XoNK5-p*$_Eg{REfR{^L> zcVW(3ToRlqokr1jF6$Iv_=_961%HX0PHZRtpaA6d`9yedknF*oKt*-#JU?b*4>1t0?N0e zhWK{W5VoVz&KqFzXkRrb(}*4+MC)Btz(EmfevwZPKvZ7OTD+0^*hPD`g~HX>j=Sko zbw|k+7G6Kjo=<*n2k@Ma<2ilcHbakqvj@HDzEl33t|M80VwsCh`E$}5Q_v~#C*+Y6 zG=nq69Q#;hUhnt|On+47-;Te)^x6e)hR1v3mvol!XH{T~I#Apm5yDwI1wRtsluR~8 z2ilAS1zA)RnBh^+cJg(&&N@W0O`P?tiO2(;ro2gq>XJ@&xYUGL1Vzmp|A=eK98O{Oc&Ds*<2!i%k8rNr1Uf2)@`D7+NA21 z_-xYM4EK}~={EI`DhP?DtilLnTFST)Riw~Lf_5m5bs8s=)KX6pTjV4rk1IBI`68dO zgnYN~(c{RGJ-o<6K%}xs>>~RkdL%RtjxAZ3gTM+w6s-6T#3WwmDEraH8M$!;yAgQe zh>4>~z3|4MB9bBqyk_;4Rn>G6wwXVv)M0q9qTWT{dc}H@Buyd8yl~7)8a))A7-kd- zYr;q+I2!_`94QdpYn0VUR*IdCR4J(KF!`x-F?sjRpEz%I*Zco{Q-0T8hpDcv_8XZm z$ty~^VJh;XLi$TOsA~} zl{WUKLf%LvrM)wd`{+4P)Ct0*Zom&o_DK17Q%)e4<`ckjr*_h1oy#j5zNM9(&n>f3 z_?A|GvO7*vD5UdOh~1#LQ!Lmq(uxj<*bYj2)wpHAM7WQIMR;Hl9+MQo!O(Wl1ipGG z6~Qjs2m1dg{TbV)CWcY``RAa&G3R@5-gYEBy0g_>A-QMPu&2K z{0KAgOIu!y!J)Oql= z#u5_W>-7zZSo3Q6EwmQL#hJ~C!o2Y5%#^h{lG*Op<5%T}oB7{qXjV1a9rZO>d`G@I z^2X&A6V~_4_rGYukxGBk?nK8YRDw_+t0HnKP4Q0-4DR@|?Wih*(mp9yxs-(p6*Kqy_c;?L0hU5< z*ShOw*z9x8?6c3F|KEF`fB*ac|L=BjQsoM-Jn?^v_8JQ9en)AUz%)*@4P@l9QV0eM zcs2iTgVFlf{M|0?zwi-ZZkPDduY6@1z3)S&nFs`eqhHnc%e`XGt)YMcLU2a|fWZcM z01POl!0h26FyJT`yU~fTe9IMs;g*bWm$SsgftvIUTU`iCO<`pjo{4(cnX_)cw%VC}dqgkNd!`s^lq_9KQT>!sM|0oI4?11wB; z;foH9lcx3?17U|9e}&F7+ik$M0|MvcOme{uBJ&Ju_}qSs937Q8tmexUYQ9We&4<_w ztmY#?X9Wq5e5`Ug`k6b5H7LL?FSfeJg!6kOA4WE8@jS`OEBi4T^tVA%rLeRdYSkn~ zuSlFSQkw#4$eIB8QL}^=QMkosVq;-uR!+x^bc3{W*|J?l_dN2=w`aA4_@9l0A@m9S zx528q8TjVunsqN%MdT${Eofso^7)9eB z;>N2FN_k_xkctk}^>lAcp5h1wj4otIfh&!9sRGu8i|Fvz?U63TylQvou~LDU#X8l+ z*{;eny3+#8RR$^>n8=kJUA?HG5-XH+ZAwW8vZ;2|5ae#g=FWU^=I#WUE7_f?oS_z7 zi6yQE+n`^-q?#s{Nt>a2NqiMZon;1Z5i zm1f5jE#N5z&026emXIfx;8l{V@QT!JK^z(Q4RB$5t!xf2 z)~D?0niuX9mkz^xPB{ z(EUobHTXwG##kbywrlp5-roNHxrhFAwW;e!>+)BB*1ul!zzoqt%?(vA=_I=eHYnzmJJ^v`=vvgLU}H1^b66uiO#_H8l;KK2-~ud3!}10S z#Li3sqW=jsi_7d_PPyvx2CHlt3dj$3c=ayXSj@qXmG$j590GF@E*U&g1?pOu68N#Y zl?=R=0jQ{AbHX*n<_-gpFKpQdBI31}4>rI)!8{1Svjc%7kU}QeeD(2IQFt|0x7wkG z=cR@NHBuvB^x3C|HsIZN*KhgMaA?VrjP!~x|7zPY!Qp6fCe#N450gQDfBwfmuK)JX z(3@-f-W;tPkbir9YgA|k_U&79&j;D@ap$W-!xO?LNC5!+jgZHgx9#2g#Ey%jlmmg@ z%~0}qbl;(kK`GeT=#pNysqoWezmOw}%P=fy6P=HT{ zGMxiu2;9<9`nw~ex7v-qyIFXYc-CIUihYC>SQKiu!7*Oj;26y|SaPLiGu{{CX$tdP zczAye^V|{2up?hD?CZ^JWPRhha(nv#clH2q)U4R)XK)ImXsw<}jI>Ue!4dF~anyo! z#R=;LSa0IR+v_s|NLV@yg$P5aF?dspC1w~vP0;24+jCaS;fAvUe=eNy=UD5cITitV z?gqFyFP#dKS8KYX7=dPY?e_xWZmCx_l$D^N+c95IzsY%r^#`kui2ARt6ZLlqL}mch z=EO2Uv(`_mltk%a$Xg*m@e^94LD|SW7SMU#ACQDeR}IyX-V4M$TP68L$F6Wn>1ml# zR%BF2rs8tFWsh9fkTS0fDBp-eo+|j=q$Ek-S95@cg8GH^fmyFV1gL3RKPuEfeBmv| z8&lyS#`xr>70&duEYutsk|_;+9v!PJ^sRa#o~5F5GH7nN--K$T=5Q_Fe1`ZTDCh~ z9|Jd`O;W!1BHKiy+?-z0V;TjAf98z@oV^AY1_QZ#0 z6|AdoLf0g|hGDDII`pQrmLAg>D`zreZ%+LY?Go%j{GOpph8V$%c#2|fxa(d?|@_Kf73XiI5RK)^5)CF+!Axl2)j)mMP|mqi=M zJ7|dSvBVYwFb^<^SxwK}K{{hu87^ivjyTX+U)RI9goW+#^#-}m>3qDecV|jGBK>(h zv;sE8(`X3q8>`_Y5$6)@HiA^lraXp$&neFo_?+=Tc!u2tPo;k1p+I_e<7MOa<4q>B zZcQWJ#a{f*44`uY7>DsXLlW&n^HQbHYGSXWb21iZ9G(H(9eLxZXSn1%{V~^{A@MVS z%fq_X@)~UxDl^vAc@U~D2Z)PY}`XrVMPrI$B{RvcFeQEbb5+I)6b(hSIYw8=CU))~-nq(Woi4u%*Cw9!;#z{pGu<~vfeL;;pY z2gi%3yzXTnYaF7e><>+`?A&Q@Z>O8-)Jh!%1q!-m`VRTi!rAm9s#kg7l- z=cOG6a-141i6Y)}8;e35LRoAT2-^kCGFNO^!MY(ilBfCLO@2aFkGi3$swI}DdNbo#ECF_y68;4!fg zk#V7+wzRz5Sx;Q$*A4oy&2!nZ$oLWZZ;tYX=)Xmx|KdNxIm0j4{|@G%+G)D z!@I|peM+%rTp&8J_QD)*?%6XGZCM1*aUwI;N7^6mF}?QXr$m+)JbOIjEQ>@<`j;X* zZ!CbP5B}u6c*H^Ev+2k36`Gv@UE~?sdSZ` z@&)R{s9#~&0rPJcEkL@2^cd0(q=kCkgM2-!d`N&ArByXc%pv`H>P0G_$syu~l z%rtsO38flkn&*(=W16MhhdQh@6P&65EhZ};d9KR4C|)l1>`{J!;~vjd(|0M}2$WT& z2JfgRQCAx^8#Ym*;aSk>1Bz6BL!n9?DQYC9;axyjaFS~HZ<;3mkjeoq_absxIZ8c( z_QkZ_aGolatu#s5O&w@|sq9Y&mHWYuG#XHM0gLr}v`%@BGL*Zi8MObAQbE=D?ug+o zl&L{mQ>ooxr&c8m7>@rzd(;e3$NkD4=}Gw%dg~7AL*4D_d8$_bOl77NwD(177IbIY z`=+g)OQvSe2g;YADfr-1mow_?BxUV!Z#`FXpZp`O=e#BCgz3mYw+gVQS)WVsKKGp^=&lo6&z?r%vOGw1V%I_fZB?JJ*Y`5ovjWY?yP% zxQ{A~o2bJ4D$+C1UlGt~w9hs=D_i6%h6BnT<)qPKY%u%XkBJg;04|6iuY2bVYPq^yZktm^WgpV?VTa z+ONc2iQk=&o^UX+DsgwxmgM~8?I{yePNw#yC8q66FH7H7%%yU`(S>Kru zgin8VVvd@#JNMq)=kp%VJMTD_|8)L)1rY_a3eyXl3ilR86+Kt%EbcG9QnIDw)rq!= zofCgoYA+osyIgKBKR$Wl4^u33rAO7qd#56p4St-Y(fC9k!t_2RrO^W*2g z(YB|3T>F*<{tFJ=UA3@k;en3yj(v+3EgrXcYiC&J=_QYMEn3>OOkTEc`BN)8S0=8^ zS=qJn)z6RsQY|RL{D|k&Yll@lY85GyYYFp_f+qxRYz9+`>OX>{b|*u zKDm#<9Wh7KdD=imct0zcxg{i6GD{@Y;myO}njv}MNb$olW@!Cbf-d)SE1@a03Z7yAME$fJm@&%`h0KmM@nT9t zo<}**Oy>Y!&<^i{4)pisi16HvvsozFfwFuLUk}7R-#|SbsAnZ=tDrR~cMr}wMqHnO zr}g6=u6-Tuz6bSiZA(yU3BFm2|2uJ|7UgL`M%$L7jD5I1ZZrQLJD#`%?c+L_ z3R;^N;dk4`FXHE2{{_wP{aJ&W`ti}IH+96_OcBPC6n`GV({A>69v8Wf7|>gcq>>5! z!w-ESfGnW?7-)Im83a3=V61IJ(UZcl0v!+DM`CRfjkQxO+7t)MCBRZA2{DXQz_Bz+ zhqRsv{m=wNR$*S}!B;6CYluSFmlVUwWFk;&%McN`9Nst;(C<}&KGo2@*1{U44ziCE z`#lZN`b`JEL=(`rXCdm)YiR=1SokNn&7t(>*!5bu>J+qdk5p-ZTcB_bdh$zv-LE+3r_wQeL{~wL$)0Kvj=^% zmv)2qtI=n1h0)7&SPGZM(N<}^ z6d^@QQBt&NP4DuYoXQ+Muh#OaJUw^ld4Zmna1QK3Q671&elE)6T$D$iuU{AU;k-~k zFVgd3J@?jEsh#KQ_2%aKcP(GKrhiFi$2XQ~U**+mc|omGv!;I)pVa8>sIAi4fn2l$ zxo8LSd`w)DXOl~Ml%~1NO)YLo+Scl>RWn-RTtThVTp_b?@K9^COU-C&A(vvWaTzkI zT}FG&fYL4_yk17NTiAZ;MDI(q7v9KVpG%O7&5)w--B_+brwWPEH%Mt?8OA9O= zOGwwZKJh;9`~Bbb?{#M8oHH}`%suDKnR5niA58@XoLe~9U#$|(V{CD2$?Y%tH}8Lc z3JQAa*alE6mj;_?t$b7`6y;wjV0l$o`Uy5Yy+wiZhoY9IE|&KL2S>;P2WPLE@Uosy zQCD9M2SDAK>S`Lcakh5D@;+hvzs07JoL0F%Y`|Vm zad5<#ux+S+r@P1Zi|pL&ow2;<*fww+oZCR6THZo?Yfo%n@n6_B_y10h>_L8ZSl%TL zjt40Y&QB)sq)h$SKx}RJ~HxD4TZ_OCCFSZURrta>~Lf>f{D=RBooOelC zXW>uJy{05X*ytYOC`98PaQ~Kkf)!4{>XOXD%`>g7iwmF4%;Va_?;E=j zqHU?Ilh>8~78gM$7M%HEk48EXi&62%3)kDO^!WA>f_^Ma%2kFV*Q>m3u0vr-HwdR4 zUnxef&5NHGL2@QJSNn6a8Z;^OeC4@wlY^9jtDgiTvd+GyUrbZfE`TRa20z9$3}&O+ zer~r*1$abwO2lkewRiH4T)>F7PM`M2XpB=aO3I|cw=xfh8sUeclCoKT)^>3`HO*sL zlDRVhYjfW76}*RA+V$5{Bdf~GErHbprb;yw?KOMJE`2Q+vOJNtfRw0!IkY@ zxFf`=^BZ$(mIqnCg4Y(NFTg3{tu*H`o6?0t3rAG1`ugNfelVQzd0p2Ed!79b zn@c)(b2zZ0q;8{jxjz|^afZ?E4P6{WpSfus#4XOFcldbx7-BS4IG~;fz9(b>y8VFBk2bl5!UgYThtYMLpr7b0;_zgzEx>poNJ^Ban1xK(`gR=A z;ew{slhtKquCH=Nec7v?{Z+JKMsJgi!p2()?>kkIBaDU)X?$lRAA{uB`?MefMPJE5 zz>68VoT@A&-Q|e}K9O`%;7Bl!dy4=*UMf6-YeN_Xd`Zw@aH%NXsbK2fl?Yi3G3v*UO2%(=+_@!xBa9d}vp0vD^ zuCyBBlw9GoRwOFg;T{$XF%LoC3`T0W z1GDb8bj|bTAFV5FRww5CD5xz2!SW;>dOvfG2c8c>+iG6Y#T`jq6Jx}>B_VaKd2bKg z(+*m44$PGO=GFZyl>B%;NzoHFXcutFogDbmGw;|c2PQ~@2 z7E|Jh6>RXk4;Fm(r=`)?4Uq^^8CkN?_)s&nwFI$oheVED*1z&7EB#X*s5h}}-&mlZ z@nhcqa_q=*?J`DDO!NlZM-Pvp(PpKvn$ql*D4?D9o-RtmP z)ay+*YjxSt1^6|6(|gavk{{xJVUc6+d8EL=@;L{a>W*>wqJ*?v zE7=-3*IzFz%^;jVWX)94dmrB6dt`{v1QJ)4#Shd>>O_aXSMJ)%iQK)fGD_q?B5+zt z1lzQSmISKkGs89s{>=5C-QZ}4yeX1>9=*rF-4Ow;P*Itrsl}z&>6rigo&Ga)Jn2qo zhtKIn#C9j?i1=TtzDPWBz-IT8|9cnL_wMDi&_?Y0A7J0#NX{hn;BqwIlPV?e5oguo z(yEVMWYz=|pDBlux(TFQ)u4ad4M7%wnAYrSNEh5hW_WFo6SVU_OhPX0iYK$tMVWsqpl) zuqj_!Pd&2O}&c$j2xd;TcwGq32Ox{cDm6yJyGB> z%gMaY2*Q&(xgQq?z3+p0EO!b#cR++~6?Exx(?-$fRS8;^ZddzP*dEB@8wCW?Ag zDfUcUxEqo&K9)BBH$W8}`-X~{BO-m*_&!XAbe$Jg?G~qn)1sddjV7`GqK1q*iD^_W zinWF>|4Y<8#m?i+{{wy~5xmaF71FswU3@E6-sOHkFlXJT1d|_1+^_S=g>>+!i|@wD z+dh_lzYM)rPzfP}wR5z*G^4JH_$87s&r2M3f|{w+4K`turPl?^Sd~-kZ_soYElsIO zWdO3x@O1}lm1I4nr2%c zw)mHt;=b%MR$N4G$@~Z1&<6NJ)1ELFD1P`48u4Mn8sMLn_JpM%V(Rd1*CF4&l$sO~ z$x!ESv^MWT+ax)!mP64&AL1e9z@T#CrslvfcoBJMy)-MInGxwJ=B`L zzyfHFG-4*6dDR8hp5}RG;`2DmMZS<`cte3SDzZcKgB%0%md>#YMLkMLk2Y>;? zeVS3t=KOW0ehVUyvaWSk*iv z)c;s>tOi-URyq9n^1a?AR8J@=${hU-e{-K^Y+1Aj>8cgC@aai=cZv^Oe`ZOlUW|9_ z2SpjIu4EaWjr6qHI1B9EK;|WraxxnT?B>6pfB8o_X(m^wB4Aguz)Ni{XiMzeIZyUS z!f`G{xW1}8rT)nb|43_PdC=?pATgB-xA^15r;6 zsZRd?0qj7Jr{q3>a^>r_o|&>GaKlfk-y;7$6eP6~$EEv(7pOPV6Pv>yUGfUXyN!kq zb0ZqR#Tlc>6vD#YOsDe}2Wx9O|Ip~a5xSIyVRovL+Z$l&_h5gJQ{<%<=II@o^ zu4xY?%+83JUv0U1zP)b&1=-`xpgK6wmKHPOfma)cC&c^PU`Ep{)hDY-5z)P!fd_jw zI{Z0OEg65qtooB(7NW!6PL=~Ve!njnP?u-0i0Q$tlZ5G$o=%AaH{RlcyScRLfQ1nG z&BFNb|5#7v_Tzvzh04nUT#5`cCf{T#&=`Fqb^oX;Tqo@qd$EtQiUDmrYAn1yNvUUO zpp0L-rp4-?Xxpib74NXQyv45bh0N2Oma?b3oIU+v=oPtx>h~>y{FhYXKh%C1KDx@1 z4`CjzyuUV*vo%Q0YeitZ$cg@{ivAjne$Fh;7foBla?i2nt)s`Mk5aN4Jht(8Fmy*; zPZ+>sk;)@C&FnPN%;*IVJ}X6d5?*I-n1n|z#Ei}5qV0Dr#LV}tnV1rfP$J#{xn%-NEHyP2R{CZId5OEEFaGqAI;% zIv%-Dv!Z7d|DbFv6j8m{SzxDtMR}Mhs(Zrr(Q4yFrJBhc#gGo@_cjnOz;6X`?DEvUa}8d84cceu+j!Bp z{^D7n^OsnlP@D~)NXoYpWo;7nY1Js1pO`2PW~c#nU94|P(v0`d{$sJd%9bu2Qt)NuGE)O!~lv^JbHqoCwX#^x zLR(@dlW)V*{;<^Q`cP;4q}JZ7E2#8V&=hMEG4n-5`MsODoJ8n=-#V}C6|QvEo!Z;w ziy{ZdwomBu4BFKzK_Tm3sX5NPgmdcKl!9g&km`XRuG{+Mtl{mK$83zjp|ugD-uC9@ z?&c&tHy_4gduvTh&jJFthyvJ%0@mNQ4PEaP-aD7M_>$u(zIE4*i3YXGQ$M8?Ws2JE z2l?EYnd<=iBsInLj30M#(NW2ZC4I4Tw;N|P>l`ldG5g2$qiRk>-lB{wbrY_50j5-L zZ2I7X)_S<6`w){)I!HKK6SfE)AzqAQk5zOKHf2vxoI14BdKfn%P+#rZFtRxwu4pur zm-ZIPM^eK_6K6xX3Lu_YWTX7;m--@gvMmSB|FRB8V0dCKCvj;fbKrzT@eC?Su&dgTbcuIAx&OS~zmdQvWW zT*b-zy@xK%jFyuPsNDW=!*1R^)93P?53LjD7UmGr+OnMV1JWucy4qXUWkt@PhOl2H z4o$2t-;G_!u^pX}Ig8!bsc*@guSi~|_NY(K49dxLw&PapnX>aZ4yr$TuoE*LMFI~I zx|Jjrr*g&8_+i$G<>X^;tDQ$>`mB@7Me-+X@n4(*y0iCSG|6;==4(ZnT)WIqc9~BN zGd@^9;OAlp?=PksdW>8dgV=vqae}TNgShFadh!+KdxGQMzk1CTSNPeBeqV~L_`#Ta z^aN3{cBxhLn4|j-^G5ZVF*Lq8tI0X!DEDQ>u+I8(0j|mPRjqpK@G(QJU?Dd-XvwKTT9Da#d zE30)qGjj;Z)*M*U#j^=0??1Rs!}H0RpVWYU$9HtI-}GTgSeK*E<#gIs_*h-ED7}7k zWb4XfL*$=FbiU+aMm+Q-&LeoXu8r|@2( zDvougF{K{7>-!~4EUud}6Le@ICbA8@IT`LRg{2y*@>Zr;Sa7$tnE3nEhPA9|fB$Ne z_Q5zU7Ri6l6CNpq{?ok?;#%>WBM&~s)P~V(k;^{SEZO+x%!17k#xq}dXEpTtBOR%< zgIH6N2GK7m`V%q_b-$#ldF)50B3!;fb zqSj0b3CISmcfW51x-sJrV(p8t)%(Lzazb&B1{mcJvFHI!2x9{Ez{h%5b_nr+jr<|y zu#|#O9PvQjzv#bQ#GJf4h4!pEsZUtt<8N8=ZuZ;6w%rR$$MDIxYUI)OKEttxXn=9Q z#V#a3JO$Z&iW!$8|1+@N5*MBSy8QRmN>&lzYP=}7wd7@L{d84Q;EYO}vvcVK*UX9s z2Gcd-HS}^p62_k`qhV7jdtJVnVzd|RrBe~(Wjk8iKwBNl8nz483kCWb+zZ5(SlzEy zUnw{vfDBU%@xHo3X~G#zM37@IoUe?WMsWiR=x0*%hP}B`-PGttz-`_o^cUX#!6zee zR}xpK%7!BwqG8+?iXm7*)!<*=%Fy{%U!r+{59RmYZRZMK7B z+CSNnj3)(6WXb;`Q#rKQHy@gJW5Kwf=V*M_qFWC2{qv+9+^;MJAYa;T1MBV7tm*!~ zuPCUgTlh7Jf3dQ})z>?;N zl82bmAFiT^q|fG@s4(*?^6RPZF_9RPomqjT?Eba((i|m_>_t#e;liop2+jJwM!tOT zuwZBHLSMSIh;YQbxP-Q(pwY`UTPjma1?k!a^1j|36_4h`*$Sox&cD#062=DZ<770L zxzSwN#V~OudD}^R9|Q=o1cWTug%}c#dT$@9I=9mZYd`E`zq@5)E0~ea<6|^t(8@Slraoznk>=x%_$J`UOjtoCXRzMuu<) zX#X}nR3IRaY5G;z;Icq3Mo7hxItTMrk|(t_;zQksb^glGW#Z%bDP(`Koxf35+?bS+ zr#X!-YRayj&j>5j)$*+UXbMc?-dX!n8(tn;n6;_6scAkV$aKaZ(0poGu|cIMQBGLF zn$=!fJDyO_!|vcj(WK2e9r2udo6B~o%8y1f_IfT$xChBeXDI+Qh^tKV>BB9%q3-ZjRiI%v2e_^b&~D47DYh!UDw*xY<;LZO5-#r>-Jed> z>!;cOz1-lRe?TU zY~a+0>S66A6q?N&itYaxSd-TQM_BumvVreK1AOY(z=hEQeDjB&^Cg>*TR}QNgk^!w zl(bOPoM9fSRU~RIB=3@GJ}EC>@>2SB{Pq)iSV&8gtUmZsgg^Oex)de-GHOmSAJzIQ zDo8Zf7y0CZ@FhGTnY1}XnTkG+rg&S16%hcRtrli%)Nt0Q(d^a{iZBnremXd59? z$Q#H>DhGJ{Q{rw_fUYJyK##21RUaN;OxAo`0UlsM)+{Io53nI?wlshTye4a|G@x3p zx^PXFr1g>9o;OGAuIX>@K0@qX=xhVO;?W?re!1KN?UW!b(sI_caz$N#|nk6malE<2G$rG~X z3@mz?oOFUItWLlorJ;KcC%yxnhsZ$MApLZPVxaun2nAj(TrpgU3J}$Ox8_WfOV!9*`%`CgOoDl-cs=Y-mLuhj~PF zDwJt~NbwK~^;)|h*8GciqYa&4)!O`|&711*VL?(F?U!(E5oj^Q0Ya--!8l}{7r+)L zVJG&)^Sjq~kMCfq-L&PjttxF5Xdn`x__k9)S{uYfir9T<@ZZuE(TV;?x~SKDQL&Hm z{!7FfCR2auo|0%(Y;c~QBq2iE9qJVko1OOy{LB59x7qHO4(rKGPLaeofv@~O+>@6b zk-~&DFO=ZgVWcz;N>tQC+&To&lQr0hN0HJrD*27^ckZXz6xgKL)GxauX_00Tv0wB5 zlBw!a5J52r%8F13*4d>zxscTC|&T^h$1QJ)wMesxkJhU>=2CtR3xwv)dN(S^!2gk zpjlLdYY&mqJispYFjRHA}-rby}Gqvs<6}w)5Vsnj(y(~bz zcEc`UA*#qdDwe4bweHdP8c7)ut5%45?Hv_sg{=Tb#d={Ye4=6_3)Q^UJkeqvbbf?1 zA2i{^XHZt!L)t?+O$l-_atToWbA$rX|Lnuk<+NMHQM699PIL&!+R~{?G3bw12nBsT z5eXIA3|bgOAR>0P2z5U$D)zhxr4+AH*QpSrM@=hIoe#)Uf9QZgYTr)&F81uz=*B)P z8I7@4Bg&fb5r;@N3}9o%aYY+S8%ie<{1LGHnghI+1z2|G5Wy`3EPHT>Jj(+t`*8eL zOq*rOyTu?WEkyTyebB5HLLos96sQf%v>2F>atx&ivxZr?q>`$%mV{WMyya<@(fLRKJh$cIQ3Bu7N7b}6a&gxG{c6y4U6 z()(f%$PAd5*9Gs@-emJcrM2|gDTdiZcXpp*$G1du-!=dRS|Pge4M4QkuxaaM0!{)# znp7hP57O@aXid0#3>nRg5quw#{Ed)?!Wa%uB%_fvrrM!gu7al~@4E}3^+XhE0Lvnr zA}KWo?`^Ve+Lo1(w~=xYh8NCd4v6j?1CW{{qPy4twCxC+)+R=N!CDt8$ak@}nU>LI zTTT(jRKT(pn@D&4h%96|xvzW402D)s=-$Ro$Q5XGDf~1!_M{v|`Lpj-Ohlf8hSLnb z-$_QZrqJud1tt{(#dsjPxeey)@`t| z0slcpLvIe}A0?wvG=~ol{+H#c@WOY)_cB}gGVLDQ*_~+U;)O~(?CTLLuqe@X=Q`9A zv1kR@pg2V;u*DqbzXrb>We9>wu4p6ikv5QYh%saef~51L^`uj!D_uT~_($6aF;Rew z{^wKcZ0;Zrs6-#FB%S_#$p^i0!i+uRPSYY}CO0d=%G3&#Tk!rw+(~2lm~%#)mzFjts(0m_eWhr|KZh z5LTNa4p~zI)6pu7MT=oB^Zt>Bc=e?ryL3$2l{rVejhAk84sfQv7YW{7IQ z8_}d0mRM?yG$TUe&mAYe4V9!7nalzlF0hNRW&;k<>>@tdfaQ;z*Giu+^7q}3-|30S z<_IthqLTYo_jN%ovWV3ST@c}O#OhtW|5_HsqDy-!hw)X_KkXx^;uT`mQV*n|08H*& zb%hes$ubmm;El{=8Ln7k{g3{(UnCfxsEDK%01llvu6xSP5(fr~^&~-r zny_!yo>L?4y-9{C=qxDxf|cNYNZd#eu`Li)CXM~AXdGwlp$hO0ju&x zz*?J_&ecTyT{l!_O_c0Mj9}t`VJV8XK5D?Jbe5uhQW<#Y%(iS3$Cf;hRH_y^X`O$b z(z%M#2Vvc$)g^sUu*HA*FD+mDI^DO={zqLn=q({VnUl~fOr|-9g9mp;F$HIze z)&M+3IYn}?g%qa5QG0;`^yBg@z~ zhf{qd%b>FG>);>aU)ayLd25qv$i38fvPEqEJEQ9}=g7pEO!vmlOe9ANV8Il@l zq6oJMwfX1FT8`0XFWlz%a}z?!p$fZK5ml`3cGiL*$F;6!(S`ZILowJ?F(uJYbQMaduE*vGlHeHV4EUuSyOft;( zNBXqyu`Oc{CEFnf#v;x>G5)4^zXi$2YS=MU7#n~JdT^$X}I=3<7_k*tGuMcUZ)MEa}150_N3zSxT z1iY7{UBcF^QkTc#r(fc^%@7w>J5%IdlkcUdAfVZ&rlQfW0#=nql`O>qKW0gBp9$XM zNe*Y}hgmBp31xcHuP6-iCugin|Fr@El`$*AD?Agh*^Izvb8D2Tnep9{EMhFomCEfG z>G&D><6TPsgwmIOjTx|5$YUgnVsYMgZH;-(f0XC}u-S&yL8s)kB-Bxnz}u);Jv({R zSG{;m7)S&(ae#$^6905k@~>-Y#AVnR=^B`kF!=s9^LQ2{AjxJVD07@`IWcs^*Mtcu z=kffbT7Tswp99n&$pF>^?}krkZyRYoFcjJQ_^&zW>!tb29Ke&Mbn7L-i7c?F>Iywc z$0Al0=W4~0WnozMBow3TnE~^P5Cy2=!VG&=ETAoL$hl{CneFI}PKCNmd^nS7KK7P= zqzbL>AchnIlEUPSNs)M($$#E$a51Q>XnK^JsrcA3MZn6?tU5$}(TQ{crNssCBU<*k z#nkd09RnTqBPHtm>r8cyYpyx&k$5z)m)_YbPDrX4`(FwKe=AN$eKK`l!qQc}Hqt5T zBm-%VzTVPlVhk10I{VC6?!Nq3$?TPDf;!CpJ|r22Iz&;D+gp1Jq|+gy(^GbekpKNGLAbTMZi5b6$V$cxLX zT=qbcL9TnXH&iTi{|y5ZvSxYB`~9Mq2DGK1LAn*H7RliV5rOghjulHc*Tc`w+Cykl zT^842G2j^Y)AqbtC6WzZ_M~-JHeI%@K0Tz6B7|`=Va@k_kk%9L?`~47X&Y%PX*<5N z{7C*uc%;Oqzr&}ZJAFF!<(JpNlV@7zLx;_S_jKy=!lkt47rzvZ?SCBk>eTmSvp&8) zf-xW+y1Oo)qqOfomX&0#lPWfEUs8dt9>H$*gSfp)CFvx_5&CJ=r>mqmm~1Wo%bFyN z@-=aAyAV`Lt3vf7v+M3x;xg4nesPvLH01_mGFF%6=q-OA@|67|&}Lea4LWw+$gF=I z|Lf9Nzw>6G={ALVEK}UAo&0LWSe1G^ z300?c_EFh@;6oBsSQLA8tHs(lr+V{7;`1%2`@|zW7z6TW<#WubdYz&sKSE;j5cy5f z-t47*>i_Jb30B!8kuGgc9p`^;E+APDj=m_Jog!swm_&qmQj3X-kCzIeGF2}wyP)4m zE!W^>@)PAO^bx#z!;<8&17>r95}P~%+A*f8-&p$qn08llS)qt_K!FJ#Y30_spG8V=$>vy~gY zs8&=rcGj2Ix4z7}hvkJx*{FImSF+!sy+daxUI8{+?qLD<)s3kF?+4uhzCNh;GOt#l zPAN;Ous83!51H$eAIVpdP>V1!<;0E10!g$2F((W z`rN;+D(kDAfz5eQMx5gi>T{S1H+P&4(n&?Sl0A=I+!OZ8wjP>LmQXF0s#~4pJQ8o4 zAS7hWKu}n&qwrpp`9|X{QT~VA2BGw}Z~^{WA_uJ5Vf2buc;C;Ac@oX*&x@dFwFdww z+5Aq_d{5ZMs)kg`^9Hpsl*;1L3@}Ln08jpT(5v$XGHqUu<(oS|y`i{a#$U<7AqkQU zS8aWtV;O%5vd;~(JH!ht=}#6*jIOeoi*<0$C!^-CNEsvRmj=$fRXy~mv3v1LjyF4r zbNIbnmf}RmB$Ev~K>iq_^sDapAr-RgSypfQjhi*} zO-5#!vITUL)`1}-*hmrLmGv_9hj1oMQ`jy8ra?LgMVS;yVTxIz)=?C!`j8lYIg?l?}14gv+ zIWwCLt|q^ih?%-(g3&&>y7J?^mSimksAHH|6NzLGr+I@{vUs~f=&PkwwxxzG?Y}#C zNOj8=-w0#Ae{1ets4Z{a&?CI;3y8dRLv9^%V4(w+PzcaiC0(;=4=j|vahW|?{jxfQ z$nbZ(sjt6EeN`-RwuN3nU)e5KrZgs93>vTY-uQRRypq^)&stgCILSI)u`IlX0Ba^I z%bXtwAGDx1Y^~K+a!1dusfijIl3@WaPiuLMuF?TR4rgv-g#rkv#VX* z%JA0N99s2ylM0rI#5h^q`I+JHT6^(03m=IayXwrBwEn~7Q};b~wEoF)~u>XEwYjiY6Pl={;PGh1DGnWJW&gz;1SnaIBtrIHIZuJkFG>_ zi8lN0(`>5c0E6}3kq_io0@*c24bjUO7?Q1UwA&KVPQC!HJu*COc-2Vfd@X$G440AY zZVHO=R*3Pihv109U823$D7QQTWz7hi4srgawa5GL7Jmv=7R3+Ee%+rJgcWXLD6a_l z&&@la@rn<*&vzoP&FY=tE)+I%Jl*Hplv~HNx+w>nqg@iAN)$E0(H;$yE1ot`NhF&O zM0=MyFHj){L&e{=0|=Bvo@SFRU%)6AYAF{W{B4#|`|8pASmQCn(A|KS)AqDmwCl70 zaatd}FCF^jm|=;0lvms3-ab{dO(EsC7|L&+kvWLL&lAaMb1G4;g?iZ(%N{RcR1ISK z4FP66OgwlUuez)rIQf6PqV2Ot@j!Fi(7V{vQ;TixG5|xaLvTh?bD`KTlx|_uLmXP{ z*Iu_k)?O~yFRlK-a&T^Le&@fxKeDm1`(U5>^=sePul{K(TzBro*fkBrcmILr=dTj* ze8ZN4i;HWA%k&B74}3!Whwa9wk!u(~UlqOm67EC6cjz?BYMDRSt*vh3SOf7Pe?HI3 z%I=L76|MsABe6SZ{�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 literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-bold-webfont.eot b/fonts/quattrocentosans-bold-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..c041ed970bfcc64ff76fdafe2555543f835c89ac GIT binary patch literal 54776 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 + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2011 Pablo Impallari wwwimpallaricomimpallarigmailcomCopyright c 2011 Igino Marini wwwikerncommailiginomarinicomCopyright c 2011 Brenda Gallo gbrenda1987gmailcomwith Reserved Font Name Quattrocento Sans +Designer : Pablo Impallari +Foundry : Pablo Impallari Igino Marini Brenda Gallo +Foundry URL : wwwimpallaricom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/quattrocentosans-bold-webfont.ttf b/fonts/quattrocentosans-bold-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7389c879ddcacf60f1d90f1fd84e1cb89ba65047 GIT binary patch literal 54564 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 literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-bold-webfont.woff b/fonts/quattrocentosans-bold-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..fc14168b0af5f01ed20cb6f8319d32f8d2ef8c5e GIT binary patch literal 27880 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 z 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 X;9^NJ3O)Y~sd_AM literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-bolditalic-webfont.svg b/fonts/quattrocentosans-bolditalic-webfont.svg new file mode 100644 index 0000000..9070a8b --- /dev/null +++ b/fonts/quattrocentosans-bolditalic-webfont.svg @@ -0,0 +1,248 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2011 Pablo Impallari wwwimpallaricomimpallarigmailcomCopyright c 2011 Igino Marini wwwikerncommailiginomarinicomCopyright c 2011 Brenda Gallo gbrenda1987gmailcomwith Reserved Font Name Quattrocento Sans +Designer : Pablo Impallari +Foundry : Pablo Impallari Igino Marini Brenda Gallo +Foundry URL : wwwimpallaricom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/quattrocentosans-bolditalic-webfont.ttf b/fonts/quattrocentosans-bolditalic-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9766a17a843667f6188fe5f06c066ebc2129dbd0 GIT binary patch literal 61860 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

$qp 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 literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-italic-webfont.svg b/fonts/quattrocentosans-italic-webfont.svg new file mode 100644 index 0000000..b613779 --- /dev/null +++ b/fonts/quattrocentosans-italic-webfont.svg @@ -0,0 +1,247 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2011 Pablo Impallari wwwimpallaricomimpallarigmailcomCopyright c 2011 Igino Marini wwwikerncommailiginomarinicomCopyright c 2011 Brenda Gallo gbrenda1987gmailcomwith Reserved Font Name Quattrocento Sans +Designer : Pablo Impallari +Foundry : Pablo Impallari Igino Marini Brenda Gallo +Foundry URL : wwwimpallaricom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/quattrocentosans-italic-webfont.ttf b/fonts/quattrocentosans-italic-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c7ba47a1c589c4222acc24c615a1cb915859d461 GIT binary patch literal 65932 zcmce<3qVxonKypUnJWVfGs9(o;WEPrBM##TBa9<*kth-*ge8<9A%qx3gA!wmvDUh- zb*WKfFgIgNVy(3{>u2pb12M*C6O5arY1Z}2*G8M#{EfBNvg^7o_498C%zVG+J!cqD z({8uz4$hqW`#$e;f1dYwj~vf&oCQB>&R$*JbXUik`TvXK_yU~Gm^Wu`wc0BD2FEox zo-?oE?t89Oo}Pu{r#Norzs$SmYgNmidAyh7X0_qE{qB3bMeBA_yn> zmn>h~`C7?6nK)j~alxxg7eBZtRFpT3?%fc?MYeuMps+;6deiTfS))$9qmbpw~h6>$9W2Ooa~Ptp}G z9L7Dkx@_@dbWXmHo)WMxa0W{y$D4Pvd+WG;+*{nga98;pQ}n# zyHx+5+OGb#`d6Bnni|bxn&-7C+DY1}+E=u{*8WbHu6s)NTisQCy?&AYwEl8TYs^m! z2E!~vtKq0&IF^geiCr4|e%$1^uDC<-+W5)wo$&+4bYqk8+r~o)mW0U(B?(Olk0k6$ zIFj%iQ$xF2$E?l70l{fP5&B4@=|;Ztp#5iSpdBd9xHKY2eylFZY9272P76eagEdBC=OSQ=$D?b;vC_D z=uMQ6;TS?I1j7jWA3p9340n++$Sn_ZB@81Y!0C0sOOyzqrE3hYGMpo%5yJ%Mr3tOb zBc~D5(-Biz@OOZ#0Phz=$JL^TE9il!bre0E0&ni-lE0E-vJ6D1HNslG9i=J&g-oUE zfSveY81lq{OXI?y0OsqU5@`{VAJ7l`>DW#JEF_mQE-wQcGVM!oZ5FPTVJp*r5HJrZ zEsp@^VaRt9&XBesiWtz=5ZWPWIf{19gC>K}KG)ICDJChK(a)Li3DU#BVj_A<0v4@! zDh*#5dqbQPciC~r2&cw321jw>Gh95wdxV#VF%IH3nV)GK1VIj3lr`T4rSFS?{R*&h z2|8gI`szCL!XWg*Ip~Fp&rp&U@R63hfOb@X;5uNt3XS0+Yk{L5f}3pE zI{~P!GH!^6HnTtzCNgM7n5?E^KMh|SzE1Q~iJoi2AE5Ue=zVycRw9WSLC?eJnY7_? z^iT_JXn;1`+aRJ8Cig6NO(O7641n&im z2^J7oAbkWajN?Q+X9W%jO48%^;<*Lkems91&z{G#Aw28hGVsJOs1U+a1Vso?bmHkt z3f+z?bd%|y11SCBrJ(#;K)4>57XjrB^e_l0N6^c8^iq#rnw4<}!K2;WWRYEf0f& zB)uftBoo7E|2jr9#Bf3O(+d2h;XJjQgKK`+!2q=WOq{R4h%0fv7W?u@t||Dw0DN6h zMnHUXk<*}cS<^?YGGrHL+t4RzB4805WdW`eK@V&AJw+#9SH{$@^wogAhPmc&6EGJ< ze;a_gmEhS9?tYx>Ks%3Oyqm+%VT{e7R~tCT1kSO6bG!_WE5KZ6OiDNwrwUdDPB3dB-V-E8> zE^t03AzK)HGF3>9ufku-27K26UkKPZiYGP!%SJp^4KG#JWF${=Uu317fZ>P0trecu zRkTO)G6Y&gJT3BV2+M=eoy5t6X_B2A;5gY9-HB~2uq$gTlI#l@|1i9(8yNd_KzYgw;5F<-4xou3_AD6?c*J-GKCwwHSexCtrv>uMzYzqHrmBU+`nl z3VB67(8mc*xW@40LEo~?_!uolF;EI>&jM!3fV~P>&q}TeV}Q?uXE$REvX0sXD)^a> zOI5Un3@2&J96U)pFT+fhp&C650p`PiSp>`@=wSnTxCRM7&!jE_w`>(iH_NbJz&*rY zH!ucjneY{Xdm&oAfL1?3tDm67*U;J!S|glaU>NuTp1FWy)1Qn-S<1;fT*q*jf)=iW z5+4D7p8yWBmcxMMszSR~z;X?+M7&fH@EpeoPXH$JY{+lFHfHlShqnWd1Y3y34Of&_ zTF}Zxw6Y%~I*b-ppoI}G0e#D!X%5EX!E>_Dut({WZ0;r8{Wkg{kB+36IDqsbY&Eb) zTK5{DKMCm10{W|femv*3qowQUcMvU6-14>p{by*AEFM|$%YdF*y$XDgM-N?cQ_JVj z@)fj9Fh+Q~Q)&4UTE2{yKS0aJFsgHCnS9%uTmFExjF=QxW!?{=RpKRhb77)5@ez4A zG~z=FcFE44#giB#gDE;Xda7gG=&mTEI{_Fz!${snbR_dCQIhU%z}=V7#|P-+6#CeM zJ45hAW6`=S_cs)~2rm!VNX2-`i;`ovYtSedKkk=3T-iUCV>jZd zdrAJ!5Nro|OA%QhNjrltMRO$GH-P=?(0qPq{D_6D#r5&`Qxr$u9YrsnGTju3(G0*L zd5CB5Y=leCqrVV?Xfq^6?r#u1Q1l~PFtYt0V~iB<$+kZhS1Il|i93d1Pbe;?=yezv z_zXIge3G-A7uW8F>=@DGTJ$LE!&3BoCumQjyo<}k9iKrrUcd;>0~hC^Y0m){p8%3G z(2=C~GRFC$Clz0mB1iH=ud?3L(GzJLfA|3UkvW6-O!hwql@SheEqEq^sr%4!8^&`# zzT|_-nw{ij5ZaQwAi{V&xLMXXmlfVP4&L|-y%2AF08R>lKi*?9T7i*KynQe5u>hW3 zE3khbT9)-0S!(izuEHCNhc+UsNqgDY%f&H`l%hHE<7NFu5!(&KEGE`E`9M+FDM}*k zNFH)6^l%#ZCI&dSAewQ)x;zRPehB}VXTKWIi>P>+(TJ=h*;5_*K}^g!l)M1>;3DGy z@;`a54*38bWFr<56%Q&}xJ0y=44Jng0!@Q$wt+Tw*n~+KK^`|5BbtIIr@{mBanliR zmB2pCKo2vyQjDXFD~C0ygyo*c-3h4cxx2Zqan0Pl+ybtZyAM9p{aicu0IbYI++yxg zZYB41G9K2HwQbV2$*%=Oe|3C1F;+Jp@$^-N0 zn`rS@|DeZ@g@n2`$GR)AafAB*z`OAJ7&O`7x*@qn|yatZj8-}eSUW@ekm&JPc8f5Bv zcsFGHlkipSlZ0IZ-Oq>5g`Wu@2_I)X0DcE7!?n{p zUL(#r8}8w_5cn2v>@-6rUuGpC|*^`g|^xtSt;ho`2z~)8tK2EQL`!BJcl33px#`UehFuqUt(f{QS zQ7&?Gcpr@j(mSC|cvO&WoTpq;C`{v4@WQxr82K3GLy`yHI2S&OXGYN42a!7?cTC*k zo*QWK97cSVahU>36r$+;(R0zg=+!@cZ(=Jlc5mMb@~MBd58-DbBon)eYW0cd|D2;} zf8i6*)|a5kuYl&zuYXRf|Mh3Xhe7odh?>Se;Dd2TW2gV3JL5)8vOUK0w|4TE$GIY7 zl#`^H3w-{ka6Nu>H0D1z?mvK&@$LNA_oKbZ8sw{=Bn$qR{jkwqMJ9j?@5GlplFQ)s zA?zOu?+U-gc>hZHJ@Viq6vrK;!JZipP52oZE?mR2xEHz=@i7N0bsTo?8d(W^Uz-R$ z>zyp%Fw=0?*_Q6UB`%FvaLAz&H$__-gX_Od8Q76)EFLEBjKPoX2z;a~$Y_jl1MXp( zHv&r(0^Fl`vypA&+8@%G@;$)$b?EjH*(*@ah0ym^vZE2+0AA(3na#P$@J3oTqc|ow<_&i#a*}Ya0rJO7rboj zDVYnlPkEK$@l^W?2&nHXXnW|Uz8OAl>iZUZ!}?~uM#jndCU_W~B4=;cH)JuyJks-k z{P6AWpmW6YU-kJ)dMlCc&Cg+^tI#5R14JVvQHYALUwg}a3ip8qr2QxO5|N`wG=0mn zD{nqS$D|clECxDbkK!|myNE&`Lf+xMu`|elF^H~*PqORRm@EQ6c!sEaPUb}H6I?`b zg8v5ou7ZZ-^Pj{PUMSsxKZSR2euVV}ykJWa;ca9nyfDJ&bQiv3PqY3bGH{zG*>z?k zsV6#$e$X?dPZ`w+ixNna+$3Ys=Kvp#P3cW;Cvt@R@@+p6oqOv=))wI&GEAce&H)d; zkS3fRMx1h79uxtCHcQ9T%fw6qZc#Ua>=+)>Ru8-+x_RPNM zHS~FtbsT?<_Qz;)6TKt%u@}VU# zP~M9)*QcnQx#<%YF$07T#&;C;798d$=x8L|i;WetGM`{>X51$_3a0hWK^}pZh{eDW z`Anp56-4H(ql@?xgMQW`ZKm;40ahabt@>8foDV zA4jgqo%Je+`BfjWxO2+Z;}0#gJ=29NBG%O2WI<0jfD zmVx$2Qm>DHc4F(1KIA*b-^Vx=nevAG{S%LF=8DL(5q%yx zrfsxu?0=XF6X z!|u95IvhP*0&dRBxWe{&)M7?tOSUq&lj$OCFTmfqgtkM_F=wG&WRF|c`QxSPHP-7m zy-NK?KA5BbFMMv7`=-a_F-&YAie-!%zB%qv=o-Z+V1`be_#EydJdB-%7t=jlj$80}TSv;o^&^jf$Y zoy<@#(NQ%qg*x9!*W|_b(~fX|BQ`&%tMX9r?h@> z;_>Z|DDt}9=XUYRguan&0yLK+_?y^9G)F)2{KTWL?0cNHjyw_N7COT`oj-4`?<=YW zO+!36Q^B@2Z%Wq+OXyTK%KLTYHDuTYuH-$tGLA#ubdfj6jGfuwIKIyGn zb^ZicZr^9*Ec%dj7I=+G4aI=h0skq+(-T_4d4fy!c`+-)c#`5GZ!_coocFPV|xdxspqolsv5SYj+)SowG-VjU^fnxT(|$kDyL0=Jo;zH80^X)K`uj2 zBZ&NQ*z+iVg;)s~07TKhVvM0!`J)lgUkR%SqcPAQ7E_MhUyc20^i~XP{IO3Q#$rZo zT7vQ`-)}(G8hz4H$3}GYqW(-PUqX!@PzNV}4B!<5`?GAvA{A>CK+Qlf|EGZ&0&@3SA6nuz8Dz4{b%ObX&qpX{G}O$AN)EM+%BNvL<2`8I{-s*m8NzyA?T5lF zT%!Is)nQEyHg-IR1Ld0`UjvjXwuf3+?kKKJ!F_7L__wHx4Dk3K)}`L{kVId|Ot8x5@n{u4etq%QIRPh+I-_zy0s z)Ecc$A7hA(i#H~i%$CHY

XaTDmPGGb`Kf$Z<}}&2vr8pW-g?O!ZDHEb>h+F7eL@ z%q*Q%R$fs#yXp?C{GL~HXYKsDyXx<5_?yOin!eV2Z_9$#``Yerf1u+*uIJlNZhiK} zSNDDQd*9!G;I)JQ@V^cn{^5_rV1M81ZyfpYvA6z_Te)cQ!@pXy_t9k+R(ElmU*aC) zxFt_8AeXP>e(>hsJ@g12bbs@+g&Y5V=ka%c{>i7mJbmUUH*kXc^|{Y4{+9dtFV1qC zwtefVEl>CE*#7(r+;cC#vYY#-_voRu^uvw9Z(x+(jQZLR@aI~t7o7Jow;T1Yz4$%I zeZnmTM@rl-?s@Jh?pxeeaPBkQ8q~<%&o!cI7NxKSB9}N{oYK%D);_U7Jj+@`!B^fw?`BT<~O#?buMstL{-7Vsdlllp~WFqF7Sxz0(#Wp zbbO=bGuz1pHr&@TYWw#EHm5_>xLd^PCl;^^3l^X~O+kEHt4GupNICqI=-vKgTboVf z(2TA?n#4|3j-ApMSj_f-*CWOh*w<4h|AhAKqAK@Jr(IO%)rwq0%httPAGFiX44cET zz_yhgHOfcSp+SbuWHUL?No;}r|FGf271+I^&fV5xx6gA{KiFk&u`gOEH$nHsQ_twy zzSTZ&YxRT9t@f=>)}2%28e7Cl+==l~w_@dDI>0kV)>GN(RENW6KfM)bz(aQewy!C$ z0Y2Q8P~fzmRyuduTjt+mbBO$cmaQ1jozAUJ`_?W=-= z&0hoL_N|RA&V_*9S!Kh9cUA%Om5nVyXn{K(stWRU9vjjAkeIeOgF%WGlM6(g1zzyO z%nIxSG)Dr&_A1%}pyzoL(;Yic*r3 zjDj;K?{Y?Axx+gX*sh+GMWPcwC|r@5^dhb`}m4FkHekf$WL~99y;0gJql{p zBcJF~`S9iSz5~6dd^^(cNdG-{?&l zbg1deqYbr@Kd}w3+{Uy>8@`E8m(pPn9g&1MwMMfgAbJBqoTj~u0Qb7il9=HwoytCZ zWt>)LPO@d><$I_3l#}u$u@YpKwE1R>$OS~3xu1_qbrepr2E-(bXbns&bosMX*2Jl* zVn{;;@6%=RR^3$IrOHx~U?lKP|5Tn&O0gyy`NVR*c=}YqWvI)psPCv)Ip>jjcl9^+ zKajW1)w-*Ae*V_E?SV({EpF&J)SkO8cg+A_+|t&(uJ@(g8#4>5CbjHL^Ei_P!IBoJ zZ7*B+{Gu6JQPk#V?(t03hAOPZcP*=1`TZ4Tdg-vbQWdPI&E8dA7qnyTomR z7qd5fO6$^WhF5BZ49-OKv6~B)kR+CA2ZI{ogB zrh@FLCMg@dV8E`yB)PC#irrF^RE6C_FvNUpQz6M>Qb5c$2NSHCKp-Ghm6|QVmB%adl>@YE}E8o(4~S*R1B(HaEKK7mvKCIXYaUI`B4Mz5L19 zE#392HjlQoANuy)p1YQnvU~1YT=w?d?#)##%j+L|lJrGg_!9iZM)0EvNh!Xf z5(URiVSBi|@?m-DOXHW0|x2>#a|<7kb^#dS==JE#-?ELe}oDyBcz19-RNT zX}L|V*2A^uH&i#4v^@S$*Q(ND{!L3&%flSUliuggGrgZlT3=E5JajRc7Xju)!xS*7 zc&6TE)ee2hbDd$6Dqrh{TqJV5n8colH4RG1ikVC2OZ-+NZ@|E_1gkDj=)CXy|Jqx- z^u+V63AtsShIQ-tpY7ud`P9$~Tl>k-_0Zm-(p~$$TVvbIuR;5$r#1<5#<#CIEg8q! z*Ol|>&#L=vghU|(m5 zCr*LH<+}VE(3J-4U>G`0i%31SB?e_Q97V{Sm z^0R*beAD)Ky7q)Vy1uL7sbgJ72R41oYd1A->zuLW7olO`_W%aaf%sX27%8X%^gK zcSsm`XQJ)^T)$uMzWAxS^Vh>i)y3_EKc40}RU^Qk9a$T&HL#ZzGbsB}v4lN`SJa=D ztUy@~h$dx`CFBEQtXVQyp=#kcn1M8>zg(l3GM%fOmt9>o{6TAW(>uowZLDwU_AFh} z?$EdVZqc4#-GauYhB~3ue|)qo(7aX0JiWY z)i7WyBK%Av*wmn`os3N~j7?nZG&_t<9N8EjBCFGq8$`A+y90Qq6TIqafEUgV;N{(9 z8Z6@EfS7ES(w#W6TSOb6hqeO9WEI@Jf1KgPP*jT74p&Ht4Xo9r`yT$_=iB!BZ56d^ zo4rkoybrZCX6s!Yix=ka>37$*``VV)zjXfDHE(rpEv+8d*Vg^GyMEd1IZfttj4>%U z)7blD*|O%jY0jR`c5!Rt9LC4Fpu>mY1p_2!8yAcrZqWNcdTdp`el9jfz*yCB&_Z0y zprkX9D-NlOIW6fS(2yv`7rTc3LFS7XlNb+&Rc{hi1AS^eG)kY2eo5aLV&ZWaN53ir zMpOCMRcf6fE`H2Q;<}|Mc7N7C6miw?&IT) zX=LP6EMg*PRKZ7R6Kf!jbIJkDjgSowfJf zhL?Z+97wUFv~J*FOLv+rTllayG~bxG?}df$Ja<2kFK}xC??O!j{Ky#yNrO2A@f7`F zP)87(z-g(|b98_>l{hWH)QJn;jLRf>p++)+{+ZZ%U1m$a&Xk&y46r$A`V`nBE;E^I zlPML$iVui7vlz!zOu3pgOvERF)sz(u1)qu+LarfGH^SP~^3GR2+3sj+u1l>5JYTlr z(AsK!%|{CtZLG_z>iK9($L8kzmshW>+t}_)DY|=R`?j{Jd~eN%pM3niC3>qX?4rQe*%rPwk1&ckIc+Wy0uVfYrSLJsi!)7Tk;P)vHG>6sWV#2+Bdi4|4Yk)iq0BW>d@ zeXO~u_uA$Znoas(r4qg?QV&0IzmiC2^m%(_oRc{NE` z*LNSQv8XkI&hfQ}78S~J_LHs0e=sJ9^Z&DLb^De!&tog=H@5j|HwWA;+k-78n_2M8 zbQ&u=9-H2DboavF4b=SPlaKc=iAiy(tW7J+mh{vtT{q_uTA?A}JrjfcF4cZ$`!ud5 z>M4m$w1&FMqI}} zomvHO;sHkz;Lt^!NGV;aVOUi1f~UOx19tW2S@Va81Hx)dpm{ zQV_ey*)5fsLkvLKE#?ICR$j;J<_N;eu_2$pw^i|%V)^YGL)T(MS2qz}JNPtJw{Q$v zL5KM)LK!?(=CG3Es`rkyNgl3RJCZ*})_myJ|Lzq&_G3 z(8$R|JTGU|9}5!7txkB>`IS{as^7jl(AvGU#kwi&p2bU>N}BF-%&)6X4|uD*NIzyL zdJ$g*o`0ft{i?^G{O*#Kuk|cn@pQu_YGp472!h7HuXZxM?J*t=fnoDG#9}> zxYg{F?!}(BNPNKCpQGWPN0Kt&m2{MDS;Piki6K{-Bntb#hTX537zcT<>oJkH)*lzo z6@u1_>4r>*bUOS?voyB>$naQ#=9~uzEz`}f>v99N^Y3RUNuLX@tDTSY_nIj~^MIu? zCXTaa=6a@AehqiPftfBFTg81Vo?haoU_Vb$16Imw8KI8-Oz$$2tESkP^srwitBhQB zwSs54nu?O8yWhXIecj=ezNTl6ch&mT>cVx!zWVmwgY|_gY=QDjl`1a2sA5f<-_^Xi zW5xUXH??^yx}R%mdU5>&=}BpediFMTy}qfI|DDG+cTTCf)8MLK-{R%94R0S0ZfTsi zdgt9sfAVs>*Ja8!RsT&}Yv-YLa~#esJLhV3nR$sW{+_@8Zhght2LiKK?K&82>ezmt z+v%H`)M=UFb5^Z8{M`oLeMh6~MqJy*r$H~O+ESl@U#WvEk~_RgltZgBZhaVI#s2|_~}3D9Ni!KH+~s^aigYY_$Z}%g=PU6X|m(Zus>Sn zgV-_my_XB(Rmm)x6O$?AM#f1^mZ$*vT`!_;?DtQ~w5S2S-6u`b4~h=>#M8WD`e`x0 zNQ#H~))h&Giaq+y(1*-z;iih-sUl~Rk`uoWy(WprCfU?yPxfLY4*FHS&hg21hgUW} zG!Q8h@gX^Ilgy$aAQo6e9l0*NB5&Ggbl|515{rbiI)6$jA2H@yT{E9u{lnTlM>-#F zf4E`y`DYd#c(iWO2cK-&>uJrmWoB2_Q7qAk15J%u+xukKp@8%0rR^}<2^+K&LwL&_;MlibTC}q;D|WgItjrM5JWBlw`vIQYkeSX~@3a5p95izdq{FrCqwH$_?J6NwiebkJ^L=vQy_Y3+5n>&o*8r^i_HP0Xc+##gwG?Y zj0NzF6pOAr^33OVl+Nh-ICS#EXMWwXzJKqPSL@v!yOuZZolO*Itf~6v?tP(?|M6@> zPh8h=-mtf&w{xbTCfm>qnQDbh#c~fT5*34(nfOssBfilviIOzrH^+I$GC%u=K97t9 zkvbSK1|w0+3^*|fL=33b=wqS`NQ0t?NB)A9gp1juueb2;W{1j`39GX;H9JCWy&(gw z7Leg<0DLi+tCfBE7+KzL0nfm=BnH+eR>AielqQiUZw=Xn@%brLA3+wu`FOAhsI%Fl z@3ioTvO`T|(V<$*>$%-v=3{%apkeawmFgPd0QGSPuAznBCAznvbZ9KQHIdk`X z|A~zBv{``=m^MeP~YEUBvPX?;5Bz@C9@ijxlLNX+s(JK|!O%e!t2P z(YGq-TP0tJ;m5*#js-Vh_{jN)f{7*RblBA?Nlzdpl0|%Z=m;U@SosS>FEgf!HT9`t z1-R9E6QX?$DP{Uai+u(P0i?0 z0m@leIXcEqf)V3Vp;kraC)Jq!h-9Tz08}*wKBWi8=c!b@Y!A>aPu+!nP1+y45v~dy zXKlxGtGJ*RA|&d3n5&gIa@UaG(4LlngR%U^h9Mt~*9gZ6Q72F%>IcwopIW8Gm^JhZ zBcY836qE_;)96)d#wlZa#4Y5gl94C+tDJ8)JW}H}UVG{H)}}QtwwtB*gf9%tZznEx>~%iU;_}mY#-+gwnmjJBCDUba z4tQjbMIKWQNJ8=gOLB7)l8|pR=7TO%C4Qk2Rf^RoDm%#Fy%gqzwk%D}m-8r}M|xAc z2YtoS3=+vlpB8DSNmfNU2^vCUJW~d7Kx3R7oohjUqk_g)hJLK{2<>FV9b)XjvE#;T zE9k5S4u}*NtH(Jb+?Ovja8hUvmWhf{<`oSwj%GlGd@IYY^O&1fY4iq`X$Ok{*Wfo` zIELaF3?z{}m_*(gOMH9k6Jg-J(D~h;j&6PP{ZBEjwd&2oM^&K_*acP=8`S|Z+-e1% zT5yMA9M}NIbS|{3q=vX_Fu;iV-8*z@`~abU^=u4pva@i%RB*pECQ*waNGd2|zK>UF zCn#heriH)_#0mlLQIl^ zLN7^!Bm*aril z4@xNpD9kc8zHvzz187!`aZ<+Qbxl%wLY@;*k!UbWDU>phlF}(zl9>a8DJ7&}-;#~Q zctDzgB+6|kM08n5i6bsz?jcLXLI+J%u{740i$!xJW3*mzGW5x@>J1xv>Ki)e5A;=Yv8Lqe4nw)&*H z|GCD8iD!8HuLGCV( zmvY&8+B(M_P0QwWzwm&&q`Aviv%1mK`1D&{E8eT9v)$d=p1)Jf#f(|m+P0aC_x2UNJ3i8sqN z!Py3V4rQx`vqKGGZa4lDn4>#(d|9JD3Ow%$4{BDR&otCH29pWT2H;smcs3@G9Yab4 z45gzWRG9?^&f)IElt~GgzeX01fw6l?B*rpgZW}W;VuDFD4R8ov$@|g5T%gXLjZ5;Wh zri$(B2R>8H8riVuM;mLWw|-N`;ZD>Azm4%|$qyhL2I20xF^5QF!Nf7f!h=&VaZFGj z-!`O+h=Ue}n5TpQCklXB(8@3q3VktTJ$D}F^}?y|jePhnjL;(WD{%aH{LeXDjF0&O z@cZJ3W{6R!%OI~NKRMzEP^XIZKd!m9H_~Zbq|;*qZ(VPpvl=l50fJ#b(val`Ejf7H zl#1YA7t}~f#F2205tr;bGwmwgx$p4D!p6g*(7S(>d-u@j3k2pmb; zJl-Bpb<^bAqvDTQoXG5)2C&Lu9w(vR+ajsiJhBsGXaBI%EF20w^9Jt|Ql-%+_;0@v zdUNz$;h6CB=x2g+v}?3pfX9tCx1o3~>Ac7vH`!$}mL@$8pa2PL_fw-5!$txOI~nex9aq;Pz!_t9~}> z$yRO59`UG5S-t9f$jlk$|D}b?gi{)Nhwojiu0^Ba)*wr$MuiL8i{xh~h$r$ja=5HX zmBZ!KFDUR=YogS&qFZE|bZPt}d`;rJp(7^}PiT)ffAMc{6fh=U2yYWMvl@cCkXM$g zXn05?FAFBT+eo}J@b>k2gi^JLZUq*l^C@3YFs4fRLO{2IO6iuO2e6Qn7k2)~3z|JB zN8rP+VSMdud<$rNh-8$3i2@Z;(mbv*WG&N3x2PiYs71fQ(}8>(4#-hTDXfT6IJ za*hlzq}VWhPrMsLU{T&yV@=?5^LVQ_pPwfrnM2Cf0i_%5^4>X=h0mv227I zCMnAb-I8sR4Aw8;e`jSw(t-xsFrP3E4pg7UY|9~IiHd`GBcV12skc5Z-l)e}mX4-E zW2@h%a{8UJ8|7!|<`RG6RCrZJp=$oKr#5YQF=qD44^}4Yt3KJ(w5}oVMXRr|bm5u$ z%D88jH}C8!=i67GJKVjdMI32-t#0+K&Mhszj#|&MMZ9PArut2TQYfySM884&PK;~&AwnsWh@XKvd5>8*J{gow3`ki5u%$Jn zIhYgTNP{C4o0tSgibZ2=%0E&jndfrq60?Y{iAtGjH&ey_;5puW1fc zbs7BQpu8%ZM@|7>Gj55al!TJ20$Pxu;-nO1o`U67(*R~Puga!-qiI!|Q7G{z%M`D^ zbJxY)3xh@FuKg|D-&s1{F{iVp@vZ8rPaa*kqus-=T=9#8%RISjo&Lt2{f!kXn+vOp zdvdGFm+xS7odddl$mp8Ky>}ZP|HHgZd<-8*GQbImu=)O>3$ofH8%lhx25AwV8q^@I zoH>sbvrK*ZtQe3sn|_1%9YB7@;0ed`RK19(bFIt;>i>=r~Pk7K^C#W-%7R9!H)(mr9%rtZb!>5gR_9<2 zGMigUmgkld9aKq;nI&yi^LIYqvim!^meF}}*%gcLC|umun zJ4`k1+lIQB;5$rxHb*+RBZGc5eVOdQPQO8%8pkTO02jODptxk_y+C>+=hJ z)^crracNq%H$OKi^RuMSLT5)WYZlg7R_%YZCM8fHXcOYr8WT;8zA1yGe)gtBa3Fr8E;N=TqFB`xvBOcqR`7Dy^pbmYiIDRWa~Rukq0C$x!?iA=?`WTv^7c6WJpAI{I!J6axoqOy5?!=(8; zPHo!$0?f@v*)8i1EdSLEtFNidyQH(j1*uxm{9IRgsB1;qWSi;8sUx}7#SR#o`jzLz z?zPS0NcFbkYl?Z*6aI(l+_vKSLS}w?kYOTS>VkW49(u}#{{eydl0jpFjq>5l_9T<- z!4xNCb#P;-E&301*V2TXIw%t;(GN#IUQSJ)^FIaa=N zNRAU&EG5TPWT5&PpW0xcY-FI)={JbqakN0<^>rAicoUmg05v34WNIM+hDl5gh(>aF zIVqXAH1;N|q=R!U$MHr%r()K~b8?IS!MXWyTXuX~-(2Hcwy-&SBN?IQwQ0r8mEOSU z24RIOud&)u(Kc&zKjBRvyz!UeC#f)VhRR1^l4<~vfyGwv5e{OatBq8^N+j`)T{TrM z#i2R__0bGPFAja5IFe=HB>{0HYU4?yFOqu(I*$B_lDV4u#QL1YP%jO{UP5ktwh zjDVOXFTxN|5e}UJhcShgS17rCjEf&v4HG&WmtDQED&L5~TJP@OTyIQM3nh0IXyFK+a##9A#c4rewn9_gQsKhe|#wM5vF*8Y7C!MeuJB1L59do z4u&0>v$=^^V`W}d*ixRok>_fHn6Vi*c<^K&c#_SkgCDVF17{NgAfluuP9z^0nI#p) zoKBU@d#Cto`$J24)AxRRs9tk<#K-rB`bYN&-1g9RSw67}7@_fH@R^ePXK|(+A5CPd zNPL9p3&jVN1&$R02oja{5m0i<95H`YZ|EJ(weTe2Gm`zF~PPzUf|$a;SP&Q z144h2H9Kbtu^!wSd2fsWay_8KEV@`Ru`Yl#tjv$eX5{=h$pzwI>djzb&Og@5>Esl# zRkmWuNl}%dWxIan3V?tgKANKUytjGXcA@mdw&Dlpx#PC<_UKz{mYyivxT8nU-#2gR ziSmuA!(#K=?uCu()BG)!9zWA2lbWhGt{L4Ay`t=m>0;0zgL_@k#Yko@K}xuVx!+ z6g4YwER0CRD{M*APWYJLlZB^W3wY&phC+eg3oOI(N2vUdpU^Xm023hkb1O$`T*HV&yLm zJzTl`>AP2b`r5;rQaa-$7!QfIBzMb zBo4f?y-65y%zZJ5nJyN-x!}PC)kc-EGi^}UFLcj;d|ruEe=ENR)ku*BwXDPMS^9-iF0Vs2d^7p_R190!koC)FTp>ZRs4~tDfO&GIBC9z9EkjuXYMT2riMB~V zl@y0y0>vwwGzr;)AQy`iA~d>eno%h3qpS~BU5+ack7Ntx+`0ALb#sUiolqR;BZ~hdQ2H?BkcNJbQQB8msysb-&B7`+G^F|Q{Lt( zmn~_tCB<{s694F@1UugLb4f+CF65r%f?kpgH6%KjgdvOE^&+omIxRUsSriaa2s{N6 zfYN1x`#(N-*D8}U-Xz8iNRwi|5OYyv5*I%ym#O}-Jx0732Z*Om&hj!oS9?J{IOtiJ zD>2!o;GCFD<%^M}B`iVV9V-HlRQ@9tOv%P%iZWTEo?ZHbdH(xn=X&P;xU;UTduNNQ zwD&uIsDZw$6bkqwns{Cz*MojWkxxNt7!$B+YN-VU33Zo9H6HIIZg2g6C-O>V^AYSoNgyrx+lqwcDfI`Jw z4x$vv40NDui4+Cw;aFty;*jD>$hDG~x!1uQj%DnjGmHajB<}k(Ix3;m(y!1jXpC~Q z7eZr{g+{FhV!%)+ZjTL!FgKJ9!!%GV=?Nt>=4VquRK>~=1-y-AQ5q3>^r&?$)k{xR zZs`5VN;nlCZ}E3j=fCGE6*i7OW6d))R<2tkw2d^1&8w`wdx4`C%mH))N8{##NQNfN z1wnEmb3yO_r-5+*T*5yz7sRGpT3&fqu)Q$))EkH_K2^C!8i^(-xz>Jw{^FP<1air`jdAb3N^h8 zI6qT4*|$+RKblQ?hUOasfOq0t5a7kU<5$iFVff^l^G$pxbnfuy{Nsm1Hooxi)zB*b zJbyB@o9_(yab`nkBk^f?;pjE>LiCY@)o(#FMOiA$FEUTrfKHOxWDr#^po)Qtg}@{z zf5rkMiUd@Y6~#JcHpNAWfjrk9Z9>wP-z*PZK6r}1@!ioL_xhKQEH)+RLmLJ4#dlP_ zBUe?Xk=m-@-bY!DBfa%gRfhh@%>zL`D9l0G33O>>9!NzQdWf|8=6N8eP3Q?Z(}hjx zqiY0%ZJlsz?V8c5wM6fhuuFJLo5U6Jmkf3&%xxX!$-V8+%zr5j|es{hz-k7Ep`%P)K^z3ueVYA;I^| zN0nGUMVK1)t7X4B`lZaKCCi-XR`=OxbFBQip&e)>wl9MY`!ea5o}a>=anrB5uYfi| zoEm59-9Q9Vz$o4aEdoJ2D8HfsnvJVWz)B6ZMyIFu0@2%m$gGU}@RhO34H=nJ+~Y1V zgX5VD(dSXKA*E*n<4&jmE(fR5C(9O;%`NyTJqDd=Lz3p))cl1Mr|(Q%6lU7ZT%`% z+plCQ`USbtKT;+5$`Gq>8aL~vN@uffeEO}kZdSyXCVm41Wl(3lF$7uZ2S25OpQiFZ zVDpOLBI2i9Z~r8jpHjU2R)wFuY+X%4N>P!*IDLuPSYIPre8gc`oFPq<9fGp&U-@TZ zBqJ0(?3v`zeIZVU3V&+oF`2(C;4dxs%Vq9lwqNS~g5)qM%H zIaa=7=ji23Y7flZIK z5`Foe$>Xk3BU9*dpPPPz03ynfK|n$*s7Lk?)*}$aJ#;(;^(gG!A(ctmDa4d9^yVt! z%*pxmo~qk?_)3#^dE468Q|1X0u$CN#Vi@q$++uePQ~j) zC}D~8x63cTd`EsO0eBa$U{HI@T`BscNwd4#>&miovhz#hHg#mVvJGkZj(C5KCj~rQ zv*lB_I@e#5zwlIWQ?g|}_;TyIJmSl)=EBW8dWb8tg)+fkPi(xmjrjm;`Cio`p`E?^ zMoh(8cH|vU|6!m+RU#=jB(acTqzVQ(UXi^Iwt9@hl(j-`X}#OlylAP%S%=q{WX+gg zWL@;cN{7#z#_ur}mrT-XwWeH?cDmoA)#_|%h}+%{Rq=b^n^I115@z>FktQfvJgl}b z$Qe6KxByc~ep9-eQgbEpf*z`+sp8W*g3VaWBQKNB4^^pWfBB9k1M9Y&R+l}o5M#sq zXXujZoO%zkb1v?p@?IaSk7hcNPld7-6WF_aL_P8>a)e9k_AsT1Jc@)Dq6AaZ$OlYK z!pjU0|7dYmhoA>>FZ1qq5B161pEgz0O%>BnubuG)Rk&y~U}@7J0%<70#gl#dR2^6{ zf&H4;ua$o3o^-mWFNgg)=~u>q(I@?g`?a2-pLSOQNwsO(N zWnKG{g?}_OtlGkC^qKHF^>e7TDy2GmdfkweifS^SS1K@|>c=bQ_#{0RpV@(GyoLxX zwe7IiWx%LC2F_covjpSdqyirfrp+j&LFvpA=Y@_>h0})>lQ@HQUP%}vR-z-v??>{8 z6#Tdl2_r?F5`L`|N|%hU%*$`-X`f}%{v_qNL2pi4wCVXf+V^^9J9-<-7L?^0Ofe=t z(;B*D`@1DV#ow;maEI^S;+jB>r?zL)mWHHNSaQ_4zPC2JtNrzPi=Jw6mF7R0Kg(Hu z&$Mc3N!QZfb?DUT*=dPMFW8LkLbt`scrPwItX{1Wxm?Vj?Bs&El()|G1!KuGO7uyN zT$6pkE4j5O=Z^cgdJsy% zVItnj>?Ga-qhoJ@!JAe9xLv(Y&#)XHL%@ zRVzy0JLRlu_1E+)t#nq>wyV;=-__xD%&Bkj?JwKsb-TTLOZU6mz4gzx;@C57s&^lM zp!ohfT!Dq(x|3}?HUWk3jZUHlXcy{lQaP-ZptwXt{H+IXiLrPK0B_Z!pcS?X#THU> zYEeMv~sxc!a#)H+;*uq@YGf){L6WSmCK9+=*ayzL5a$K&$(=2C;S$tqwQ0r*X zGNzg|kTY6DLvQeMIgoZ>HC#TOBY`bIaVDNj;UCoIhCWI8ZRkH(9Z}!t;9yGV<0?JB zFeS97d3OuHw=1owOISjMMWZiu)j7IC9sFMCCJX!m6Y~pnc$+1w$wNWP?K}fEjnS`@ zeFkKH;VATLWuF0YI~&xiLj$rVLZuFhW~c@oJ_CwoG}y4|Nm~Dar2zsh3ZOH1xv6|c zM`wgLsG5gf-8fFsPzD>5&?>y4O=Wp)!)q=04yw{eeyjSu$~JNlW5^9}P(7nLgm=Bx zQ+embSM&RF>VAAHcRE z8|%?|lR7Az(JV!rWyRZWsQ@mSt)Iz(b4S(OsVonrMTsTIPU$%d^egNbh&X<_Cs(ne zv=Ev%Rpn>q-Qmj4<^}jj8R!31wiir%N_R{*ab%VEWE|) zjf6Cf(=)B&4kmQ7?f^Y2X=&>b&XANfxv+u=>amE^ND!yr`aYV8nPuzEZ@6Mf09K-% z_sqsKeBv`JkM-1UIup9|LidqPH4m1qc&TmE)g#^IkL>B#@UKUfzw2H6>WVoVa=f<3 zXVrF=Wj3~!{(Yg=yR&}tdZD|Q*S`1o-CK_>doDEm!K#L*{_(N4eXHjzK6r6s+uqf8 zEIM>yS>3M3YqQfIPRlK6>hv|N@VXwed0t}n;FxehIH#F|I*X~?k2!jAU@j0hg+*;Z zT5yVnT#YH|a5ZARl7-L+_Z(<+%4l@TVq=XT>`S-C=-vG``Mk~BpDyE5@zu-U`h62A z1FQ=(iD?7UWG$SL`~j3`rA?-+Yue;|2ss=H$%dMYKBFbUP8g0c2eldUTCpjxB6wQ| z#48}hLI+Algj$qyiM&Xx6f13tr@QFgBjcuS5Ol;L{ePiNUP?CK}?sWcI8!6&mzV0n<7EX(;-5yWwRhn;dQ1+b#0{C zJXoZIfj6yjO&p8d{MVqnRW78HB+~?3(;3);?k3mj(t+zX_U+wpeSK4xF~{50U*Be% zVVTCbjy)pYFRJGDbZ^GuE_h;Z2v><+tz~|Z`c-6asn&~cVQgoC4SYWKRcLn1L|5b~ z{ZPdF!6-{Uo@@h!BC^GCxie9wjoa}_Zzs-+!c*?w@^)>Qzvfc+;;TB>^$*NxTy{}i zbD+(iBYDD?omI7AN$8SA{wsS!ZR7u0JvZv$0pM_ zl%9!@5H<>M57^027Co;T(xmIN6ivZUX@ngG=7}&&D*U^`-B$5N+sW7S1fy@QPcY}k zJ_xJ{+_Y|;@Y=f9Utjm$dl-|OUr{dUE-b-+U%2RzgFM3tZT=E zON5nXaYawdwO4O$aW*toUg)juZSUNE^`=%CSIu^dOU0*gT{FS*ET@=Qa9!p8sDuXu z2=z3hV+S%VF{2yF#kS)*vF`{w1~DzNV~~v`5FHoDxr%Ee7UU*jQl@MZIjF^hnYd_9 z&J|CSiL;EQ}B`)vA zZhyF~XP(pT%00WlwYa18f&z0#ZD4s<03=9cgP#CS`AuM4(!q*`#E}GmnrH6C0n~(o zCwZ2U9hIhbpU?{X?M3ETvtGJ2>s8TdE6CqZP-(=c9*w=jW4Xn&|B_8My(Ku9N0wtI zVyXm&8blP8?>T^OjWS+gIKZbSf)#WsWh6oCqGs5P@pL2w;>7b{F(LB=fW-C!_9G~X zWKgX|{s@*o`4JP906Gq=ZMr;&H=6(o5sNvRV}O*YS&07wCXJ|KBv!C?9Fj`g?AE6&!iiJ@QsK;#z8DVggB(&6O1DYNC_A%!wxlTr9e0c z9f*hzV~1ccHrf?3q2JUL4Q8MiLw;~n3!{ZFfx-BY;o}@rO-dNay|~O^>vRXZYD)9M zHvOKqZg+9RLT`J2O`xwVc59U`%iG{_Xf@XTtF&z)cU#zLGosyZi7SNT=r1*5x;&ee z=LUmLQbf#X5`w}zk3B4RkP2Z^Bw9#mKokR~CZ_$Di1$-EcAT-0dZi5JL)u$H!F}tu z?T}vDebOv;)IusEdsX~cWy3l{91=~$;vLO_aO{u~#SUaNHm7e^vi2Y!AXlULh;FZ3}N3-I}Q7UQrk$3oC#gB4T)IzIhJP1#t; za&)wG<>;|=Q4j#G5qR}9=o_S1;{}ZxFThIzjAz&^4|r_#_gHurtu66~*;sJpkL1%qQ*&yOCley|?Sr9KWm^;!=ySXP~dc zvM_K_^^-5P28hU5`b<`s&o1u~9*-^RIeN55m;>(c7-YF#k!30SE*sQgd=WPhvW{eF z=38_m&3sy@kOncS$c#`YTC(D45T}rr2x(I;HbNWJTp?;idq)JFE#FzfqgcoRrJb7+ zrR5gWpw&hW2!a-n1xbMvCoYA_mugXZ@0*`y_bT#mrY_vu=g`9a&=TP?%NW6Yy1S?4 znoXOVopa%jV-;V}xqZ{sw2Dh?1!NS1jAp_|t3{siNj5l}W}z_P&SYs|MIZLp&CZ9E zg7wfjzpQgLwa~dbKdDw-0IG#ZRJp|8RgMqnb^MU8|42!mxc*1r+RcV4%9c5qEX+7PmSY!?m)78P6+WI+M$+(uyCfJd` zs*Y<}9bq%7YQJn_gKy`K(bE-PS5w2bwH?dy6!W9IOylnlEW7mLr1`;#yTwg$9j@*R z9DHfs>5P(-ouuS~IaRr7ZXA6Xrwur27UKI^ z+}|NOq*81&PzHk+T3rON?2x24^H3>(7fv_qf>LwT1^@<&nia|*H$v`dSXf9o!KD=r z8q^$TfY_3Z$ViQ~1moml*c<`5Sf8`Ab6$bAu!_wd0Tx>Z##vPF@8CBmF7jxU6n)eP^?3=4^F5>iil3iUS&X$_PtEVRJj8x;&SGc<4&cY$Th2l_F+PiCEm=VPX}|AFuZ* zG2+yC@dR=0Z1UL4=er#)p`{V=aZh8VIF7Bt0@9li#5l<~3N!tFU1dqk0t z(G(04uO@bLa=-*-tp?Zja5@Jp2$53KlHpuAe1d&gidKOhk%G&h#UXFd2)UGvmkS&} z;MRe)&cfV&C`1wo(%fmA7l zwN?YJ3Yo~y%|T{z5MiatlULxJbC$2Yqpn==DeQjhgrhq>Af0UYx+l zsR?vgPrMPQ{QHM11j=`R3)V4geQDaU=aq%5l6cN83r15()qpr0HW>B9R0uj31LT!i z4Wc!ALMB?XfCWXdBH|QgdY(BBaVbP=#2h!4XbrL)%G3jhF=V29)f2?JriaHU+Y<5U zri*WVAzVZJYZhWNv}{v-B);y574!;yj@UzM#Vp6k-u~Fe*ki&Wp#>aZ3LTr$Sea`b zd@lX!Zun4*?3*c^ef3z>xHTFj57_OGR>8rkGDa%M`3v}z{$)LR=ZpgggXAX!1=>=XGPLfi`Z-zf>vu&igrxeYmu zha1IU#=4Z1s%_(`0iS!8r~#M$$Mr4dQbS`qJjzgDnM-sTYzsZ1&R|(yC^vJ{w&6|T zNofQ93)N>!QftWFP7bF&>4xssUU<`!G}p+yKTBZ`WB{GKS{X&DN&b>&ZOL(b{jfE*UZh?)B(wjo?* zwV~>X+C51V-bfejtrs#ie3e(4p#ISCvNS8(rsGR|q8|?ZGy5UQQZPv)elli3kGJAz z%4!a-Q7m3dH6v()jk(ypr`9HdTRMZoS@L>Sfb|OVGs;R~zmdq^h_bLKIUS!#Uqy2f zxEM^!04W2Xm5~yVO0tO1@e~WyWMaSqMD`k9%k70bN zQ2%m~ED@e}DBIcsHetx07{~Mn_n7-`N<|7e&yY$_3`$z17z;oIf+b1dfJ+4dU_M#O zk~J%~lUvG@j1%$rmv}8i)g{Xk*CHFa$`HV{xV6H)4!b@r)@r21oD?25t)lUK%!;pS z4sp4he@1cv`pZn){DO&dhebqlRD=8`-p4_~G)Aj&UC?90lkg@Eek5O4stV(3pu^$` z=rHYZ6vX-FQhJQI^zwqkX|mc{ptf?l?bse1(xK8EAnjyuf$bX)09;Ve!wQv7Cff3P zBTDbWY5*!MaozDyrv@ABsm7Di(Xz#4FXI*yS4a}_BpOIMSP#zFmFjNbUn0*WaUr=-6OW30vb(Jfo8PalK|_&YC={Y+1pvg z7@`W`+JZ88@x=5b?X0^%z*c;M6%7e~upjCzgWGlfb+wzHzyIsO`mjtu`+t_+Sh{0q z{|Ece_V)klI#AFKVDBY%mw3-oaS14FwT{M`1RbqTyyxWGsg$&_z|zF}sNMnpBpox_ z{mP6smVJ-{ryz@q6q7rWMeYbDGK^_3z^1YhDt0BZJ$aNJ;2BQ0sIU?4KlUue1PK>= zN;X2U&0-eN@c7yEHqMM?UxehUx5Z7T9TQF&r zpa7SHlI_Q<1OY%*jZr{Vf`+oxGmx%|>vzJ1n3NYUnVFSRC zzY1e`4vk?ijbYh27}u}l9FXn~LwAFC>})s`gAA&=hx2^nHZODFCjXYbKKv&_wmYVUN82 zg|Pn3O&tMtqk9>Qz42yU2qDk`8E6l*2`7jfe_ziSyT~ zX~vc`a`tJMYUs%Y^wgb#_a}Wn5q$Nr*PiQpF82Dde|*yU$??wP|J8ZoIQm1yw4(<# z1?ugB9R_`FoyW*L;uhA zGgd7d`q`Z}RilL%82$`TTFeJOn!3@?d*bU8jdNMIN6_XnQpZDR8kUslT5=? z>4^IL@sqE}IIkzhh88G>$(TJ+-;{QqF&SceUfBr!Ld>28U~hz;L(NkuK5x zjo92sa(${^;<~KZR@}!$twrLucd+j%J}xi^%0Am*A@&wQfyJwCs}PE{G7UnpNa`>k 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 literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-regular-webfont.eot b/fonts/quattrocentosans-regular-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..346db6fe355a585730baf6c20faffe897ad17ff6 GIT binary patch literal 54444 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%0AVfB*851$l{%{<8e4lKjQ%b~cm@ zIhxh#3`bdKt)t|=!w>HM*FDXx+d{qVk3G<4vo0I{e*5r+!#%=>&fM(EGLOC_$yMJ| zl`E_p67(PL=^7pTM%U4i+tOVDd&3=h)!kLyPxM#V9pNWz4r6XkP5&c{oA*A{G<5nu zAKLuvgXIo?g>|K6zTeTd_uQ8H8qcCO*XK!1t7#7sCQx-oJ8tT&_kq>`u9WEIaGEPJbO@*&x>ux-3>aUr`$aJeXpjaoQ z<#c7~l>)b=>EcY?kDE^ZYklMJ&Z^C)H}@U;dh6Xsp83n4dOO{@1+^oO-aU*nO`}>H zI^MR`=a#u?oU&ORuY&etO9N zyH7_h)VbUKWB1X|e%K=J-5q{mjnDmrr=oTA%y9T}_}5Fzca$`*IsJRkAGFbU(Lai8 z*Rha^dYBgLVJ`L1&U=^&a-^E%44^y_oh`&MBNZb_VFsR?>jY1NI(%;dY4n8WcYR`5NqLXT(brW5TQ9Dsf2D_*l4mG;A1+wtEEaCa_wi-I?1o z%+TgkYw+zuZA`UxMtpbT1>sn@U1@1zm}IKLEAN69cC6+P<=)6Eg)v@{%!;PSf(h>c zC2VG>OHC3cA2bH!1U=CtiJYx0AtQ@M25k15bCKB(4>ZX;&PN+Q?DW)~9Z~qG>tNlJ z-B)hLM@6~9rf_}LZNy3nuGH^v8o~C)wFjOdjxI^#A5$5)Fi}lWC=nDAOQl2;xoSWw zSqE?wj|tewOl}|te$PB9Uf92X!lh}PI3+@~i0dYz^K9^h(1-R^z!-0jH^zN^U;q_r zqd&#jM>U>#l0*~7b>w3BW&Un5MrKG0Hj?)!VL~QiZQ!s9#gY0$}01lt5C6^O82M^{L^OV1ls- z9fj5@f~HEK)?qg)t;;G51>Uwm+=Pao7fmdjCW9?le`Q@nmZjO!u z?iYmY9!p7MJZdu~#w{RNYN=#VB)K6b$t}Q|CS_8RbN#MJa*7Uv@e5sH8ho{|1Vmz?ssez7t>lbM!!s2%A{yjDKHMzMy6xoLG z+u9`bcNX?C*}xP|O~yd1;w(lU3(WTXATD*c8b?3EL(gg~ zxsKu#l5GaFY|Wy+TeBbrZMlWuH#t8Y8p#rr=D@3w;&4&9`7^pdb$Rgp1u*PL0CIN? z2I3r5V7fZy7l`jfh)5d-34}y}!)k+G0%()(4%w9=BU<*zl9zJ{}$q|69lR&u$q0 z`QvS^PyC$oBk}*soUZw#4fqI1_J_E^l%tTA45pMR8)AyIm=lthgDeU;6Qo6C6B7p@ zD2?XBNkJ*cEG6(^5t}w>N#<}G#~VS7Kjb9dvtQJ`7}j<*IcF`~_vH3tQ6l^A-o1Rw zTkeTtVx1O}`7QOXZ3BM}Dht>j?Xuc{z6aRKXe?0+iz173wS)RQ-zTM=my5xQ#Y)_J zE=F$Fag8OQp)ta*$v&)EVb1iai9`#;ZhNp-=~Z>sJby(dur;Gym{Z!PiIzrJF* zd*pkr_K~jUJwuDzwn@vk{(I-%Y@gj)u%OM?(6iW=mbxJs~ahpkgzSPgJF(Ex>J!5JgDC-PRg%LuobS zk>YOaso5&UK%A1ODopx}kLRdj1Mi>S|LU=0uRT`1`K2wRZw${D-cXHPd+oiK zUV2Zp;M(TCACELOjr^F)X38DcWTSm8>0J&VRImrr1`5BBGKUvUi>5@7MuqS!CJM&m zq9~j@JaPF|NZvm60jOai5=Ii?DAzHw6knaB_lKw|S*7)J9YY#Lamj^54Lp*u3VEFg zfJfzE6IB@rqQqslw#u>6ZjeT(IACLXJyH_k2s#j zG#J+vPteT*?S9= z3(L(Rg`2EFZ%q`V;_FsH>qY|KR4<_Emi^CYSK#-y-&nyQ~gquwxv?d|%Wy;uG9V;6zI} zLBd5y>)?`CpqP-0<0Kt80F$Qy2k9m`52(RZMZN{cbW%{M2*8)?Ya!-G3 zr-0(sPq&e^4?g2YT`VWXOq^&Gd*`n{Jh)xTdTgCZU-Gjp{RdXg+n2j=)or~m2A$6g zHm+^)2(S<+wnswUbzA!DR`m(qp)J9CAu~lDb9{l*z)MyRr-CI$ z1&?lgtS~8;Z0|xMh~m*o258A9*$F6+^T)kc&SZ2RmTVyzGkS9<(Ut-FVD3&<8{-$U zhr)|-lXr^i+9w^4$VlOl3VS)?7TkdCsU1E!H1?X_Fwx>}+tg9AyrU^6GyAr0uB~1# zJyd;E^oj1pU8UQ;C%n`5+|JgMM;wi&w3gBP>(U+Wv>}trZfo5!_FF@O#ot<%{V11N zh~taLHJ^ds?q#6@(rek^HA(c#dGPeIQpBueL4zzt1P-G7SHROf@=2JKhVl!D%K5o4 za|-h5w1A>*GArnrqkd=l_4=9&KCzv%MF;K2YA{LyC9ptpW2R`ld@*Mz$QSfb{8TnXu zq`(vD#mwXmlYAH8C`6Lh(!#rNvmCb8O5E|@0m^DMNr^<+yUfqotgg}p^&LHQxzr-( z!Fw;46J<$`y6VlAF{byHH`C(|vz!QfUCKqST;Uye(XAeHD2HC*7B5K}8=`^Ma7~5%F#_PR&WCwSToW4(F#O0?E_PuwPy%Aibf(c!S5Ush)n4oC{d#%d zuCB8CTpfEC8jOaYie;r_@Z0Q0k0((5clARZ{<8A2)5HBo)>ZC4va9~V_R=@r^0ihv zT=_NK)xpl{f`Z!R)s>xxS@8+H;^?7AesBHPU*+s#w5ok!1PN}_|)$z~#hPbSNlW=qNBF^1lk&15=J z^p~6>(b*BKGDIK>R<9ok;^V8^`2?(gv5)3Gp>q;ti6#o2NYYY+0N|%KTA{RynKu!l^ob4UumqMvhK=%-c01@(fRD3btIAMPNctZ9}7{ zQQLXk8b$E*M4rxq;8O;Id--Tr2eCsrgqTsMAy#3QuTYIn#{9P= z`k{o$A1JHI1W_f#oMmK~lZ))osBrLbSQ2(04jkhhj%pA#o>YIz#$$c+d-X;{-uHnv3$48m~M5D zhEXtYg+fK%F&StE7(0aOys3OQGR7X4c<|YWH$D5{{P_=@ zczDxu4^`}QwQlNYd$`rU5P{jy9 zX5(m!qPP(5e4b_Gf)#f{ZB(U09{?h=A9$Y3T7x+a2i=qtge-1il*}c|*VVzo@dsdX zZim_4(0yN7<3OX+(X_Uu_iuv}S2V9k&gP)AZCmK>l_z(==$R;2t*-2#3c3Yb>*yNI~R*OG}b-m+ccMH^zy{@ZL@b;~$9oQL_ zN#YxTF`c-MWKtZ_Ocb|3cpEYs(*vRykAfcJ=WeR@C8>^|^>x&_vaQ=J>E7G!4NUN@>y*5<67;78D;<@jaWzIk z8u3sLjRMTzEazo1vho!lJQg$1l#P*(4>sBp2C!FF6r7Pf9LwDuMIZ z84sN2ljMizO^39hv>YGV9J4HPMkXl%wGYqWcgum_&5}8bo~2Qca$2IBPawY=pX9}| z#PdYO@SrhQ+~aH-Yh1ZXof|BFt-hlq?Wcdy`jZ-$`>CGZ&Ll@=>F$pH3X8wzyB&k4 zTDI0#_06~VN<7IWrn1_`qD^}}?qNr;vC2B9*pXOZscmX5s9Af*+*+cU7kL)l>N8j& z{BV>jF$Ge1QLh-MdTyMC1fFh4`N$zSDWPVvd{PF&guot!33*~5&q&VT31Jy9Q6&m4 zK(ecRayA%N1%~Bbncp}fesM<$F3rzVqZMnBCyo>Y-cZoc%OWHw`}szmI0rXrUb=2*r;{0wRDXuf}m z2L?@@Wpq+f`vtE!GO@d+!*9#4x~o#`zSbsnthYCB;(ay3laY(Q#`yrLi!=HFQ=>k> z1Hu~N{=MP$=U}Z|P`O~S2$)ka?IzSi|4#tV*Mkp`oHIO|oH+eDS0Ifyo_!eg&k1nP zIqcsTT!L$#U==EMhj+gcVd4Mx6}0i5%EABI#1AL((H8u&sxaD`?gON5e+3_)Q}|wZ z^Kro=xQ~Z73Hy$RUkJZ0d?>sX-Y={S2g2FH=I{u7fY&BIg^)fD9pGUhBv)V!9DK7- z4hn8Ed`kGwOiPK7qQJ~?UQ+HB@hM5Y6b@8BEo=~4e;97f?|A4?pUG^<)|%4XjfIUv zK0x=e%5<%%E_7(EsNN>3O_~1Jw*W@C+*BgkkMj|NeaNsvEDSOu<|9;*=Me*5eZ7yc zL--_IDE@iaAR30n=QnPcsKYu~^3%D5kC6>kjoGd=*EiUh_D!6bePbqm zPmpSSQW;F!3b>PIK~|;}J=U^~R6t&rv;^r!`qOHrBe`iONt!D66k4IfJzp2=Adq;a#&APj4 zl9Fn>*41RUceGfL>g`l}Jk0}jw%a;8a-Cf*0pR0g#3qz!ovee&V{;Vz<@v_)r|^dev^5j>3j%*dZs0G- z;{!oo6n{Z@+vH-b>S3~vtS>Sk5$l3Y3HwsE77+$*l;IvlAR=0mSaJoFHJ?!oB6SIK zLb_N>VmX?n8?3#65HOWae zcdf0>zO$nx)wOhf5fFCkV2!P*qdm{r)m9-^iGh|bXKqJlOM2Zvvj-vsIN2|Zs(Qrb zwC<82xlz`@uGW|&15LA2iCJ@ewAzVJf@p$EGgaig@OY#WtJ|MZ(%{MN>|5n^EXmHW zdg@DTD>tsS``u~6L1S5YkyfiUIZfJm0k2l8%g)3w+!L-7x}Z~|Ygx&pmy>T;%L=D9 zmb9J_Zdl6-|8Q(AYqsD|jn8}BQ`%GZN^C7_xK6$Bix)LsR{*(n3uwy#wA~ZFta@5~ z5PKiEu}(c?qghXuUpDE+rJ~Z1%|s)_mI=Ls)O1WU;H8BRETnPLeGO9P1!)%NGpt7> zjZYI-O%a$R1ICCh1t;+0!V;mmERtTGNFhpsdBxp*l$%3VZ~#mhTZc&A0%RM&xI(y- zf@Ef+st2iPYlqwR`W&vUjg7l}xxRf}!-GE@DBtH;a^Iq+fkpYk%N;-1)M^mS;ooZA z`48vLPMlb1sP%2l4;sQ>2%`Ruk;5zNhkNUi#Ge@!bgd&gH$;Zje#8SS*&gUMitix9 zSMvMh*@*UH!A_xH)&tf8OsTLaQ*)jcEIHs@DJTs&INLk0utRc~q+H^RBs_sL5Q+Pt zB)$YLi4uBfeP%8n6trBzVV24y+xee=fE?WLfnbUpXj;cF+)+{Zw z`(^d*ZR(nlEnDjS?LL>k!P_*tYj>-44HEQLZW?XMA6WkElD_YDx+>j|xEDC8@0{Bp z-#gI%A3ZvCR(@ux^?TVyPl?Ck<2R#@d#IuD;RQ#kp7MFzzQZ*~Du?Uq9(&;gUUp#cYC;+t|FITADz10#lvDKz|y zN#H;&77+7=11V-)r6X7Zj2g*BLa{s>WgJk*Q!#h^X?9&2rIsOwsmxyHvX}X5l-OQ) z^?!!nE)`aUfAA!&XA6Yixv0eYCY~H7s<}ri0*h>kuQjH~)k+*>E+R0Z>QKMua%8OKHdev-xR%Nu$T2N1A4fKp; zRC%D7e7}>kCQ|e2X=|C3M14~DYA&{-OQ(&(^2W+cA<((XU%AEWclP_MRy6bv_ct_F z8jOL?fqBZ^c~w1&@59~Aott;g%gyN8yt6bnL%iCu%NuBHUfbmK+PAyC%}Xl3+0oH6 zgxS3(x5jb9{Y~{Nm*2ZEdx@iNWzRi0DzswiknpOeANHjUI|sqjC#Csmd7zX6a!n@i z%UTF91TiVzXhD_+#5goueAd1*&gEC)aQOWWM~RS0XY&djC7L8>AmDWQ{jTT{jjOOL z$$+LEdl-<9VwTnP1CNEe70@enldFGP_Yu+uaq=qQywMcWLT4pK& zcB8L_JlB)N!$aQ^z^ju`swQRuRPY)_UKD%*1WW< z^~Ivr&0Y0d-I?}pF9^1l*ainHH@YqEoh|JfTZ_eWo8Ld$({uFw%_A3&thoE=hnv^E z@_1X@W3Q}V^U~vY-0}EJs~U#8%ks1C%XB)c+kO2{_*@TUdjs7xrNKBE7B7muntE6x zv)P|9Dc2`EfVf*Y#>j9s6XLuySjGwz#1$w@-L){)#vn7+AS32RLY zpAI*!T<-~VG?ukEt?u5S@2vK6H?PG}xe0u@^_ZOD*ttfh7K zDVdJhn!r378YN_okmI+EMo$329*moLj2Zf;r9)Y7_G-C5FJ=X|W!+g4lT zTG;NZ>u#^B>t06vJIY2?Wmwxp_Wn0xoJjMvM8&{{hF5bNQ*DH2Xler&9-uLvCl#9v z8p94x=Bhs#4kXC{vmlw91>_}yGDgq_GCZIxFv2Qx=zVPOaa7zjv0gB(5-j15R*eo0 z3cnaUcW&@Ezaf6!D)s?yZHP=P!g>g@i4%b}OsBJ=X)n2aaWirdLfSNHil>Ib)s6K( zq%}g}LCVoY<{?kpOI75c8V6bkLUIsJUaVXu z6O*r8&IL{3)rGeZIU(dNq?OAKh&QZU#P5ea+X}L50V!T%PKB+`{Gc z8J?!9zPno2W#l(6@iqlo^3&a&C2Jm9>}$NUpmFfl#!m5Qo5R!9Qq|b7aDg}A$hbY* z(fdvBtu|ZdQeQ{C%T2O9KJUg(`9EV~GP_CM4Rmq9N%pKx{1V6-x;c}E&QR{0zZqL^)l|JQx>6B!E5yR`{F!! z4|%kX=8FyA-n^xGCO@7ld*5FGj>brtE=-sbYw)p{~ZZI=FqYKNn- z%dy~&(vlu`c+Xrzio+Lh>9y$x`}OU$jw)ZS%|w0e6#Ik@&_jdS%T($A_hP zFNo;#QI^t7n6t5)4Hx3vBK1+*T! zq0(DTabg z+A$2H9LoMnMNufy%wsAsBNJPj1!VyX{$(}va~w>9^Jc218Q|Fz@HRl} z+-}HCc+A~}c* ziZ@wkKG8U)j>dcy^Rqx_zn@&ulrmaY8mjOS$tp?`JY%=0>9Kr^hW6Xwr(A1cO~G|q zi##Ooj7XKGV*$lVfwwg8V1NpWkIfBo z%_lR=cmJz&-;FHP3~gB5*YyXq;U3kqTd&KPjzW zUd2KVnoYVfm4#hrA)f)(FT>-FgcVw~47tR&vD2GQ<9ICy_-5%WMbEn;@sws|T_zWyzBX>2(< z)W*9NtW{?RJ-Pa1J>m_y0dH<@cE+_{k=8%cuD`@%CplizVbfqg0BCv&2r3eMYMr%Y z^+^SO*+?4&!0tloZ6aSB99!^7yVcNEAm&8CgDHRwItQU2qzXbc-GnXT22Q+V={ich zKj~;%SmLnz{dPE-;u-dZ8=Outy3}FqaGGKVbEu4daxA7|u}i*hOvi&;+!R)ypK_5< zoI2)XA~QH$t-zCmeNV_W&%J?6&PSmAS+yZqNXvviBC5sjUJ_k?o+v0QS-2Hu5Rv!W z+uGXW$`cn)u1N2xbi4flx7V+6Tk2dj-@LoVRc*QYPHd67U~tz}yWEv!$e;TJw4ybX zDS)ao8j)sxIexJ*w%!;snvzKA8u`*=e#(~~W6O-B^O6}$BH%oxw1Om39+XyqU#vl< zoenTjbYLw8tvQbVsRX;(D84P83ug)4;lpCeL}z&I{}d{P`pK2Z(XJ}{Vhx3R_?q$> zcv+^_lvk2^;8`D_)~HZUs0zxtB(6zq06 zWhncw?sd8yJx4Y3g7%j8wwGFpjE!CP%C?e{yImqo>|5R5+A2?Gna$L>|Ki>yGcR`c zE!EtvR>#cjq$%Tjn||o0A7hLv>nJZk4m@6x+Vl0%B0i38218N;#Z=(wr*t9$1-m@)MF19Bt4t0YIge7|bjmh`*YOD-Ct4_` zc-b8f+^SeyK!D&@Vg-{@L)&$O)FwJ%Eu8Q^cs&98w_#^Kno)!jG4whytKO^n{5P&Ic*TOK6Q{ZlfiN=VU7JoKz4!heW-cg2aEUGXQ}Z z2n;_r1$ST(n-ZbaKxf*lHHlc4vX?Q=;=L8rqxfrAjDCloRF@Viq@1MS@86;>PG0iy zmWws(CGZub+IJ8s12~lUr;L1(ej)B74xn|h_ zza>xKjBXl`I+Kxj=D7IU{^Jvssd47v;#@Kht37Rh1M@Hq3sH}%JFy70Km=W_W%+rPzjSq5QBh%mIpM~bG{!(p%!nZDWvpA=?QRuq2O zHUwvj9Kt%S!5qFv|K&RpsCwhH)&DKnqVph3gT4$lp_3*?McQz$>C0imbd6?9mNZg! zG1yLXy3NJ^7jh%ru`g;ZS0Oyn!QIr-!{a0;Jr&GJ$jE{Tjkyjg3U()&_?jZ^cj zuK@8Y@hWl>=2INz8k)u2uoEqInv~~a{4JpI=b`bZ*qGPxF4>qd-lYH;cBYQ;t{epL za&ZlJI(-S`GmgtI!@=>Jj@Pe%k&8D>@@RgijNy|UFC|!$iS>>*Vg@_CR{FUMr3`Yo zI-VymMU`h`bk8=)#1`{$IzPIdItDtWVU)t=K9ORxYo;UX%b6oyzrgV9op~xxC0nKLU zZ<49VwBxH4Fc#x>Oy)9`)?VZQ+d0uBshC)$=53i!243??y(Tq1F`sXXiaekKow=9axlTSS^gj9j6?ZP+ zQB~I--{&!t_he?sdq^^QOok*RAu~)KUZh-l%X zUZs?(*K)aZCPd0b>$O%Yiq~7O)js@M+G6dsT5Gx5dPS1^UwfaKIV2Mj)bA_#hI96r zGv}PW_T%im*IK`Y=gfdLeb@LB$BK+M-|b7ryoCmX!Yx^QY;suB+5$cK3^WVi7$dMt zPLhP$%OrJxMa)uY>>8paK;AebU4;$W{st6VEF~B z`$X9wROwYH8%m&N1bG{^e3g_21A6j?a!&W!*XFpdVtvR88DB5PpL`K-D2-Og!C@IT zB`_l`4biJ#c;Z{hVQ3oSiNOAWjCP65L#tlI=E2iak3w$k8k0x(onrl4^cryM5=us4 z@qBJ?Zkz4blImVRye)NUuCp`w@c#6s!A@Hz0BHbd$*;Z8zOTOqi@oQiqpEuE-m2vd z4h5zf7%3AEA<=LFb@*=W$*0pK7any)=rq#n8uqtX)~~>_eud~0&9DiBdkMXlh}Q;Z ze0p19_P{M}3o7u~>E&Lt<%H2bi{pA+Rx${TvJ9D~V;C*KrLeOVu8wrBhuqxl_=<+u z+!#zSri7iJ$h6-)pb#We?y5{)dhwRSMa%Zwu<&agMRkkn$~$^G=G1nrDQMW>$W32a zR?%MC*4EZlmQPc~n4GgT;Jcd*-BSg-S5MP$LBAiQX_N+O8bWYI5K)oY z=Yfbq&@{+KnXL6e5_c5fO%72!382P|yqT2Hm>Q)nbiJRIsSzC43xf01=o)un;b~nM zloUNi>(oFS>Z5-8Nc^Qg>!2Ho;E-_H6+?FS>t#0~iA2cm>Le19ot{KOf^)gKnW+sH z&9m_?2Klg*URYlomSkF>o45u*$=yb2U6zv;%BF-WQEGISC4Tw!aE?nQyJt%B6QH!K z%a3%I#ZpA3Li3dZ>Hrt=4zLF8xW??87rGr^hr=~mJFX@P&a<3Eb<8U$ z;o+ZnL#Em|tC*zPR6jw3_Lji&shavhGI+i^EJx#-a6OgI|K(a*d2cd&WjsfgX3a|# zo-)@jyTR_xi%XhflXsX(+>U9MY_ri_RuE=1Lcz6C*Yq(D67uinb+-x;JfGH=zyz%y zCE#sx`#czu)L}@{!o{yuW!+>*&nG(3kdluDw`z<~s)6djCI~EAps(h>GW4as8RYFy zP1JmB^R2$RCTtrlUo60AqIuReRR0XW-c7165(~LSQ)>pt#|@JLWwD8uMGacR%Tp_h z;km-EZ+_9RNApG#Eh1PX!?nUpqSowdgch$%q%dLCmyxbWsI>%p9+$$*9;4W{2+t;G`sdVs`QiYDH%Iup90~XAk9T>L(=0y^>!%yQY z*jCY8meafD61|JttvIYFl2b}bspq=Raw0X)-9o!|>rhrfA%B6^A~c(~kO0~zSOGuL z7t=!HmgE#WojM8QOM_SlP?2z*;+P`fu(*4WzHp=*U<_ zE$uzuFSpqib+50w_e@1Z+q;8Tz`^9*gg0tjTD)`rxY99~r#5zR+>vA9t`${{xoZe^ zw7zS7SCAVfnS$UWa4bibDaTPpb);tvaJH&Q_27qXVL{#Jf;!bDqIL|=6na(=F0t5? zfiKd~snzqMvY@D-XQi=WHuS70GM`7&vnHgbW#ks{aBYmQ^}w#kSBB^wzHARVD|pXT zmVXwE*W`-U-cXRQqvaW%r&bnQv;3;o9;ciVORClyQne;nI#(X*D+vJrtMzQls+myL zx@6~ZF}|jFJw8or7qi4~)iRf9S*k*yA z1rDsuKC_r=CGoT36L-gt~wxPO8C zM&nqbmR7~|JT3XFfr0aQS(K2a)zxJI$G-8h;EsKe#YrxULOu-(w+urV#bR~!bBIG5 z0+UtI)a20@G&Y)oJfs)+=r=q*k{eblf~y8jBh`(P$?L{dIbx{CW~$^i&FRreCz791Cw}fo!eK0p;QHWod$hXA>fJ&BPAC% zb`)bLv#>x%W1?~HOjICP>hm$v39BL3h9>39s>Z>LFRE$jUc^_Zv!^CYlYb#vEE|z4 z+!SOgmy8`9$)l&S7_tTZ&lUP_EZ+q5-&n7qkYvDK1v&#-@MVpCXtK-d@dMRAz*{;N|GgX22KNM4xDbTPw)rU z(0f*5FiTv^jZbb}@69!)mODmS=3gC4%MY~8`@r#bO-ugj;csjkURUyvW6^ExaFleO ze4t}9xJbY2IkB^KVTZis^rE`%nu=9(S~k`6RO}o%d2G}9W4CnNKis=$@4AMDbq_2) z^;FG@om2*gq5M39`jLd#%Xet{_p$Ua7zcAkS`p2v(`Z)h=6z8Jn%HcV4u>u$V_@%1 zvJfWCWE~Dm%bLkmDlryc4jlSnDoo>8g-nQN!i3mIr*jp3DB743N?*^pX;&i1$|qC> zUI8cbQ~lJVr2UJC)|q&UcR1&jq0N);ZE-+{e&!7cCERs$g zT85^i4ODt2#h0a$P6iEsur-~?dB`i+C<)Z zc)7VIC^u1a$CMk@+Rt8Y!9IEN%=H@IGX2gNX@31R%jCK1(hna3W!H`T6<`j7aW>aV zyZt?86?#mViK2me^QfhQitL}O6k@JYD256|)}y&fAsQfIMamFhFBu$ZUR|pI*x)`x zJ}0T|;|i!%j-~B|RMRVC$PnX1u{(Tvel5(d>#Ci!Vr5JryPBhQu&Xgm3g=VY<67$F zrm?_2Rjb=1A19B`?bqAuTP|5|RJncm7*v&FP^FnOKs=hyJjG{(q^L&B!7k#O!AFzh z2+7ESA~c?WBe2lhUt8l2yf^iF`*C6#M=EUfzej4Lcl-3cG-&fDS zCAaO7575~=5ok^x5)tW2={ed*PpMAjLvYb@pz~WR$T>bt69rF^BbHH2@F%ySVDOSB zw|j)c;94siOv`-|;)4K>^kg*fI_W$Zh!vj4Jm5(#w>(M4_nDR_rC6&fB1=TTa2J|6 zV%!D6FdFEDbX3kTti2}UJ9_m7bouQAZWndN9rtQ12-sST<*#CSiEkY?xTX*BIAqT+&YozT#@b$(q^Z}>#~ zIJGnQg5l#jecqOSp1cwHZYaUVhgEEP-DsI8N6K^jy#m`X_^bHK zst0A|U=!y@#O$81tehyT&zPU!3np}i9#P*i!LWe8R3>2jDW}G~OGFuU^ET)L-I&K6 z8OJ9ibIV!4f}@kjLvJZwo>~?Op@{g_-V8I3OiuLdf=~!U>Pj=nrvL~;CxtEZS!ilH z)xy+uIr@$>v?TTiI!kWm#OIU~NJK3T(MQUH_lI_w2)~|?#6RF9cpUO65&ty11X^vm z3jA-OSYk22FFiB|P~E6oRnfS#m^UTBavGM3+)fyAp#_(gz|a(3cPKoE?0>^~90b7` z7fy@(8N}cZ=k~xkun8H8i6?VwborRziJ`a7cmd=rKWGC zb|{`KI6$o*3Mb$FC>-G~K$$Q^!Q7M9P@{=l3&&u*7*f#YperNxh8b_lD3{+i7>@oY zYR#ZpaS!2i#L@B@$PddbI#DxVH89;k90q*B=v^9R!CDaw^|);+=cM`lFyO3!xhaLu zDz$LLJhwG;wq(iATz*q}c%)Zebh%>bN-gABM{{$VF5H<>)%-J(`=9>!(8wP~es5Jv ztr85|_;~gY#&?u+XlJC++KuR8sJjI6sK7{ezKb#muZ&_Ol3;67 zvIIHTB3aUy)`Yb$QkImEOOq3h&R%dT!n3iR3+`D`MlA-pUXx|WVxtfzn1mK%!I{~n zXU{CbH@=Em{LI87%xU^r0Ynpu4?e)vR+w`*7fnB9Cu<&)>$G*of$r`BfvxVZ(_yPUL4Aj! z*;bu|ZB-mB-7w0~$1s9Gy=iV=G}=-U+7eopFts`nYYc-n7B)?UYKX-KY^!(()rQ7J zmSkb%<`RwG!e{iO(#D59ny5tu&9jc^)T6>VqEK(ArIwCuHpKkslFsurv#G`Wu>@b7 zep7_kV|B}9WJAT&hvctg{e#*e`N2kF_LM-gC%`K?Qy8=xlL#7(7aqpJT90_8hpBGi z;Yyd%3Dy{<#6QA-`yj$vV;GI+oB}WDbD#f5IUTJs(NEQZ#V$mk0yzOOLSM?n{3U3I0rlRr-kR`dyj73TJ-XjD;XR0usv zYpUq0(1ysVUh~8X<`Wo8gs1Svz!Di@HTg7)=i05y`~x}(2r*zx3OUH@gZGueIT5~0 zk>b8W7{H$`Ki{1-xoUUj;*~O}u(*)LjU{$Q>XY7-x52{=^VTR{?y+h<6+bqbq{-@>4vJ0C zXq7IOb|mJyQz{lOra9?8#Ng|MjgjydM9w1UMH~;$X+qWb zVr`&E@IxG2BfO9Rt_gtuu&dFq(f~<-LlR_KI>FdgKg8Hql1nq=+!AR=nTXn6Cf(_e z-`=cIZ?rG`CzPT7g6k%ybN)&ozC8uEN3by%;`1`+@$C_dhguS+vSbBtZ$Zi%>M&xd z;aa&@UJDn|E*HF(MA!18UV?}h9oPle>VQq^A&hGZ;VCebOESdJWhjOQGYFJD%%t2H z*ge1yg5kBE?ivm=@&h)vFAN8gQF-B5A-~}*#JU4sILUU%UFKL=nLOEyE^jL03*)U6 zUNn@-5%8($IG*C7Nb`sum5Y&04(D(?;L1!;Yzxhdfp;f4xIMF}g43Wj2=OPl7yhh{ zU;u7s$HVfWV{uD%of_==QC)uN;l9p0J3Y~I#deum;!~=7;fAhr+ysy|Hf2jTvG^H@Sj|Zq11TlaD zE%_p%kxPxH2QDzkL>89|qJUef@r_{vx5x<{T#_0%I!+zd4$XYn#DBpFPO$_8ibVJ! zUJoduA0qZz+<+ZpN6AtwJET*`mPwsGFTSv>^;p+2^_|w%ktpe-ktliIh(-F#<)4Ou|>;vZs_9Yc#@h~=E29Q(ALJ~%96&5bD!BCZtipSOWDMt#F{w_wQ0sc1<_ zg5ZxR5)@tv>^@+EIio3s`!FU$r4#2GN=~7POL8Ivk&;ZiDOB>w!h;6U5%}C?IxVb6 zlY<|;)}t_%i=|L*h=mG>KZheD;#3I_I29;pf4C{~0(2kp6cnxT-27-+W7oFI0#7Oh zo08o;-qZ^hD(bTuvbL00J3DrEZ{eY)5N?Vh@<=?7hmprj_H{OtK{>Rzho;lG72&he zMgG8Yru>1X!NX^P`jEl%XD5}FYsj8KnX;!Le;GWIRtDt{=t(PjXbbX3LU2a5=@nT;QI3vMK8=ZnznPtEp?ZcfZ0E~8 z*cxHqRbxO|LlR^MTZ)C$&V+{IcA z|ID^a53HuksEnm!3t7yM*Wb7CWpm)`M-2n#q=- zq}<3$z1qbNORqo{uZ%vSyoCKfvSY^cI2Xs&t^EAM^c(q2c39bo-)(0F>QZ(<&1WX# zgDeBra;Q0@pDG=!LvCU%(!1;=ww=n~!Fq@6k$zB(hP>`&jp}1;J>*#<{VzMM+{L=3 z=Rn?g8S=fvwkYR8vo%3(e_#jsv0vl)f6bmiCvq3=JwW+TA3;9mvSyOC^a!qf3HM23 z1NiNr!ONNrr`STQ#w}DzSvSc%)W>KYy-4LiWuX6{4ASvw#>at=)9@He^_KyagZ`s@ zkL^Qxke36Mh4$g)kb~hu-17QpBJJb#7Jleok)Pr8MowLEMTn8CqNl3Db4?_?d ziGr*x!#QXJ{*QV)J81k8Wd1TcAbpo@<86eo1M<(=Hl>2?lE2N)sDEVp=(ia8t;A=L zuB%>R9fqB3hk~eFY7y$&0_H}YaT||O-+^^~@wXba?lw71enq)kc~tqS@~K*;eoZ~3 zo>YIUeqdN=SZz3KEHmC={3Pt|u$RKS!{3UqM|4Kq5pg8qrN}u^X;H0FuSBOsZ-_n~ z{Z>pv%+{EPVxBXpraaS1(-G51Y)9;$%+2Pr=8r99mM+T!mgg-WTJ6?WYq#|Q>(kaZ zrpeQGPy0BoCGK?mtoQ>7wH+-yEUh)|{q%d%-?y9X&u7RPk7nj%wq$O|+?`dObzj!6vS(yJ zkbT;b=IC?W?|3^$&S}fJE$4mb${Bex4$b&&Zg%dv+@I!E<~^SGasH9~zs+ozd88nz zps`>>!Q%y=7T#L;o1%q9r)SNc^=$E?;(J`tu5Optt-5b@pPU^x`w35@=k1a^OB>1} z%YIdUY)xmV{M&HE62YDtfukSdY_Tt~ zOr*rV%uI5Ewy$76TiaKeOLl4d29_fCYWqfJmv7hh&$6}hAHPs`<)|?1Cpk(NTY~UED zBCbg~?wo(;_jp&o6c0jmX#$VD?+V+&S$7ozSjVvEsEzl54{J^GX{Bi7MZ!01{69os*_+x4r6 zxpxEJR?8)seV5&dQQr>{H|>Am>v$jA#U5ebV~$%dD90_B5o|>S z+V5cQb33~OI<@!M`|LF4c|P_O`vv=@1YL|2DMd-(i%BNLA~#DG$tq2g;-q+3UY%ek z*$>$BY#81HXV^~muTlbghW&?>C?&DoQnHjH*`(=GYS`e~6|-j5&C<3FVq5Rhwr*|f z(YEEZbAoet zMq_?whS$7gfj6!VJ9jKe^&0cLI+<6s&+{tz4c>73ydkw+#?l88VMv5~w1g3fupBL6 zK*E@)B_ilIhqOc_5)retL=+N{30fi=iKsX&5raguRV1+Hq}| + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 2011 Pablo Impallari wwwimpallaricomimpallarigmailcomCopyright c 2011 Igino Marini wwwikerncommailiginomarinicomCopyright c 2011 Brenda Gallo gbrenda1987gmailcomwith Reserved Font Name Quattrocento Sans +Designer : Pablo Impallari +Foundry : Pablo Impallari Igino Marini Brenda Gallo +Foundry URL : wwwimpallaricom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fonts/quattrocentosans-regular-webfont.ttf b/fonts/quattrocentosans-regular-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e4146706d18f5a4c906c5aa74a0baed66ac411f3 GIT binary patch literal 54220 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

T-wc>wP-UK*{6~Kq6$$b#YR%;#`!^9`p^&e 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 literal 0 HcmV?d00001 diff --git a/fonts/quattrocentosans-regular-webfont.woff b/fonts/quattrocentosans-regular-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..09ed324db3616d83401beb1c5b80c7c58dc1e2c3 GIT binary patch literal 27408 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 literal 0 HcmV?d00001 diff --git a/images/background.png b/images/background.png new file mode 100644 index 0000000000000000000000000000000000000000..b63b420f5ab432ec3af24a61af2090bbb4fc7e5e GIT binary patch literal 4559 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 literal 0 HcmV?d00001 diff --git a/images/body-background.png b/images/body-background.png new file mode 100644 index 0000000000000000000000000000000000000000..d6a152f121d00dc584ef2092e490a5386612e3db GIT binary patch literal 1097 zcmb7Dy>HV%6gQu$Dmrwd4CUl9FcAALA5Cq=lq61Q1g@gg53rsENG)X%U1eVVC?&pg(mzfMb!(6}=%;9<4RrsBa7d)^ng};{4pyuUq-JV+V@yyb+ zZZ0jDlEqJ61es7~1P-PK2%Ux-$f3fwd1YotZjlG=5W1l7`$bi?GRPAjgOmVcCLBj# zJSm_gl#V-c}n(WOQhbAz!igNg&ww>@eT7etK49BER8XO-@^;CMY+H|SgwyD%{{ zJP`>nYN;EjX@3uOoL+Q5EBK(^KN1J}qKCx_4oK5C+2CqpQ7TW)``Dnw*9mEKZ?Rk_ zlmvC+f&5HLKtMB0+l@SJgr>4U0*b$|Y2U3Mf~c$mU^A zO{8*An#iTQSe2Mf2fMV3wGObTFE$c`RU^q z*cU6ZV#H{)heq9XG5thlPq^&RlRkEt-hC$Q%j zCr7T7*Dsx4U4M1--gA8?e!uytIXaBKD6dC%zJ9pA^(fW)*4W*9@;0Wg-FR3#K6d)$ vKoPDC-<|zO^ZkkchQLkXaF*{IXOEoZel(sv*!xxAj2?HXFs(k$&93|gmL^-% literal 0 HcmV?d00001 diff --git a/images/bullet.png b/images/bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..2b7dc9a06c3111819e8637d8e47d6b26d169f770 GIT binary patch literal 993 zcmaJ=zi-n(6n0x$m5Pe0EPy~dxkW;P*mp^s)NV}`$2E;qmnwRa3mBT?W@h8SvRjyfI&QeW%J05@!voh*`DMw3W>CaUcYZ6xm9539 zz16a1ho8LxW@3dAc$ius_SXDJiB-PCtFUuo7J1Ndp)Hj^5LGjmK#_zPNCHgRFo!@c zE1)ctv+{M2h6su%Ey8>XK}E_d5P`#wXVF4uRVnMnVJvo2`6i`-B8u&HTWDtl5;jC6 z%d!a5Vmh5-9;s;4r&gTuqlqqqjw3sC1L_hVB#c&_Y*3YFN)J--f*!3O9hQj|OpL8S zL;_4w>N=X{|6M(==N-{9KGJ(Eaa7$5uvo?s*$8dcxYdayRG<_?Y*7+c30dn_aj8it ziJBw;#fl^#U|P28CzcMvG?kJcQOmb+NmqHsA-JxiXwX1;*~m*pEsf9|(u}D>5o&ry z(x5!0<-1s&*c%@9X&37pVW&>SCd1$bEV7P6_YQW9ka(bzwBjD4rKrO^G_pBK%jm~z zIlww6YssCcMPy>cq_zLFx@%(dlN@`4%Pzgq$3C0)kPUXsLUSGb)& zjq{)G*PiqZZC|0)H4Ze?let+u4^1!;XFb3Xo+q+jE-~0OeSMumebBp>bZDr>#V%#(? literal 0 HcmV?d00001 diff --git a/images/hr.gif b/images/hr.gif new file mode 100644 index 0000000000000000000000000000000000000000..a64b56c03bb421a39803668e18ebdb1756f58604 GIT binary patch literal 1349 zcmZ?wbhEHb3}xbG*v!rF`}gnvXBhtd`}hC6(4S8y|9m?0@#DvT*G&IiHTv`C&(o(* z|NsBbfCLo(b4U0FD7Yk+Bm!w0`-+0ZVOC1Fx149FSLnD0yb6rCN zD^qhT149KUPy*Ukl#*r@w&$a zmzVPg@)As;uP=V3xw&xF#U(+h2=`(&xHzP;AXPsowK%`DC^;3VTp46l zft7PnYGO%#QAmD%4lEP{GV)9Ei!<^I6r6)i^$Zn!6O%LZKq6orzP?tTdBr7(dC94s zF1AWQGxRbuQ>>f}+zibv-P}x_os0|(T@9VwEF7H;oLr1eoeV53oXlZ*UGkGlb5rw5 zV0u$vdL0c6aOwpmhTH<6%`T}$nPsUdZbkXI3SduLW#V>=3r_Q(dQ)(@#nR0cr(S)a zWAs5$ixkx`Az=CeG2sap$bl#Q)I4B%F9IfP#{d8R{`vju=a28-zJB@q>Enm@@7}(7 z{p#h5=g*!#dHm?%gZuaH-no72=8fyuu3ou(>Eea+=gyuved^?i(;JWy=vu(<;#{XS-fcBg8B32&Y3-H=8WmnrcRkWY2t+b zzTTehuFj73w$_&BrpAW)y4srRs>+J;veJ^`qQZjwyxg4Ztjvt`wA7U3q{M{yxY(HJ zsK|)$u+Wg;puhlsKVKhjFHaA5H&+*DCr1Z+J6juTD@zM=GgA{|BVeY|)78<|($r8_ zQ&mw`QdE$ala-N{l9Uh^6BQ8_5)|O)7GCi=I~9_mn91U4Of}`JIdkXDUr*J?K^PD``p!Q*KgdsrFvoi!J^v_A3c8Z dwB)YG<-})i-oAVP!TbXItKyH}zY8*00{|)z6eR!v literal 0 HcmV?d00001 diff --git a/images/octocat-logo.png b/images/octocat-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..28a3ad1bc9df1909230419619060c05d36542c55 GIT binary patch literal 3085 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 WV+R-9@!R~r2jHw6u(vQ?(f + + + + + Twython by ryanmcgrath + + + + + + + + + + +

+

Twython

+

An actively maintained, pure Python wrapper for the Twitter API. Supports both the normal and streaming Twitter APIs.

+
+ + + +
+ +
+

Twython

+ +

Twython is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today!

+ +

Features

+ +
    +
  • Query data for: + +
      +
    • User information
    • +
    • Twitter lists
    • +
    • Timelines
    • +
    • Direct Messages
    • +
    • and anything found in the docs +
    • +
    +
  • +
  • Image Uploading! + +
      +
    • Update user status with an image
    • +
    • Change user avatar
    • +
    • Change user background image
    • +
    • Change user banner image
    • +
    +
  • +
  • Support for Twitter's Streaming API
  • +
  • Seamless Python 3 support!
  • +

Installation

+ +
(pip install | easy_install) twython
+
+ +

... or, you can clone the repo and install it the old fashioned way

+ +
git clone git://github.com/ryanmcgrath/twython.git
+cd twython
+sudo python setup.py install
+
+ +

Usage

+ +
Authorization URL
+ +
from twython import Twython
+
+t = Twython(app_key, app_secret)
+
+auth_props = t.get_authentication_tokens(callback_url='http://google.com')
+
+oauth_token = auth_props['oauth_token']
+oauth_token_secret = auth_props['oauth_token_secret']
+
+print 'Connect to Twitter via: %s' % auth_props['auth_url']
+
+ +

Be sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date.

+ +
Handling the callback
+ +
from twython import Twython
+
+# oauth_token_secret comes from the previous step
+# if needed, store that in a session variable or something.
+# oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens
+
+# In Django, to get the oauth_verifier and oauth_token from the callback
+# url querystring, you might do something like this:
+# oauth_token = request.GET.get('oauth_token')
+# oauth_verifier = request.GET.get('oauth_verifier')
+
+t = Twython(app_key, app_secret,
+            oauth_token, oauth_token_secret)
+
+auth_tokens = t.get_authorized_tokens(oauth_verifier)
+print auth_tokens
+
+ +

Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py

+ +
Getting a user home timeline
+ +
from twython import Twython
+
+# oauth_token and oauth_token_secret are the final tokens produced
+# from the 'Handling the callback' step
+
+t = Twython(app_key, app_secret,
+            oauth_token, oauth_token_secret)
+
+# Returns an dict of the user home timeline
+print t.get_home_timeline()
+
+ +
Catching exceptions
+ +
+

Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError

+
+ +
from twython import Twython, TwythonAuthError
+
+t = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET,
+            BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET)
+
+try:
+    t.verify_credentials()
+except TwythonAuthError as e:
+    print e
+
+ +

Dynamic function arguments

+ +
+

Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library.

+ +

https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes "status" amongst other arguments

+
+ +
from twython import Twython, TwythonAuthError
+
+t = Twython(app_key, app_secret,
+            oauth_token, oauth_token_secret)
+
+try:
+    t.update_status(status='Hey guys!')
+except TwythonError as e:
+    print e
+
+ +
+

https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments

+
+ +
from twython import Twython, TwythonAuthError
+
+t = Twython(app_key, app_secret,
+            oauth_token, oauth_token_secret)
+
+try:
+    t.search(q='Hey guys!')
+    t.search(q='Hey guys!', result_type='popular')
+except TwythonError as e:
+    print e
+
+ +
Streaming API
+ +
from twython import TwythonStreamer
+
+class MyStreamer(TwythonStreamer):
+    def on_success(self, data):
+        print data
+
+    def on_error(self, status_code, data):
+        print status_code, data
+
+# Requires Authentication as of Twitter API v1.1
+stream = MyStreamer(APP_KEY, APP_SECRET,
+                    OAUTH_TOKEN, OAUTH_TOKEN_SECRET)
+
+stream.statuses.filter(track='twitter')
+
+ +

Notes

+ +
    +
  • Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the Twitter v1.1 API Documentation to help migrate your API calls!
  • +
  • As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time.
  • +

Questions, Comments, etc?

+ +

My hope is that Twython is so simple that you'd never have to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net.

+ +

Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well.

+ +

Follow us on Twitter:

+ +

Want to help?

+ +

Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!

+
+ +
+ + + + \ No newline at end of file diff --git a/javascripts/main.js b/javascripts/main.js new file mode 100644 index 0000000..c57e54c --- /dev/null +++ b/javascripts/main.js @@ -0,0 +1,53 @@ +var sectionHeight = function() { + var total = $(window).height(), + $section = $('section').css('height','auto'); + + if ($section.outerHeight(true) < total) { + var margin = $section.outerHeight(true) - $section.height(); + $section.height(total - margin - 20); + } else { + $section.css('height','auto'); + } +} + +$(window).resize(sectionHeight); + +$(document).ready(function(){ + $("section h1, section h2").each(function(){ + $("nav ul").append("
  • " + $(this).text() + "
  • "); + $(this).attr("id",$(this).text().toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g,'')); + $("nav ul li:first-child a").parent().addClass("active"); + }); + + $("nav ul li").on("click", "a", function(event) { + var position = $($(this).attr("href")).offset().top - 190; + $("html, body").animate({scrollTop: position}, 400); + $("nav ul li a").parent().removeClass("active"); + $(this).parent().addClass("active"); + event.preventDefault(); + }); + + sectionHeight(); + + $('img').load(sectionHeight); +}); + +fixScale = function(doc) { + + var addEvent = 'addEventListener', + type = 'gesturestart', + qsa = 'querySelectorAll', + scales = [1, 1], + meta = qsa in doc ? doc[qsa]('meta[name=viewport]') : []; + + function fix() { + meta.content = 'width=device-width,minimum-scale=' + scales[0] + ',maximum-scale=' + scales[1]; + doc.removeEventListener(type, fix, true); + } + + if ((meta = meta[meta.length - 1]) && addEvent in doc) { + fix(); + scales = [.25, 1.6]; + doc[addEvent](type, fix, true); + } +}; \ No newline at end of file diff --git a/params.json b/params.json new file mode 100644 index 0000000..05e3dac --- /dev/null +++ b/params.json @@ -0,0 +1 @@ +{"name":"Twython","tagline":"An actively maintained, pure Python wrapper for the Twitter API. Supports both the normal and streaming Twitter APIs.","body":"Twython\r\n=======\r\n\r\n```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today!\r\n\r\nFeatures\r\n--------\r\n\r\n* Query data for:\r\n - User information\r\n - Twitter lists\r\n - Timelines\r\n - Direct Messages\r\n - and anything found in [the docs](https://dev.twitter.com/docs/api/1.1)\r\n* Image Uploading!\r\n - **Update user status with an image**\r\n - Change user avatar\r\n - Change user background image\r\n - Change user banner image\r\n* Support for Twitter's Streaming API\r\n* Seamless Python 3 support!\r\n\r\nInstallation\r\n------------\r\n\r\n (pip install | easy_install) twython\r\n\r\n... or, you can clone the repo and install it the old fashioned way\r\n\r\n git clone git://github.com/ryanmcgrath/twython.git\r\n cd twython\r\n sudo python setup.py install\r\n\r\nUsage\r\n-----\r\n\r\n##### Authorization URL\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\nt = Twython(app_key, app_secret)\r\n\r\nauth_props = t.get_authentication_tokens(callback_url='http://google.com')\r\n\r\noauth_token = auth_props['oauth_token']\r\noauth_token_secret = auth_props['oauth_token_secret']\r\n\r\nprint 'Connect to Twitter via: %s' % auth_props['auth_url']\r\n```\r\n\r\nBe sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date.\r\n\r\n##### Handling the callback\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token_secret comes from the previous step\r\n# if needed, store that in a session variable or something.\r\n# oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens\r\n\r\n# In Django, to get the oauth_verifier and oauth_token from the callback\r\n# url querystring, you might do something like this:\r\n# oauth_token = request.GET.get('oauth_token')\r\n# oauth_verifier = request.GET.get('oauth_verifier')\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\nauth_tokens = t.get_authorized_tokens(oauth_verifier)\r\nprint auth_tokens\r\n```\r\n\r\n*Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py*\r\n\r\n##### Getting a user home timeline\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token and oauth_token_secret are the final tokens produced\r\n# from the 'Handling the callback' step\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\n# Returns an dict of the user home timeline\r\nprint t.get_home_timeline()\r\n```\r\n\r\n##### Catching exceptions\r\n> Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET,\r\n BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET)\r\n\r\ntry:\r\n t.verify_credentials()\r\nexcept TwythonAuthError as e:\r\n print e\r\n```\r\n\r\n#### Dynamic function arguments\r\n> Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library.\r\n\r\n> https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes \"status\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.update_status(status='Hey guys!')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n> https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes \"q\" and \"result_type\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.search(q='Hey guys!')\r\n t.search(q='Hey guys!', result_type='popular')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n##### Streaming API\r\n\r\n```python\r\nfrom twython import TwythonStreamer\r\n\r\nclass MyStreamer(TwythonStreamer):\r\n def on_success(self, data):\r\n print data\r\n\r\n def on_error(self, status_code, data):\r\n print status_code, data\r\n\r\n# Requires Authentication as of Twitter API v1.1\r\nstream = MyStreamer(APP_KEY, APP_SECRET,\r\n OAUTH_TOKEN, OAUTH_TOKEN_SECRET)\r\n\r\nstream.statuses.filter(track='twitter')\r\n```\r\n\r\nNotes\r\n-----\r\n- Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls!\r\n- As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time.\r\n\r\nQuestions, Comments, etc?\r\n-------------------------\r\nMy hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net.\r\n\r\nOr if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well.\r\n\r\nFollow us on Twitter:\r\n* **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**\r\n* **[@mikehelmick](http://twitter.com/mikehelmick)**\r\n\r\nWant to help?\r\n-------------\r\nTwython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} \ No newline at end of file diff --git a/stylesheets/normalize.css b/stylesheets/normalize.css new file mode 100644 index 0000000..bc2ba93 --- /dev/null +++ b/stylesheets/normalize.css @@ -0,0 +1,459 @@ +/* normalize.css 2012-02-07T12:37 UTC - http://github.com/necolas/normalize.css */ +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + */ +audio:not([controls]) { + display: none; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ +[hidden] { + display: none; +} + +/* ============================================================================= + Base + ========================================================================== */ +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ +html { + font-size: 100%; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -ms-text-size-adjust: 100%; + /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ +body { + margin: 0; +} + +/* ============================================================================= + Links + ========================================================================== */ +/* + * Addresses outline displayed oddly in Chrome + */ +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ +a:hover, +a:active { + outline: 0; +} + +/* ============================================================================= + Typography + ========================================================================== */ +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ +/* 1 */ +q { + quotes: none; +} + +/* 2 */ +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ============================================================================= + Lists + ========================================================================== */ +/* + * Addresses margins set differently in IE6/7 + */ +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ============================================================================= + Embedded content + ========================================================================== */ +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; + /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ +svg:not(:root) { + overflow: hidden; +} + +/* ============================================================================= + Figures + ========================================================================== */ +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ +figure { + margin: 0; +} + +/* ============================================================================= + Forms + ========================================================================== */ +/* + * Corrects margin displayed oddly in IE6/7 + */ +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; + /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ +button, +input, +select, +textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; + /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ +button, +input { + line-height: normal; + /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + /* 1 */ + -webkit-appearance: button; + /* 2 */ + *overflow: visible; + /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; + /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; + /* 2 */ +} + +/* ============================================================================= + Tables + ========================================================================== */ +/* + * Remove most spacing between table cells + */ +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/stylesheets/pygment_trac.css b/stylesheets/pygment_trac.css new file mode 100644 index 0000000..62fd970 --- /dev/null +++ b/stylesheets/pygment_trac.css @@ -0,0 +1,70 @@ +.highlight .hll { background-color: #404040 } +.highlight { color: #d0d0d0 } +.highlight .c { color: #999999; font-style: italic } /* Comment */ +.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.highlight .g { color: #d0d0d0 } /* Generic */ +.highlight .k { color: #6ab825; font-weight: normal } /* Keyword */ +.highlight .l { color: #d0d0d0 } /* Literal */ +.highlight .n { color: #d0d0d0 } /* Name */ +.highlight .o { color: #d0d0d0 } /* Operator */ +.highlight .x { color: #d0d0d0 } /* Other */ +.highlight .p { color: #d0d0d0 } /* Punctuation */ +.highlight .cm { color: #999999; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #cd2828; font-weight: normal } /* Comment.Preproc */ +.highlight .c1 { color: #999999; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #e50808; font-weight: normal; background-color: #520000 } /* Comment.Special */ +.highlight .gd { color: #d22323 } /* Generic.Deleted */ +.highlight .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #d22323 } /* Generic.Error */ +.highlight .gh { color: #ffffff; font-weight: normal } /* Generic.Heading */ +.highlight .gi { color: #589819 } /* Generic.Inserted */ +.highlight .go { color: #cccccc } /* Generic.Output */ +.highlight .gp { color: #aaaaaa } /* Generic.Prompt */ +.highlight .gs { color: #d0d0d0; font-weight: normal } /* Generic.Strong */ +.highlight .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */ +.highlight .gt { color: #d22323 } /* Generic.Traceback */ +.highlight .kc { color: #6ab825; font-weight: normal } /* Keyword.Constant */ +.highlight .kd { color: #6ab825; font-weight: normal } /* Keyword.Declaration */ +.highlight .kn { color: #6ab825; font-weight: normal } /* Keyword.Namespace */ +.highlight .kp { color: #6ab825 } /* Keyword.Pseudo */ +.highlight .kr { color: #6ab825; font-weight: normal } /* Keyword.Reserved */ +.highlight .kt { color: #6ab825; font-weight: normal } /* Keyword.Type */ +.highlight .ld { color: #d0d0d0 } /* Literal.Date */ +.highlight .m { color: #3677a9 } /* Literal.Number */ +.highlight .s { color: #ff8 } /* Literal.String */ +.highlight .na { color: #bbbbbb } /* Name.Attribute */ +.highlight .nb { color: #24909d } /* Name.Builtin */ +.highlight .nc { color: #447fcf; text-decoration: underline } /* Name.Class */ +.highlight .no { color: #40ffff } /* Name.Constant */ +.highlight .nd { color: #ffa500 } /* Name.Decorator */ +.highlight .ni { color: #d0d0d0 } /* Name.Entity */ +.highlight .ne { color: #bbbbbb } /* Name.Exception */ +.highlight .nf { color: #447fcf } /* Name.Function */ +.highlight .nl { color: #d0d0d0 } /* Name.Label */ +.highlight .nn { color: #447fcf; text-decoration: underline } /* Name.Namespace */ +.highlight .nx { color: #d0d0d0 } /* Name.Other */ +.highlight .py { color: #d0d0d0 } /* Name.Property */ +.highlight .nt { color: #6ab825;} /* Name.Tag */ +.highlight .nv { color: #40ffff } /* Name.Variable */ +.highlight .ow { color: #6ab825; font-weight: normal } /* Operator.Word */ +.highlight .w { color: #666666 } /* Text.Whitespace */ +.highlight .mf { color: #3677a9 } /* Literal.Number.Float */ +.highlight .mh { color: #3677a9 } /* Literal.Number.Hex */ +.highlight .mi { color: #3677a9 } /* Literal.Number.Integer */ +.highlight .mo { color: #3677a9 } /* Literal.Number.Oct */ +.highlight .sb { color: #ff8 } /* Literal.String.Backtick */ +.highlight .sc { color: #ff8 } /* Literal.String.Char */ +.highlight .sd { color: #ff8 } /* Literal.String.Doc */ +.highlight .s2 { color: #ff8 } /* Literal.String.Double */ +.highlight .se { color: #ff8 } /* Literal.String.Escape */ +.highlight .sh { color: #ff8 } /* Literal.String.Heredoc */ +.highlight .si { color: #ff8 } /* Literal.String.Interpol */ +.highlight .sx { color: #ffa500 } /* Literal.String.Other */ +.highlight .sr { color: #ff8 } /* Literal.String.Regex */ +.highlight .s1 { color: #ff8 } /* Literal.String.Single */ +.highlight .ss { color: #ff8 } /* Literal.String.Symbol */ +.highlight .bp { color: #24909d } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #40ffff } /* Name.Variable.Class */ +.highlight .vg { color: #40ffff } /* Name.Variable.Global */ +.highlight .vi { color: #40ffff } /* Name.Variable.Instance */ +.highlight .il { color: #3677a9 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/stylesheets/styles.css b/stylesheets/styles.css new file mode 100644 index 0000000..980ee2b --- /dev/null +++ b/stylesheets/styles.css @@ -0,0 +1,1010 @@ +/* +Leap Day for GitHub Pages +by Matt Graham +*/ +@font-face { + font-family: 'Quattrocento Sans'; + src: url("../fonts/quattrocentosans-bold-webfont.eot"); + src: url("../fonts/quattrocentosans-bold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-bold-webfont.woff") format("woff"), url("../fonts/quattrocentosans-bold-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-bold-webfont.svg#QuattrocentoSansBold") format("svg"); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Quattrocento Sans'; + src: url("../fonts/quattrocentosans-bolditalic-webfont.eot"); + src: url("../fonts/quattrocentosans-bolditalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-bolditalic-webfont.woff") format("woff"), url("../fonts/quattrocentosans-bolditalic-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-bolditalic-webfont.svg#QuattrocentoSansBoldItalic") format("svg"); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Quattrocento Sans'; + src: url("../fonts/quattrocentosans-italic-webfont.eot"); + src: url("../fonts/quattrocentosans-italic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-italic-webfont.woff") format("woff"), url("../fonts/quattrocentosans-italic-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-italic-webfont.svg#QuattrocentoSansItalic") format("svg"); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Quattrocento Sans'; + src: url("../fonts/quattrocentosans-regular-webfont.eot"); + src: url("../fonts/quattrocentosans-regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-regular-webfont.woff") format("woff"), url("../fonts/quattrocentosans-regular-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-regular-webfont.svg#QuattrocentoSansRegular") format("svg"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Copse'; + src: url("../fonts/copse-regular-webfont.eot"); + src: url("../fonts/copse-regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/copse-regular-webfont.woff") format("woff"), url("../fonts/copse-regular-webfont.ttf") format("truetype"), url("../fonts/copse-regular-webfont.svg#CopseRegular") format("svg"); + font-weight: normal; + font-style: normal; +} + +/* normalize.css 2012-02-07T12:37 UTC - http://github.com/necolas/normalize.css */ +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + */ +audio:not([controls]) { + display: none; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ +[hidden] { + display: none; +} + +/* ============================================================================= + Base + ========================================================================== */ +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ +html { + font-size: 100%; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -ms-text-size-adjust: 100%; + /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ +body { + margin: 0; +} + +/* ============================================================================= + Links + ========================================================================== */ +/* + * Addresses outline displayed oddly in Chrome + */ +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ +a:hover, +a:active { + outline: 0; +} + +/* ============================================================================= + Typography + ========================================================================== */ +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ +/* 1 */ +q { + quotes: none; +} + +/* 2 */ +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ============================================================================= + Lists + ========================================================================== */ +/* + * Addresses margins set differently in IE6/7 + */ +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ============================================================================= + Embedded content + ========================================================================== */ +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ +img { + border: 0; + /* 1 */ + -ms-interpolation-mode: bicubic; + /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ +svg:not(:root) { + overflow: hidden; +} + +/* ============================================================================= + Figures + ========================================================================== */ +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ +figure { + margin: 0; +} + +/* ============================================================================= + Forms + ========================================================================== */ +/* + * Corrects margin displayed oddly in IE6/7 + */ +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ +legend { + border: 0; + /* 1 */ + padding: 0; + white-space: normal; + /* 2 */ + *margin-left: -7px; + /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ +button, +input, +select, +textarea { + font-size: 100%; + /* 1 */ + margin: 0; + /* 2 */ + vertical-align: baseline; + /* 3 */ + *vertical-align: middle; + /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ +button, +input { + line-height: normal; + /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + /* 1 */ + -webkit-appearance: button; + /* 2 */ + *overflow: visible; + /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ + *height: 13px; + /* 3 */ + *width: 13px; + /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ +input[type="search"] { + -webkit-appearance: textfield; + /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; + /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ +textarea { + overflow: auto; + /* 1 */ + vertical-align: top; + /* 2 */ +} + +/* ============================================================================= + Tables + ========================================================================== */ +/* + * Remove most spacing between table cells + */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + font: 14px/22px "Quattrocento Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #666; + font-weight: 300; + margin: 0px; + padding: 0px 0 20px 0px; + background: url(../images/body-background.png) #eae6d1; +} + +h1, h2, h3, h4, h5, h6 { + color: #333; + margin: 0 0 10px; +} + +p, ul, ol, table, pre, dl { + margin: 0 0 20px; +} + +h1, h2, h3 { + line-height: 1.1; +} + +h1 { + font-size: 28px; +} + +h2 { + font-size: 24px; + color: #393939; +} + +h3, h4, h5, h6 { + color: #666666; +} + +h3 { + font-size: 18px; + line-height: 24px; +} + +a { + color: #3399cc; + font-weight: 400; + text-decoration: none; +} + +a small { + font-size: 11px; + color: #666; + margin-top: -0.6em; + display: block; +} + +ul { + list-style-image: url("../images/bullet.png"); +} + +strong { + font-weight: bold; + color: #333; +} + +.wrapper { + width: 650px; + margin: 0 auto; + position: relative; +} + +section img { + max-width: 100%; +} + +blockquote { + border-left: 1px solid #ffcc00; + margin: 0; + padding: 0 0 0 20px; + font-style: italic; +} + +code { + font-family: "Lucida Sans", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; + font-size: 13px; + color: #efefef; + text-shadow: 0px 1px 0px #000; + margin: 0 4px; + padding: 2px 6px; + background: #333; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +pre { + padding: 8px 15px; + background: #333333; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -o-border-radius: 3px; + -ms-border-radius: 3px; + -khtml-border-radius: 3px; + border-radius: 3px; + border: 1px solid #c7c7c7; + overflow: auto; + overflow-y: hidden; +} +pre code { + margin: 0px; + padding: 0px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th { + text-align: left; + padding: 5px 10px; + border-bottom: 1px solid #e5e5e5; + color: #444; +} + +td { + text-align: left; + padding: 5px 10px; + border-bottom: 1px solid #e5e5e5; + border-right: 1px solid #ffcc00; +} +td:first-child { + border-left: 1px solid #ffcc00; +} + +hr { + border: 0; + outline: none; + height: 11px; + background: transparent url("../images/hr.gif") center center repeat-x; + margin: 0 0 20px; +} + +dt { + color: #444; + font-weight: 700; +} + +header { + padding: 25px 20px 40px 20px; + margin: 0; + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + text-align: center; + background: url(../images/background.png) #4276b6; + -moz-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); + -webkit-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); + -o-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); + box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); + z-index: 99; + -webkit-font-smoothing: antialiased; + min-height: 76px; +} +header h1 { + font: 40px/48px "Copse", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #f3f3f3; + text-shadow: 0px 2px 0px #235796; + margin: 0px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; +} +header p { + color: #d8d8d8; + text-shadow: rgba(0, 0, 0, 0.2) 0 1px 0; + font-size: 18px; + margin: 0px; +} + +#banner { + z-index: 100; + left: 0; + right: 50%; + height: 50px; + margin-right: -382px; + position: fixed; + top: 115px; + background: #ffcc00; + border: 1px solid #f0b500; + -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + -o-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); + -moz-border-radius: 0px 2px 2px 0px; + -webkit-border-radius: 0px 2px 2px 0px; + -o-border-radius: 0px 2px 2px 0px; + -ms-border-radius: 0px 2px 2px 0px; + -khtml-border-radius: 0px 2px 2px 0px; + border-radius: 0px 2px 2px 0px; + padding-right: 10px; +} +#banner .button { + border: 1px solid #dba500; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffe788), color-stop(100%, #ffce38)); + background: -webkit-linear-gradient(#ffe788, #ffce38); + background: -moz-linear-gradient(#ffe788, #ffce38); + background: -o-linear-gradient(#ffe788, #ffce38); + background: -ms-linear-gradient(#ffe788, #ffce38); + background: linear-gradient(#ffe788, #ffce38); + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; + -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); + -o-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); + background-color: #FFE788; + margin-left: 5px; + padding: 10px 12px; + margin-top: 6px; + line-height: 14px; + font-size: 14px; + color: #333; + font-weight: bold; + display: inline-block; + text-align: center; +} +#banner .button:hover { + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffe788), color-stop(100%, #ffe788)); + background: -webkit-linear-gradient(#ffe788, #ffe788); + background: -moz-linear-gradient(#ffe788, #ffe788); + background: -o-linear-gradient(#ffe788, #ffe788); + background: -ms-linear-gradient(#ffe788, #ffe788); + background: linear-gradient(#ffe788, #ffe788); + background-color: #ffeca0; +} +#banner .fork { + position: fixed; + left: 50%; + margin-left: -325px; + padding: 10px 12px; + margin-top: 6px; + line-height: 14px; + font-size: 14px; + background-color: #FFE788; +} +#banner .downloads { + float: right; + margin: 0 45px 0 0; +} +#banner .downloads span { + float: left; + line-height: 52px; + font-size: 90%; + color: #9d7f0d; + text-transform: uppercase; + text-shadow: rgba(255, 255, 255, 0.2) 0 1px 0; +} +#banner ul { + list-style: none; + height: 40px; + padding: 0; + float: left; + margin-left: 10px; +} +#banner ul li { + display: inline; +} +#banner ul li a.button { + background-color: #FFE788; +} +#banner #logo { + position: absolute; + height: 36px; + width: 36px; + right: 7px; + top: 7px; + display: block; + background: url(../images/octocat-logo.png); +} + +section { + width: 590px; + padding: 30px 30px 50px 30px; + margin: 20px 0; + margin-top: 190px; + position: relative; + background: #fbfbfb; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + -o-border-radius: 3px; + -ms-border-radius: 3px; + -khtml-border-radius: 3px; + border-radius: 3px; + border: 1px solid #cbcbcb; + -moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); + -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); + -o-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); +} + +small { + font-size: 12px; +} + +nav { + width: 230px; + position: fixed; + top: 220px; + left: 50%; + margin-left: -580px; + text-align: right; +} +nav ul { + list-style: none; + list-style-image: none; + font-size: 14px; + line-height: 24px; +} +nav ul li { + padding: 5px 0px; + line-height: 16px; +} +nav ul li.tag-h1 { + font-size: 1.2em; +} +nav ul li.tag-h1 a { + font-weight: bold; + color: #333; +} +nav ul li.tag-h2 + .tag-h1 { + margin-top: 10px; +} +nav ul a { + color: #666; +} +nav ul a:hover { + color: #999; +} + +footer { + width: 180px; + position: fixed; + left: 50%; + margin-left: -530px; + bottom: 20px; + text-align: right; + line-height: 16px; +} + +@media print, screen and (max-width: 1060px) { + div.wrapper { + width: auto; + margin: 0; + } + + nav { + display: none; + } + + header, section, footer { + float: none; + } + header h1, section h1, footer h1 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + } + + #banner { + width: 100%; + } + #banner .downloads { + margin-right: 60px; + } + #banner #logo { + margin-right: 15px; + } + + section { + border: 1px solid #e5e5e5; + border-width: 1px 0; + padding: 20px auto; + margin: 190px auto 20px; + max-width: 600px; + } + + footer { + text-align: center; + margin: 20px auto; + position: relative; + left: auto; + bottom: auto; + width: auto; + } +} +@media print, screen and (max-width: 720px) { + body { + word-wrap: break-word; + } + + header { + padding: 20px 20px; + margin: 0; + } + header h1 { + font-size: 32px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + } + header p { + display: none; + } + + #banner { + top: 80px; + } + #banner .fork { + float: left; + display: inline-block; + margin-left: 0px; + position: fixed; + left: 20px; + } + + section { + margin-top: 130px; + margin-bottom: 0px; + width: auto; + } + + header ul, header p.view { + position: static; + } +} +@media print, screen and (max-width: 480px) { + header { + position: relative; + padding: 5px 0px; + min-height: 0px; + } + header h1 { + font-size: 24px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + } + + section { + margin-top: 5px; + } + + #banner { + display: none; + } + + header ul { + display: none; + } +} +@media print { + body { + padding: 0.4in; + font-size: 12pt; + color: #444; + } +} +@media print, screen and (max-height: 680px) { + footer { + text-align: center; + margin: 20px auto; + position: relative; + left: auto; + bottom: auto; + width: auto; + } +} +@media print, screen and (max-height: 480px) { + nav { + display: none; + } + + footer { + text-align: center; + margin: 20px auto; + position: relative; + left: auto; + bottom: auto; + width: auto; + } +} From 594aa3b349eb4f07bfce712d8675444146cc0d38 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 21:15:59 -0700 Subject: [PATCH 386/687] Create gh-pages branch via GitHub --- index.html | 12 +++++++++++- params.json | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index afb1e7f..84cb6b0 100644 --- a/index.html +++ b/index.html @@ -230,6 +230,16 @@ sudo python setup.py install - + + + \ No newline at end of file diff --git a/params.json b/params.json index 05e3dac..f2f3ee5 100644 --- a/params.json +++ b/params.json @@ -1 +1 @@ -{"name":"Twython","tagline":"An actively maintained, pure Python wrapper for the Twitter API. Supports both the normal and streaming Twitter APIs.","body":"Twython\r\n=======\r\n\r\n```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today!\r\n\r\nFeatures\r\n--------\r\n\r\n* Query data for:\r\n - User information\r\n - Twitter lists\r\n - Timelines\r\n - Direct Messages\r\n - and anything found in [the docs](https://dev.twitter.com/docs/api/1.1)\r\n* Image Uploading!\r\n - **Update user status with an image**\r\n - Change user avatar\r\n - Change user background image\r\n - Change user banner image\r\n* Support for Twitter's Streaming API\r\n* Seamless Python 3 support!\r\n\r\nInstallation\r\n------------\r\n\r\n (pip install | easy_install) twython\r\n\r\n... or, you can clone the repo and install it the old fashioned way\r\n\r\n git clone git://github.com/ryanmcgrath/twython.git\r\n cd twython\r\n sudo python setup.py install\r\n\r\nUsage\r\n-----\r\n\r\n##### Authorization URL\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\nt = Twython(app_key, app_secret)\r\n\r\nauth_props = t.get_authentication_tokens(callback_url='http://google.com')\r\n\r\noauth_token = auth_props['oauth_token']\r\noauth_token_secret = auth_props['oauth_token_secret']\r\n\r\nprint 'Connect to Twitter via: %s' % auth_props['auth_url']\r\n```\r\n\r\nBe sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date.\r\n\r\n##### Handling the callback\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token_secret comes from the previous step\r\n# if needed, store that in a session variable or something.\r\n# oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens\r\n\r\n# In Django, to get the oauth_verifier and oauth_token from the callback\r\n# url querystring, you might do something like this:\r\n# oauth_token = request.GET.get('oauth_token')\r\n# oauth_verifier = request.GET.get('oauth_verifier')\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\nauth_tokens = t.get_authorized_tokens(oauth_verifier)\r\nprint auth_tokens\r\n```\r\n\r\n*Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py*\r\n\r\n##### Getting a user home timeline\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token and oauth_token_secret are the final tokens produced\r\n# from the 'Handling the callback' step\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\n# Returns an dict of the user home timeline\r\nprint t.get_home_timeline()\r\n```\r\n\r\n##### Catching exceptions\r\n> Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET,\r\n BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET)\r\n\r\ntry:\r\n t.verify_credentials()\r\nexcept TwythonAuthError as e:\r\n print e\r\n```\r\n\r\n#### Dynamic function arguments\r\n> Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library.\r\n\r\n> https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes \"status\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.update_status(status='Hey guys!')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n> https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes \"q\" and \"result_type\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.search(q='Hey guys!')\r\n t.search(q='Hey guys!', result_type='popular')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n##### Streaming API\r\n\r\n```python\r\nfrom twython import TwythonStreamer\r\n\r\nclass MyStreamer(TwythonStreamer):\r\n def on_success(self, data):\r\n print data\r\n\r\n def on_error(self, status_code, data):\r\n print status_code, data\r\n\r\n# Requires Authentication as of Twitter API v1.1\r\nstream = MyStreamer(APP_KEY, APP_SECRET,\r\n OAUTH_TOKEN, OAUTH_TOKEN_SECRET)\r\n\r\nstream.statuses.filter(track='twitter')\r\n```\r\n\r\nNotes\r\n-----\r\n- Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls!\r\n- As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time.\r\n\r\nQuestions, Comments, etc?\r\n-------------------------\r\nMy hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net.\r\n\r\nOr if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well.\r\n\r\nFollow us on Twitter:\r\n* **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**\r\n* **[@mikehelmick](http://twitter.com/mikehelmick)**\r\n\r\nWant to help?\r\n-------------\r\nTwython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} \ No newline at end of file +{"name":"Twython","tagline":"An actively maintained, pure Python wrapper for the Twitter API. Supports both the normal and streaming Twitter APIs.","body":"Twython\r\n=======\r\n\r\n```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today!\r\n\r\nFeatures\r\n--------\r\n\r\n* Query data for:\r\n - User information\r\n - Twitter lists\r\n - Timelines\r\n - Direct Messages\r\n - and anything found in [the docs](https://dev.twitter.com/docs/api/1.1)\r\n* Image Uploading!\r\n - **Update user status with an image**\r\n - Change user avatar\r\n - Change user background image\r\n - Change user banner image\r\n* Support for Twitter's Streaming API\r\n* Seamless Python 3 support!\r\n\r\nInstallation\r\n------------\r\n\r\n (pip install | easy_install) twython\r\n\r\n... or, you can clone the repo and install it the old fashioned way\r\n\r\n git clone git://github.com/ryanmcgrath/twython.git\r\n cd twython\r\n sudo python setup.py install\r\n\r\nUsage\r\n-----\r\n\r\n##### Authorization URL\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\nt = Twython(app_key, app_secret)\r\n\r\nauth_props = t.get_authentication_tokens(callback_url='http://google.com')\r\n\r\noauth_token = auth_props['oauth_token']\r\noauth_token_secret = auth_props['oauth_token_secret']\r\n\r\nprint 'Connect to Twitter via: %s' % auth_props['auth_url']\r\n```\r\n\r\nBe sure you have a URL set up to handle the callback after the user has allowed your app to access their data, the callback can be used for storing their final OAuth Token and OAuth Token Secret in a database for use at a later date.\r\n\r\n##### Handling the callback\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token_secret comes from the previous step\r\n# if needed, store that in a session variable or something.\r\n# oauth_verifier and oauth_token from the previous call is now REQUIRED # to pass to get_authorized_tokens\r\n\r\n# In Django, to get the oauth_verifier and oauth_token from the callback\r\n# url querystring, you might do something like this:\r\n# oauth_token = request.GET.get('oauth_token')\r\n# oauth_verifier = request.GET.get('oauth_verifier')\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\nauth_tokens = t.get_authorized_tokens(oauth_verifier)\r\nprint auth_tokens\r\n```\r\n\r\n*Function definitions (i.e. get_home_timeline()) can be found by reading over twython/endpoints.py*\r\n\r\n##### Getting a user home timeline\r\n\r\n```python\r\nfrom twython import Twython\r\n\r\n# oauth_token and oauth_token_secret are the final tokens produced\r\n# from the 'Handling the callback' step\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\n# Returns an dict of the user home timeline\r\nprint t.get_home_timeline()\r\n```\r\n\r\n##### Catching exceptions\r\n> Twython offers three Exceptions currently: TwythonError, TwythonAuthError and TwythonRateLimitError\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(MY_WRONG_APP_KEY, MY_WRONG_APP_SECRET,\r\n BAD_OAUTH_TOKEN, BAD_OAUTH_TOKEN_SECRET)\r\n\r\ntry:\r\n t.verify_credentials()\r\nexcept TwythonAuthError as e:\r\n print e\r\n```\r\n\r\n#### Dynamic function arguments\r\n> Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library.\r\n\r\n> https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes \"status\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.update_status(status='Hey guys!')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n> https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes \"q\" and \"result_type\" amongst other arguments\r\n\r\n```python\r\nfrom twython import Twython, TwythonAuthError\r\n\r\nt = Twython(app_key, app_secret,\r\n oauth_token, oauth_token_secret)\r\n\r\ntry:\r\n t.search(q='Hey guys!')\r\n t.search(q='Hey guys!', result_type='popular')\r\nexcept TwythonError as e:\r\n print e\r\n```\r\n\r\n##### Streaming API\r\n\r\n```python\r\nfrom twython import TwythonStreamer\r\n\r\nclass MyStreamer(TwythonStreamer):\r\n def on_success(self, data):\r\n print data\r\n\r\n def on_error(self, status_code, data):\r\n print status_code, data\r\n\r\n# Requires Authentication as of Twitter API v1.1\r\nstream = MyStreamer(APP_KEY, APP_SECRET,\r\n OAUTH_TOKEN, OAUTH_TOKEN_SECRET)\r\n\r\nstream.statuses.filter(track='twitter')\r\n```\r\n\r\nNotes\r\n-----\r\n- Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls!\r\n- As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time.\r\n\r\nQuestions, Comments, etc?\r\n-------------------------\r\nMy hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net.\r\n\r\nOr if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well.\r\n\r\nFollow us on Twitter:\r\n* **[@ryanmcgrath](http://twitter.com/ryanmcgrath)**\r\n* **[@mikehelmick](http://twitter.com/mikehelmick)**\r\n\r\nWant to help?\r\n-------------\r\nTwython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!\r\n","google":"UA-40660943-1","note":"Don't delete this file! It's used internally to help with page regeneration."} \ No newline at end of file From 8bfb585afba40395fe144f1091bbcc9aa81d1cfa Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 5 May 2013 01:23:10 -0300 Subject: [PATCH 387/687] Add this. --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 84cb6b0..ee3a7dc 100644 --- a/index.html +++ b/index.html @@ -225,7 +225,7 @@ sudo python setup.py install

    Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!

    @@ -242,4 +242,4 @@ sudo python setup.py install - \ No newline at end of file + From 100e8380ac813d5eb88eca24433e2db562e0621b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sun, 5 May 2013 01:23:42 -0300 Subject: [PATCH 388/687] fasdfasdf --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index ee3a7dc..611ef2e 100644 --- a/index.html +++ b/index.html @@ -225,7 +225,7 @@ sudo python setup.py install

    Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated!

    From 5534ea2480f16e4a335b173e88b8ba85c73c7c35 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 10 May 2013 21:28:57 -0400 Subject: [PATCH 389/687] Fixes #193, fixed when Warning is raised, fixed error raising, version bump - Added `get_retweeters_ids` method - Fixed `TwythonDeprecationWarning` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) - Fixed error message bubbling when error message returned from Twitter was not an array (i.e. if you try to retweet something twice, the error is not found at index 0) --- HISTORY.rst | 6 ++++++ setup.py | 2 +- twython/__init__.py | 2 +- twython/endpoints.py | 4 ++++ twython/twython.py | 7 +++++-- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index a044dcf..9f0e559 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,12 @@ History ------- +2.10.0 (2013-05-xx) +++++++++++++++++++ +- Added ``get_retweeters_ids`` method +- Fixed ``TwythonDeprecationWarning`` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) +- Fixed error message bubbling when error message returned from Twitter was not an array (i.e. if you try to retweet something twice, the error is not found at index 0) + 2.9.1 (2013-05-04) ++++++++++++++++++ diff --git a/setup.py b/setup.py index dd44b6d..42de4b2 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '2.9.1' +__version__ = '2.10.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index 4d888fa..6390bcb 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '2.9.1' +__version__ = '2.10.0' from .twython import Twython from .streaming import TwythonStreamer diff --git a/twython/endpoints.py b/twython/endpoints.py index 365a0a2..2695af4 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -61,6 +61,10 @@ api_table = { 'url': '/statuses/oembed.json', 'method': 'GET', }, + 'get_retweeters_ids': { + 'url': '/statuses/retweeters/ids.json', + 'method': 'GET', + }, # Search diff --git a/twython/twython.py b/twython/twython.py index a3f055f..ae509b5 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -114,7 +114,7 @@ class Twython(object): self.api_url % self.api_version + fn['url'] ) - if deprecated_key: + if deprecated_key and (deprecated_key != api_call): # Until Twython 3.0.0 and the function is removed.. send deprecation warning warnings.warn( '`%s` is deprecated, please use `%s` instead.' % (deprecated_key, api_call), @@ -169,7 +169,10 @@ class Twython(object): # If there is no error message, use a default. errors = content.get('errors', [{'message': 'An error occurred processing your request.'}]) - error_message = errors[0]['message'] + if errors and isinstance(errors, list): + error_message = errors[0]['message'] + else: + error_message = errors self._last_call['api_error'] = error_message ExceptionType = TwythonError From 6841e7ef287abb1d4260e7ac3c9860a168edac59 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 21:34:22 -0400 Subject: [PATCH 390/687] Tests for all main endpoints --- .travis.yml | 20 +++ requirements.txt | 2 + test_twython.py | 410 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 432 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements.txt create mode 100644 test_twython.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3f75b4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.3 +env: + APP_KEY='kpowaBNkhhXwYUu3es27dQ' + APP_SECRET='iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU' + OAUTH_TOKEN='1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx' + OAUTH_TOKEN_SECRET='dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg' + SCREEN_NAME='TwythonTest' + PROTECTED_TWITTER_1='TwythonSecure1' + PROTECTED_TWITTER_2='TwythonSecure2' + TEST_TWEET_ID='332992304010899457' + TEST_LIST_ID='574' +script: nosetests -v test_twython:TwythonAPITestCase --cover-package="twython" --with-coverage +install: + - pip install -r requirements.txt +notifications: + email: false diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..09acfa6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests==1.2.0 +requests_oauthlib==0.3.1 diff --git a/test_twython.py b/test_twython.py new file mode 100644 index 0000000..84db80c --- /dev/null +++ b/test_twython.py @@ -0,0 +1,410 @@ +import unittest +import os + +from twython import Twython, TwythonError, TwythonAuthError + +app_key = os.environ.get('APP_KEY', 'kpowaBNkhhXwYUu3es27dQ') +app_secret = os.environ.get('APP_SECRET', 'iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU') +oauth_token = os.environ.get('OAUTH_TOKEN', '1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx') +oauth_token_secret = os.environ.get('OAUTH_TOKEN_SECRET', 'dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg') + +screen_name = os.environ.get('SCREEN_NAME', 'TwythonTest') + +# Protected Account you ARE following and they ARE following you +protected_twitter_1 = os.environ.get('PROTECTED_TWITTER_1', 'TwythonSecure1') + +# Protected Account you ARE NOT following +protected_twitter_2 = os.environ.get('PROTECTED_TWITTER_2', 'TwythonSecure2') + +# Test Ids +test_tweet_id = os.environ.get('TEST_TWEET_ID', '318577428610031617') +test_list_id = os.environ.get('TEST_LIST_ID', '574') # 574 is @twitter/team + + +class TwythonAPITestCase(unittest.TestCase): + def setUp(self): + self.api = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + + # Timelines + def test_get_mentions_timeline(self): + '''Test returning mentions timeline for authenticated user succeeds''' + self.api.get_mentions_timeline() + + def test_get_user_timeline(self): + '''Test returning timeline for authenticated user and random user + succeeds''' + self.api.get_user_timeline() # Authenticated User Timeline + self.api.get_user_timeline(screen_name='twitter') # Random User Timeline + + def test_get_protected_user_timeline_following(self): + '''Test returning a protected user timeline who you are following + succeeds''' + self.api.get_user_timeline(screen_name=protected_twitter_1) + + def test_get_protected_user_timeline_not_following(self): + '''Test returning a protected user timeline who you are not following + fails and raise a TwythonAuthError''' + self.assertRaises(TwythonAuthError, self.api.get_user_timeline, + screen_name=protected_twitter_2) + + def test_get_home_timeline(self): + '''Test returning home timeline for authenticated user succeeds''' + self.api.get_home_timeline() + + # Tweets + def test_get_retweets(self): + '''Test getting retweets of a specific tweet succeeds''' + self.api.get_retweets(id=test_tweet_id) + + def test_show_status(self): + '''Test returning a single status details succeeds''' + self.api.show_status(id=test_tweet_id) + + def test_update_and_destroy_status(self): + '''Test updating and deleting a status succeeds''' + status = self.api.update_status(status='Test post just to get deleted :(') + self.api.destroy_status(id=status['id_str']) + + def test_retweet(self): + '''Test retweeting a status succeeds''' + retweet = self.api.retweet(id='99530515043983360') + self.api.destroy_status(id=retweet['id_str']) + + def test_retweet_twice(self): + '''Test that trying to retweet a tweet twice raises a TwythonError''' + retweet = self.api.retweet(id='99530515043983360') + self.assertRaises(TwythonError, self.api.retweet, + id='99530515043983360') + + # Then clean up + self.api.destroy_status(id=retweet['id_str']) + + def test_get_oembed_tweet(self): + '''Test getting info to embed tweet on Third Party site succeeds''' + self.api.get_oembed_tweet(id='99530515043983360') + + def test_get_retweeters_ids(self): + '''Test getting ids for people who retweeted a tweet succeeds''' + self.api.get_retweeters_ids(id='99530515043983360') + + # Search + def test_search(self): + '''Test searching tweets succeeds''' + self.api.search(q='twitter') + + # Direct Messages + def test_get_direct_messages(self): + '''Test getting the authenticated users direct messages succeeds''' + self.api.get_direct_messages() + + def test_get_sent_messages(self): + '''Test getting the authenticated users direct messages they've + sent succeeds''' + self.api.get_sent_messages() + + def test_send_get_and_destroy_direct_message(self): + '''Test sending, getting, then destory a direct message succeeds''' + message = self.api.send_direct_message(screen_name=protected_twitter_1, + text='Hey d00d!') + + self.api.get_direct_message(id=message['id_str']) + self.api.destroy_direct_message(id=message['id_str']) + + def test_send_direct_message_to_non_follower(self): + '''Test sending a direct message to someone who doesn't follow you + fails''' + self.assertRaises(TwythonError, self.api.send_direct_message, + screen_name=protected_twitter_2, text='Yo, man!') + + # Friends & Followers + def test_get_user_ids_of_blocked_retweets(self): + '''Test that collection of user_ids that the authenticated user does + not want to receive retweets from succeeds''' + self.api.get_user_ids_of_blocked_retweets(stringify_ids='true') + + def test_get_friends_ids(self): + '''Test returning ids of users the authenticated user and then a random + user is following succeeds''' + self.api.get_friends_ids() + self.api.get_friends_ids(screen_name='twitter') + + def test_get_followers_ids(self): + '''Test returning ids of users the authenticated user and then a random + user are followed by succeeds''' + self.api.get_followers_ids() + self.api.get_followers_ids(screen_name='twitter') + + def test_lookup_friendships(self): + '''Test returning relationships of the authenticating user to the + comma-separated list of up to 100 screen_names or user_ids provided + succeeds''' + self.api.lookup_friendships(screen_name='twitter,ryanmcgrath') + + def test_get_incoming_friendship_ids(self): + '''Test returning incoming friendship ids succeeds''' + self.api.get_incoming_friendship_ids() + + def test_get_outgoing_friendship_ids(self): + '''Test returning outgoing friendship ids succeeds''' + self.api.get_outgoing_friendship_ids() + + def test_create_friendship(self): + '''Test creating a friendship succeeds''' + self.api.create_friendship(screen_name='justinbieber') + + def test_destroy_friendship(self): + '''Test destroying a friendship succeeds''' + self.api.destroy_friendship(screen_name='justinbieber') + + def test_update_friendship(self): + '''Test updating friendships succeeds''' + self.api.update_friendship(screen_name=protected_twitter_1, + retweets='true') + + self.api.update_friendship(screen_name=protected_twitter_1, + retweets='false') + + def test_show_friendships(self): + '''Test showing specific friendship succeeds''' + self.api.show_friendship(target_screen_name=protected_twitter_1) + + def test_get_friends_list(self): + '''Test getting list of users authenticated user then random user is + following succeeds''' + self.api.get_friends_list() + self.api.get_friends_list(screen_name='twitter') + + def test_get_followers_list(self): + '''Test getting list of users authenticated user then random user are + followed by succeeds''' + self.api.get_followers_list() + self.api.get_followers_list(screen_name='twitter') + + # Users + def test_get_account_settings(self): + '''Test getting the authenticated user account settings succeeds''' + self.api.get_account_settings() + + def test_verify_credentials(self): + '''Test representation of the authenticated user call succeeds''' + self.api.verify_credentials() + + def test_update_account_settings(self): + '''Test updating a user account settings succeeds''' + self.api.update_account_settings(lang='en') + + def test_update_delivery_service(self): + '''Test updating delivery settings fails because we don't have + a mobile number on the account''' + self.assertRaises(TwythonError, self.api.update_delivery_service, + device='none') + + def test_update_profile(self): + '''Test updating profile succeeds''' + self.api.update_profile(include_entities='true') + + def test_update_profile_colors(self): + '''Test updating profile colors succeeds''' + self.api.update_profile_colors(profile_background_color='3D3D3D') + + def test_list_blocks(self): + '''Test listing users who are blocked by the authenticated user + succeeds''' + self.api.list_blocks() + + def test_list_block_ids(self): + '''Test listing user ids who are blocked by the authenticated user + succeeds''' + self.api.list_block_ids() + + def test_create_block(self): + '''Test blocking a user succeeds''' + self.api.create_block(screen_name='justinbieber') + + def test_destroy_block(self): + '''Test unblocking a user succeeds''' + self.api.destroy_block(screen_name='justinbieber') + + def test_lookup_user(self): + '''Test listing a number of user objects succeeds''' + self.api.lookup_user(screen_name='twitter,justinbieber') + + def test_show_user(self): + '''Test showing one user works''' + self.api.show_user(screen_name='twitter') + + def test_search_users(self): + '''Test that searching for users succeeds''' + self.api.search_users(q='Twitter API') + + def test_get_contributees(self): + '''Test returning list of accounts the specified user can + contribute to succeeds''' + self.api.get_contributees(screen_name='TechCrunch') + + def test_get_contributors(self): + '''Test returning list of accounts that contribute to the + authenticated user fails because we are not a Contributor account''' + self.assertRaises(TwythonError, self.api.get_contributors, + screen_name=screen_name) + + def test_remove_profile_banner(self): + '''Test removing profile banner succeeds''' + self.api.remove_profile_banner() + + def test_get_profile_banner_sizes(self): + '''Test getting list of profile banner sizes fails because + we have not uploaded a profile banner''' + self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) + + # Suggested Users + def test_get_user_suggestions_by_slug(self): + '''Test getting user suggestions by slug succeeds''' + self.api.get_user_suggestions_by_slug(slug='twitter') + + def test_get_user_suggestions(self): + '''Test getting user suggestions succeeds''' + self.api.get_user_suggestions() + + def test_get_user_suggestions_statuses_by_slug(self): + '''Test getting status of suggested users succeeds''' + self.api.get_user_suggestions_statuses_by_slug(slug='funny') + + # Favorites + def test_get_favorites(self): + '''Test getting list of favorites for the authenticated + user succeeds''' + self.api.get_favorites() + + def test_create_and_destroy_favorite(self): + '''Test creating and destroying a favorite on a tweet succeeds''' + self.api.create_favorite(id=test_tweet_id) + self.api.destroy_favorite(id=test_tweet_id) + + # Lists + def test_show_lists(self): + '''Test show lists for specified user''' + self.api.show_lists(screen_name='twitter') + + def test_get_list_statuses(self): + '''Test timeline of tweets authored by members of the + specified list succeeds''' + self.api.get_list_statuses(id=test_list_id) + + def test_create_update_destroy_list_add_remove_list_members(self): + '''Test create a list, adding and removing members then + deleting the list succeeds''' + the_list = self.api.create_list(name='Stuff') + list_id = the_list['id_str'] + + self.api.update_list(list_id=list_id, name='Stuff Renamed') + + # Multi add/delete members + self.api.create_list_members(list_id=list_id, + screen_name='johncena,xbox') + self.api.delete_list_members(list_id=list_id, + screen_name='johncena,xbox') + + # Single add/delete member + self.api.add_list_member(list_id=list_id, screen_name='justinbieber') + self.api.delete_list_member(list_id=list_id, screen_name='justinbieber') + + self.api.delete_list(list_id=list_id) + + def test_get_list_memberships(self): + '''Test list of lists the authenticated user is a member of succeeds''' + self.api.get_list_memberships() + + def test_get_list_subscribers(self): + '''Test list of subscribers of a specific list succeeds''' + self.api.get_list_subscribers(list_id=test_list_id) + + def test_subscribe_is_subbed_and_unsubscribe_to_list(self): + '''Test subscribing, is a list sub and unsubbing to list succeeds''' + self.api.subscribe_to_list(list_id=test_list_id) + # Returns 404 if user is not a subscriber + self.api.is_list_subscriber(list_id=test_list_id, + screen_name=screen_name) + self.api.unsubscribe_from_list(list_id=test_list_id) + + def test_is_list_member(self): + '''Test returning if specified user is member of a list succeeds''' + # Returns 404 if not list member + self.api.is_list_member(list_id=test_list_id, screen_name='jack') + + def test_get_list_members(self): + '''Test listing members of the specified list succeeds''' + self.api.get_list_members(list_id=test_list_id) + + def test_get_specific_list(self): + '''Test getting specific list succeeds''' + self.api.get_specific_list(list_id=test_list_id) + + def test_get_list_subscriptions(self): + '''Test collection of the lists the specified user is + subscribed to succeeds''' + self.api.get_specific_list(screen_name='twitter') + + def test_show_owned_lists(self): + '''Test collection of lists the specified user owns succeeds''' + self.api.get_owned_lists(screen_name='twitter') + + # Saved Searches + def test_get_saved_searches(self): + '''Test getting list of saved searches for authenticated + user succeeds''' + self.api.get_saved_searches() + + def test_create_get_destroy_saved_search(self): + '''Test getting list of saved searches for authenticated + user succeeds''' + saved_search = self.api.create_saved_search(query='#ArnoldPalmer') + saved_search_id = saved_search['id_str'] + + self.api.show_saved_search(id=saved_search_id) + self.api.destory_saved_search(id=saved_search_id) + + # Places & Geo + def test_get_geo_info(self): + '''Test getting info about a geo location succeeds''' + self.api.get_geo_info(place_id='df51dec6f4ee2b2c') + + def test_reverse_geo_code(self): + '''Test reversing geocode succeeds''' + self.api.reverse_geocode(lat='37.76893497', long='-122.42284884') + + def test_search_geo(self): + '''Test search for places that can be attached + to a statuses/update succeeds''' + self.api.search_geo(query='Toronto') + + def test_get_similar_places(self): + '''Test locates places near the given coordinates which + are similar in name succeeds''' + self.api.get_similar_places(lat='37', long='-122', name='Twitter HQ') + + # Trends + def test_get_place_trends(self): + '''Test getting the top 10 trending topics for a specific + WOEID succeeds''' + self.api.get_place_trends(id=1) + + def test_get_available_trends(self): + '''Test returning locations that Twitter has trending + topic information for succeeds''' + self.api.get_available_trends() + + def test_get_closest_trends(self): + '''Test getting the locations that Twitter has trending topic + information for, closest to a specified location succeeds''' + self.api.get_closest_trends(lat='37', long='-122') + + # Spam Reporting + def test_report_spam(self): + '''Test reporting user succeeds''' + self.api.report_spam(screen_name='justinbieber') + + +if __name__ == '__main__': + unittest.main() From d3e17dcd4bf31166602e1f54cb0d349012c83156 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 21:52:25 -0400 Subject: [PATCH 391/687] Auth test, updating func names and tests that failed Coverage 56% --- .travis.yml | 2 +- test_twython.py | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3f75b4d..be9c1f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ env: PROTECTED_TWITTER_2='TwythonSecure2' TEST_TWEET_ID='332992304010899457' TEST_LIST_ID='574' -script: nosetests -v test_twython:TwythonAPITestCase --cover-package="twython" --with-coverage +script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --cover-package="twython" --with-coverage install: - pip install -r requirements.txt notifications: diff --git a/test_twython.py b/test_twython.py index 84db80c..2659108 100644 --- a/test_twython.py +++ b/test_twython.py @@ -21,6 +21,17 @@ test_tweet_id = os.environ.get('TEST_TWEET_ID', '318577428610031617') test_list_id = os.environ.get('TEST_LIST_ID', '574') # 574 is @twitter/team +class TwythonAuthTestCase(unittest.TestCase): + def setUp(self): + self.api = Twython(app_key, app_secret) + + def test_get_authentication_tokens(self): + '''Test getting authentication tokens works''' + self.api.get_authentication_tokens(callback_url='http://google.com/', + force_login=True, + screen_name=screen_name) + + class TwythonAPITestCase(unittest.TestCase): def setUp(self): self.api = Twython(app_key, app_secret, @@ -290,7 +301,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_get_list_statuses(self): '''Test timeline of tweets authored by members of the specified list succeeds''' - self.api.get_list_statuses(id=test_list_id) + self.api.get_list_statuses(list_id=test_list_id) def test_create_update_destroy_list_add_remove_list_members(self): '''Test create a list, adding and removing members then @@ -344,11 +355,11 @@ class TwythonAPITestCase(unittest.TestCase): def test_get_list_subscriptions(self): '''Test collection of the lists the specified user is subscribed to succeeds''' - self.api.get_specific_list(screen_name='twitter') + self.api.get_list_subscriptions(screen_name='twitter') def test_show_owned_lists(self): '''Test collection of lists the specified user owns succeeds''' - self.api.get_owned_lists(screen_name='twitter') + self.api.show_owned_lists(screen_name='twitter') # Saved Searches def test_get_saved_searches(self): @@ -359,11 +370,11 @@ class TwythonAPITestCase(unittest.TestCase): def test_create_get_destroy_saved_search(self): '''Test getting list of saved searches for authenticated user succeeds''' - saved_search = self.api.create_saved_search(query='#ArnoldPalmer') + saved_search = self.api.create_saved_search(query='#Twitter') saved_search_id = saved_search['id_str'] self.api.show_saved_search(id=saved_search_id) - self.api.destory_saved_search(id=saved_search_id) + self.api.destroy_saved_search(id=saved_search_id) # Places & Geo def test_get_geo_info(self): From 0d3ae38c390ffe4b597efe502d161a8674041718 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 22:07:05 -0400 Subject: [PATCH 392/687] Update .travis.yml --- .travis.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index be9c1f4..572a778 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,19 @@ language: python python: - - 2.6 - - 2.7 - - 3.3 + - '2.6' + - '2.7' + - '3.3' env: - APP_KEY='kpowaBNkhhXwYUu3es27dQ' - APP_SECRET='iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU' - OAUTH_TOKEN='1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx' - OAUTH_TOKEN_SECRET='dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg' - SCREEN_NAME='TwythonTest' - PROTECTED_TWITTER_1='TwythonSecure1' - PROTECTED_TWITTER_2='TwythonSecure2' - TEST_TWEET_ID='332992304010899457' - TEST_LIST_ID='574' + - APP_KEY='kpowaBNkhhXwYUu3es27dQ' + - APP_SECRET='iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU' + - OAUTH_TOKEN='1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx' + - OAUTH_TOKEN_SECRET='dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg' + - SCREEN_NAME='TwythonTest' + - PROTECTED_TWITTER_1='TwythonSecure1' + - PROTECTED_TWITTER_2='TwythonSecure2' + - TEST_TWEET_ID='332992304010899457' + - TEST_LIST_ID='574' script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --cover-package="twython" --with-coverage -install: - - pip install -r requirements.txt +install: pip install -r requirements.txt notifications: email: false From bb5e0dece15e1715099f58b3fc77a1803d5d7bd2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 16:36:53 -0400 Subject: [PATCH 393/687] Adding secure travis keys Changed keys for the old dev app, so they're invalid now. Adding secure generated keys for app key/secret, oauth token/secret --- .travis.yml | 25 +++++++++++++------------ test_twython.py | 8 ++++---- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index 572a778..f968a0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,19 @@ language: python python: - - '2.6' - - '2.7' - - '3.3' + - 2.6 + - 2.7 + - 3.3 env: - - APP_KEY='kpowaBNkhhXwYUu3es27dQ' - - APP_SECRET='iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU' - - OAUTH_TOKEN='1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx' - - OAUTH_TOKEN_SECRET='dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg' - - SCREEN_NAME='TwythonTest' - - PROTECTED_TWITTER_1='TwythonSecure1' - - PROTECTED_TWITTER_2='TwythonSecure2' - - TEST_TWEET_ID='332992304010899457' - - TEST_LIST_ID='574' + global: + - secure: "U+9hLkIIV004Ap4w7EA0THPBErvdL8fIpZ6BuwVcf6MMRwZ3Sqm4s23ukvQs\nTUGa9+97gsh41RCijpriAQLogXtyRZlwWpk3rWQmMavxaiibciNE+Py9GG5z\nUpgw+AWNS2ZfRsBsg2GhsWLk+UHb63ixFbU0PCRPalx9e6ycW2g=" + - secure: "Zf2Q7mBwWdM7sDboyZ5KR3qj5M6J9XqCFNbDaPEHJru8FSv0HB6WCQe7ddh6\nQ3OKCGiCQyFgzBPfBKD6qIVmYYbiGqFD75grKG/0M4ZJw1CSrkOQQ3qs/Nlk\nhuKmTpY7zx+c1m/XjHSWE2nW7bWnRybDDsJL0y7gcfeupwePyDU=" + - secure: "cXRljeHb4/jS5rMb24uLhC8pdQU0psqdT5ErZLYiOlxxG7SpM0Nn3ULiZ5xT\nK7K7s3bkt2MyFc68MST9TwZS7UUGYopDBV/SF0+EXdtMmtUZ5pm552G4Lwqf\nrXmQDYZDrLrq8T+1sMSGiLhUUP9kKyVdN8Oy1UzuVkJrN4in/Do=" + - secure: "GqPSc1AHB8mVaQtw3NL2ZT1pOi3QARPmw+fNeU0zl66dsFIt3GsFPsk3ncjn\n5OdsRjsvwyyE/SMreJvARxnxEAlxsQ2t/UWBPwaeYJjcnkZ6wJ66UcWw63YT\nX5XEmrbJy58bo5qZ3rABFb5JW4rWb9q/02L48riHVSuvzYi6YP8=" + - SCREEN_NAME=TwythonTest + - PROTECTED_TWITTER_1=TwythonSecure1 + - PROTECTED_TWITTER_2=TwythonSecure2 + - TEST_TWEET_ID=332992304010899457 + - TEST_LIST_ID=574 script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --cover-package="twython" --with-coverage install: pip install -r requirements.txt notifications: diff --git a/test_twython.py b/test_twython.py index 2659108..26008cf 100644 --- a/test_twython.py +++ b/test_twython.py @@ -3,10 +3,10 @@ import os from twython import Twython, TwythonError, TwythonAuthError -app_key = os.environ.get('APP_KEY', 'kpowaBNkhhXwYUu3es27dQ') -app_secret = os.environ.get('APP_SECRET', 'iQ0Jr0e2xvhOwLubifWAFtmXnVT7VZAIqUXnI2FcCjU') -oauth_token = os.environ.get('OAUTH_TOKEN', '1419197916-OZKOynuB1rZ1g4DXwS2wjr1wGnknPHCUEkbvvKx') -oauth_token_secret = os.environ.get('OAUTH_TOKEN_SECRET', 'dh9YbCkDR3h3XQFXA7pjufI6S55CCZ6UjdvBNUmi1hg') +app_key = os.environ.get('APP_KEY') +app_secret = os.environ.get('APP_SECRET') +oauth_token = os.environ.get('OAUTH_TOKEN') +oauth_token_secret = os.environ.get('OAUTH_TOKEN_SECRET') screen_name = os.environ.get('SCREEN_NAME', 'TwythonTest') From a76b93e49107d987bb7633a7374909be2a27ccf5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 16:55:55 -0400 Subject: [PATCH 394/687] Okay, let's try this secure stuff again --- .travis.yml | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index f968a0f..8f1d82d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,29 @@ python: - 2.6 - 2.7 - 3.3 -env: - global: - - secure: "U+9hLkIIV004Ap4w7EA0THPBErvdL8fIpZ6BuwVcf6MMRwZ3Sqm4s23ukvQs\nTUGa9+97gsh41RCijpriAQLogXtyRZlwWpk3rWQmMavxaiibciNE+Py9GG5z\nUpgw+AWNS2ZfRsBsg2GhsWLk+UHb63ixFbU0PCRPalx9e6ycW2g=" - - secure: "Zf2Q7mBwWdM7sDboyZ5KR3qj5M6J9XqCFNbDaPEHJru8FSv0HB6WCQe7ddh6\nQ3OKCGiCQyFgzBPfBKD6qIVmYYbiGqFD75grKG/0M4ZJw1CSrkOQQ3qs/Nlk\nhuKmTpY7zx+c1m/XjHSWE2nW7bWnRybDDsJL0y7gcfeupwePyDU=" - - secure: "cXRljeHb4/jS5rMb24uLhC8pdQU0psqdT5ErZLYiOlxxG7SpM0Nn3ULiZ5xT\nK7K7s3bkt2MyFc68MST9TwZS7UUGYopDBV/SF0+EXdtMmtUZ5pm552G4Lwqf\nrXmQDYZDrLrq8T+1sMSGiLhUUP9kKyVdN8Oy1UzuVkJrN4in/Do=" - - secure: "GqPSc1AHB8mVaQtw3NL2ZT1pOi3QARPmw+fNeU0zl66dsFIt3GsFPsk3ncjn\n5OdsRjsvwyyE/SMreJvARxnxEAlxsQ2t/UWBPwaeYJjcnkZ6wJ66UcWw63YT\nX5XEmrbJy58bo5qZ3rABFb5JW4rWb9q/02L48riHVSuvzYi6YP8=" - - SCREEN_NAME=TwythonTest - - PROTECTED_TWITTER_1=TwythonSecure1 - - PROTECTED_TWITTER_2=TwythonSecure2 - - TEST_TWEET_ID=332992304010899457 - - TEST_LIST_ID=574 +env: + global: + - secure: |- + W+6YFPFcqhh3o9JzQj4D3FI9LdvMCrrRcbqlcbNaMqTjMo3MMLNs08bhvzFm + wWjU+vSdaZO79/WzHidifBf2BbAgF4xNfY/mOl9EycPmvjNPx2AzOtfG2rb/ + PxJoY0Vloxi0ZfJqWpiAc/xTpXQ/2lRmkVbggeQ5px3wYLOfKr4= + - secure: |- + FCsHF9JqATGopWQK+M88o6ZxEYduZKTTrQeTbm4yI8g1PksjmO9vG7cYYub0 + mlpWUwZ8EHjQKOcMPRQqE1z71rr6zGRHlyy7TaiXGZMXr3JtyTuvJDkHQjIz + DR1+/1FGlTl4dWGDiOfWWsLOKAOnI2/u/z9CROgwMybjFl5l3R8= + - secure: |- + XB0p3AQ4gWYhZMt0czR6qbWmx80qJHCC2W3Zi5b7JMJAP5alb4GqzbZyuf8M + zUFwUgM2j/93/Vds/QGL4oMVSy6ECOo4lWnXs1Gt08ZMJZrJjvTSnjWrqwL9 + txRwyxZgFuJwtmIfIILTus+GMlGeW9lKvFDJwb698Fx3faXC35A= + - secure: |- + GCwKif9aZkpBVU6IJzDQAf0o13FrS7tRsME/CG+1xorqcLPcq+G6aN+10NGN + OyWCX2RceE/3dwP3fDiypwl4hIWWzKnAza3toebomvniAKSsDGOG59j+n+QY + TDeyJJ5oIa1DYbL94aeZyc4nn/C8ZafADqiHGfOU8swSdarnIcg= + - SCREEN_NAME=TwythonTest + - PROTECTED_TWITTER_1=TwythonSecure1 + - PROTECTED_TWITTER_2=TwythonSecure2 + - TEST_TWEET_ID=332992304010899457 + - TEST_LIST_ID=574 script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --cover-package="twython" --with-coverage install: pip install -r requirements.txt notifications: From 077406a8b344e335af037e31e0839a84c86bfa0b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 17:38:28 -0400 Subject: [PATCH 395/687] Adding coverage to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 09acfa6..70bd0e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +coverage==3.6.0 requests==1.2.0 requests_oauthlib==0.3.1 From 008c53048a3eaef180bc99f97ab678e22663eab5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 21:23:50 -0400 Subject: [PATCH 396/687] Cooler twitter handle, different secure tokens --- .travis.yml | 26 +++++++++++++------------- test_twython.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f1d82d..839a6d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,22 +6,22 @@ python: env: global: - secure: |- - W+6YFPFcqhh3o9JzQj4D3FI9LdvMCrrRcbqlcbNaMqTjMo3MMLNs08bhvzFm - wWjU+vSdaZO79/WzHidifBf2BbAgF4xNfY/mOl9EycPmvjNPx2AzOtfG2rb/ - PxJoY0Vloxi0ZfJqWpiAc/xTpXQ/2lRmkVbggeQ5px3wYLOfKr4= + YZRZzjEVXWcKYWz+L+TI7Odun0ak8ORBHrg4UzrzzX6ok1cZAS4UxAT42DpS + xknP+sUVNziHpOVDVDnIZ7XG8+NISfBC+mwFmOLfxPiXFqC+/6XZWv4FlHJO + op5AummUolF8bX7SFOLjG6/Eox9mFOsw8+aIqhly44hlmJ2tpLY= - secure: |- - FCsHF9JqATGopWQK+M88o6ZxEYduZKTTrQeTbm4yI8g1PksjmO9vG7cYYub0 - mlpWUwZ8EHjQKOcMPRQqE1z71rr6zGRHlyy7TaiXGZMXr3JtyTuvJDkHQjIz - DR1+/1FGlTl4dWGDiOfWWsLOKAOnI2/u/z9CROgwMybjFl5l3R8= + NzPo81Xb2xuMwcT1JszIPSohGDdJvwim/8zALMSr2ZirL74O3Edsq0DXG6dm + vy9jcppjKsa8bq9SFrqikVMDUsP/ktW+VoTDbA/JvPBtjFpdDot53T//arsA + bd494n9a4kQG5gnAnqSRy9QRAXMAs4M3IuccjR0CuebbsXIzLZQ= - secure: |- - XB0p3AQ4gWYhZMt0czR6qbWmx80qJHCC2W3Zi5b7JMJAP5alb4GqzbZyuf8M - zUFwUgM2j/93/Vds/QGL4oMVSy6ECOo4lWnXs1Gt08ZMJZrJjvTSnjWrqwL9 - txRwyxZgFuJwtmIfIILTus+GMlGeW9lKvFDJwb698Fx3faXC35A= + Ex+cw8adFNwK7UJ1DQTz0FHprhkohsNJqRwNYrvHmaW9bIZIOAj+14uTHpN1 + u1zteyW91/Lro9JQ32b31TXWlLG0ftl+L8WdOp9+Mx36CbvwKSvxwuTkreg/ + UUdL2mBKMMdt+HCC8rvTnMuZhCkfqtpZIbSGQvIOZlhDdeULZFw= - secure: |- - GCwKif9aZkpBVU6IJzDQAf0o13FrS7tRsME/CG+1xorqcLPcq+G6aN+10NGN - OyWCX2RceE/3dwP3fDiypwl4hIWWzKnAza3toebomvniAKSsDGOG59j+n+QY - TDeyJJ5oIa1DYbL94aeZyc4nn/C8ZafADqiHGfOU8swSdarnIcg= - - SCREEN_NAME=TwythonTest + BAhyQxN6cQv9PQgtSYG2HjuOHbhfX8bePJ7QA5wQpqh5NG2jFviY/AQMAt3t + l7jd8ZFscac4nygpypX1T3VBd7ZjN94SND17KiLC98SSW8lJPh4Ef/+4/6L3 + /ZSFsZebbr3y5E8Dzm5bzVlOVDC+o2mTDF51ckr+0l4VhAu6vE0= + - SCREEN_NAME=__twython__ - PROTECTED_TWITTER_1=TwythonSecure1 - PROTECTED_TWITTER_2=TwythonSecure2 - TEST_TWEET_ID=332992304010899457 diff --git a/test_twython.py b/test_twython.py index 26008cf..3c9c341 100644 --- a/test_twython.py +++ b/test_twython.py @@ -8,7 +8,7 @@ app_secret = os.environ.get('APP_SECRET') oauth_token = os.environ.get('OAUTH_TOKEN') oauth_token_secret = os.environ.get('OAUTH_TOKEN_SECRET') -screen_name = os.environ.get('SCREEN_NAME', 'TwythonTest') +screen_name = os.environ.get('SCREEN_NAME', '__twython__') # Protected Account you ARE following and they ARE following you protected_twitter_1 = os.environ.get('PROTECTED_TWITTER_1', 'TwythonSecure1') From ea0b646fd103f48fa40cebbd2e51ae37088ebf09 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 12:40:25 -0400 Subject: [PATCH 397/687] Fixes #194 [ci skip] --- twython/endpoints.py | 20 ++++++-- twython/helpers.py | 16 ++++++ twython/twython.py | 116 +++---------------------------------------- 3 files changed, 39 insertions(+), 113 deletions(-) create mode 100644 twython/helpers.py diff --git a/twython/endpoints.py b/twython/endpoints.py index 2695af4..1246a3d 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -56,7 +56,10 @@ api_table = { 'url': '/statuses/retweet/{{id}}.json', 'method': 'POST', }, - # See twython.py for update_status_with_media + 'update_status_with_media': { + 'url': '/statuses/update_with_media.json', + 'method': 'POST', + }, 'get_oembed_tweet': { 'url': '/statuses/oembed.json', 'method': 'GET', @@ -169,12 +172,18 @@ api_table = { 'url': '/account/update_profile.json', 'method': 'POST', }, - # See twython.py for update_profile_background_image + 'update_profile_background_image': { + 'url': '/account/update_profile_banner.json', + 'method': 'POST', + }, 'update_profile_colors': { 'url': '/account/update_profile_colors.json', 'method': 'POST', }, - # See twython.py for update_profile_image + 'update_profile_image': { + 'url': '/account/update_profile_image.json', + 'method': 'POST', + }, 'list_blocks': { 'url': '/blocks/list.json', 'method': 'GET', @@ -215,7 +224,10 @@ api_table = { 'url': '/account/remove_profile_banner.json', 'method': 'POST', }, - # See twython.py for update_profile_banner + 'update_profile_background_image': { + 'url': '/account/update_profile_background_image.json', + 'method': 'POST', + }, 'get_profile_banner_sizes': { 'url': '/users/profile_banner.json', 'method': 'GET', diff --git a/twython/helpers.py b/twython/helpers.py new file mode 100644 index 0000000..b2ec713 --- /dev/null +++ b/twython/helpers.py @@ -0,0 +1,16 @@ +def _transparent_params(_params): + params = {} + files = {} + for k, v in _params.items(): + if hasattr(v, 'read') and callable(v.read): + files[k] = v + elif isinstance(v, bool): + if v: + params[k] = 'true' + else: + params[k] = 'false' + elif isinstance(v, basestring): + params[k] = v + else: + continue + return params, files diff --git a/twython/twython.py b/twython/twython.py index ae509b5..4079c30 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -9,6 +9,7 @@ from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError +from .helpers import _transparent_params warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > @@ -126,7 +127,7 @@ class Twython(object): return content - def _request(self, url, method='GET', params=None, files=None, api_call=None): + def _request(self, url, method='GET', params=None, api_call=None): '''Internal response generator, no sense in repeating the same code twice, right? ;) ''' @@ -134,6 +135,7 @@ class Twython(object): params = params or {} func = getattr(self.client, method) + params, files = _transparent_params(params) if method == 'get': response = func(url, params=params) else: @@ -201,7 +203,7 @@ class Twython(object): we haven't gotten around to putting it in Twython yet. :) ''' - def request(self, endpoint, method='GET', params=None, files=None, version='1.1'): + def request(self, endpoint, method='GET', params=None, version='1.1'): # In case they want to pass a full Twitter URL # i.e. https://search.twitter.com/ if endpoint.startswith('http://') or endpoint.startswith('https://'): @@ -209,15 +211,15 @@ class Twython(object): else: url = '%s/%s.json' % (self.api_url % version, endpoint) - content = self._request(url, method=method, params=params, files=files, api_call=url) + content = self._request(url, method=method, params=params, api_call=url) return content def get(self, endpoint, params=None, version='1.1'): return self.request(endpoint, params=params, version=version) - def post(self, endpoint, params=None, files=None, version='1.1'): - return self.request(endpoint, 'POST', params=params, files=files, version=version) + def post(self, endpoint, params=None, version='1.1'): + return self.request(endpoint, 'POST', params=params, version=version) # End Dynamic Request Methods @@ -380,110 +382,6 @@ class Twython(object): for tweet in self.searchGen(search_query, **kwargs): yield tweet - # The following methods are apart from the other Account methods, - # because they rely on a whole multipart-data posting function set. - - ## Media Uploading functions ############################################## - - def updateProfileBackgroundImage(self, file_, version='1.1', **params): - warnings.warn( - 'This method is deprecated, please use `update_profile_background_image` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.update_profile_background_image(file_, version, **params) - - def update_profile_background_image(self, file_, version='1.1', **params): - """Updates the authenticating user's profile background image. - - :param file_: (required) A string to the location of the file - (less than 800KB in size, larger than 2048px width will scale down) - :param version: (optional) A number, default 1.1 because that's the - current API version for Twitter (Legacy = 1) - - **params - You may pass items that are stated in this doc - (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image) - """ - - return self.post('account/update_profile_background_image', - params=params, - files={'image': (file_, open(file_, 'rb'))}, - version=version) - - def updateProfileImage(self, file_, version='1.1', **params): - warnings.warn( - 'This method is deprecated, please use `update_profile_image` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.update_profile_image(file_, version, **params) - - def update_profile_image(self, file_, version='1.1', **params): - """Updates the authenticating user's profile image (avatar). - - :param file_: (required) A string to the location of the file - :param version: (optional) A number, default 1.1 because that's the - current API version for Twitter (Legacy = 1) - - **params - You may pass items that are stated in this doc - (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image) - """ - - return self.post('account/update_profile_image', - params=params, - files={'image': (file_, open(file_, 'rb'))}, - version=version) - - def updateStatusWithMedia(self, file_, version='1.1', **params): - warnings.warn( - 'This method is deprecated, please use `update_status_with_media` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.update_status_with_media(file_, version, **params) - - def update_status_with_media(self, file_, version='1.1', **params): - """Updates the users status with media - - :param file_: (required) A string to the location of the file - :param version: (optional) A number, default 1.1 because that's the - current API version for Twitter (Legacy = 1) - - **params - You may pass items that are taken in this doc - (https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media) - """ - - return self.post('statuses/update_with_media', - params=params, - files={'media': (file_, open(file_, 'rb'))}, - version=version) - - def updateProfileBannerImage(self, file_, version='1.1', **params): - warnings.warn( - 'This method is deprecated, please use `update_profile_banner_image` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.update_profile_banner_image(file_, version, **params) - - def update_profile_banner_image(self, file_, version='1.1', **params): - """Updates the users profile banner - - :param file_: (required) A string to the location of the file - :param version: (optional) A number, default 1 because that's the - only API version for Twitter that supports this call - - **params - You may pass items that are taken in this doc - (https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner) - """ - - return self.post('account/update_profile_banner', - params=params, - files={'banner': (file_, open(file_, 'rb'))}, - version=version) - - ########################################################################### - @staticmethod def unicode2utf8(text): try: From 6238912b9669718a6f2cfb874c88a3bfc6664a29 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 13:36:46 -0400 Subject: [PATCH 398/687] Update examples and READMEs [ci skip] --- README.md | 49 ++++++++++++++++++++++++++++++++ README.rst | 48 +++++++++++++++++++++++++++++++ examples/update_profile_image.py | 4 ++- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29e4d3b..a357e2e 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,55 @@ except TwythonError as e: print e ``` +#### Posting a Status with an Image +```python +from twython import Twython + +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + +# The file key that Twitter expects for updating a status with an image +# is 'media', so 'media' will be apart of the params dict. + +# You can pass any object that has a read() function (like a StringIO object) +# In case you wanted to resize it first or something! + +photo = open('/path/to/file/image.jpg', 'rb') +t.update_status_with_media(media=photo, status='Check out my image!') +``` + +#### Posting a Status with an Editing Image *(This example resizes an image)* +```python +from twython import Twython + +t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + +# Like I said in the previous section, you can pass any object that has a +# read() method + +# Assume you are working with a JPEG + +from PIL import Image +from StringIO import StringIO + +photo = Image.open('/path/to/file/image.jpg') + +basewidth = 320 +wpercent = (basewidth / float(photo.size[0])) +height = int((float(photo.size[1]) * float(wpercent))) +photo = photo.resize((basewidth, height), Image.ANTIALIAS) + +image_io = StringIO.StringIO() +photo.save(image_io, format='JPEG') + +# If you do not seek(0), the image will be at the end of the file and +# unable to be read +image_io.seek(0) + +t.update_status_with_media(media=photo, status='Check out my edited image!') +``` + ##### Streaming API ```python diff --git a/README.rst b/README.rst index 9e6ec44..0e44426 100644 --- a/README.rst +++ b/README.rst @@ -144,6 +144,54 @@ and except TwythonError as e: print e +Posting a Status with an Image +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + from twython import Twython + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + + # The file key that Twitter expects for updating a status with an image + # is 'media', so 'media' will be apart of the params dict. + + # You can pass any object that has a read() function (like a StringIO object) + # In case you wanted to resize it first or something! + + photo = open('/path/to/file/image.jpg', 'rb') + t.update_status_with_media(media=photo, status='Check out my image!') + +Posting a Status with an Editing Image *(This example resizes an image)* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + from twython import Twython + + t = Twython(app_key, app_secret, + oauth_token, oauth_token_secret) + + # Like I said in the previous section, you can pass any object that has a + # read() method + + # Assume you are working with a JPEG + + from PIL import Image + from StringIO import StringIO + + photo = Image.open('/path/to/file/image.jpg') + + basewidth = 320 + wpercent = (basewidth / float(photo.size[0])) + height = int((float(photo.size[1]) * float(wpercent))) + photo = photo.resize((basewidth, height), Image.ANTIALIAS) + + image_io = StringIO.StringIO() + photo.save(image_io, format='JPEG') + + # If you do not seek(0), the image will be at the end of the file and + # unable to be read + image_io.seek(0) + + t.update_status_with_media(media=photo, status='Check out my edited image!') Streaming API ~~~~~~~~~~~~~ diff --git a/examples/update_profile_image.py b/examples/update_profile_image.py index 9921f0e..f35d4de 100644 --- a/examples/update_profile_image.py +++ b/examples/update_profile_image.py @@ -2,4 +2,6 @@ from twython import Twython # Requires Authentication as of Twitter API v1.1 twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) -twitter.update_profile_image('myImage.png') + +avatar = open('myImage.png', 'rb') +twitter.update_profile_image(image=avatar) From 27c51b8ba618f2a3dbb33f8a7b6cc94aae510fbc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:15:11 -0400 Subject: [PATCH 399/687] Fixes #192, update HISTORY --- HISTORY.rst | 3 +++ twython/twython.py | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 9f0e559..c824ff2 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,6 +6,9 @@ History - Added ``get_retweeters_ids`` method - Fixed ``TwythonDeprecationWarning`` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) - Fixed error message bubbling when error message returned from Twitter was not an array (i.e. if you try to retweet something twice, the error is not found at index 0) +- Added "transparent" parameters for making requests, meaning users can pass bool values (True, False) to Twython methods and we convert your params in the background to satisfy the Twitter API. Also, file objects can now be passed seamlessly (see examples in README and in /examples dir for details) +- Callback URL is optional in ``get_authentication_tokens`` to accomedate those using OOB authorization (non web clients) +- Not part of the python package, but tests are now available along with Travis CI hooks 2.9.1 (2013-05-04) ++++++++++++++++++ diff --git a/twython/twython.py b/twython/twython.py index 4079c30..af1e159 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -245,12 +245,14 @@ class Twython(object): def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''): """Returns a dict including an authorization URL (auth_url) to direct a user to - :param callback_url: (optional.. for now) Url the user is returned to after they authorize your app + :param callback_url: (optional) Url the user is returned to after they authorize your app (web clients only) :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value """ callback_url = callback_url or self.callback_url - request_args = {'oauth_callback': callback_url} + request_args = {} + if callback_url: + request_args['oauth_callback'] = callback_url response = self.client.get(self.request_token_url, params=request_args) if response.status_code == 401: @@ -285,7 +287,7 @@ class Twython(object): def get_authorized_tokens(self, oauth_verifier): """Returns authorized tokens after they go through the auth_url phase. - :param oauth_verifier: (required) The oauth_verifier retrieved from the callback url querystring + :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring """ response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) From c42a987f38634752b23061bda28fed8427ddb04d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:32:58 -0400 Subject: [PATCH 400/687] basestring compat for python 3 transparent params --- twython/helpers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython/helpers.py b/twython/helpers.py index b2ec713..74aea99 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -1,3 +1,6 @@ +from .compat import basestring + + def _transparent_params(_params): params = {} files = {} From 35d84021736f5509dc37f12ca92a05693cff5d47 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:45:38 -0400 Subject: [PATCH 401/687] Include ints in params too Oops ;P --- twython/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/helpers.py b/twython/helpers.py index 74aea99..7b8275b 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -12,7 +12,7 @@ def _transparent_params(_params): params[k] = 'true' else: params[k] = 'false' - elif isinstance(v, basestring): + elif isinstance(v, basestring) or isinstance(v, int): params[k] = v else: continue From 48fc5d4c36d5e562948fcc6459e31dfcb17fa69b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 17 May 2013 13:13:55 -0400 Subject: [PATCH 402/687] Update READMEs with nice "# of downloads" image [ci skip] --- README.md | 2 ++ README.rst | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index a357e2e..9b1cf9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Twython ======= +[![Downloads](https://pypip.in/d/twython/badge.png)](https://crate.io/packages/twython/) + ```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today! Features diff --git a/README.rst b/README.rst index 0e44426..255be92 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,9 @@ Twython ======= + +.. image:: https://pypip.in/d/twython/badge.png + :target: https://crate.io/packages/twython/ + ``Twython`` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today! Features From c00647a5a02d9efd9e90f126ca86c654efac935f Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 17 May 2013 21:32:44 -0400 Subject: [PATCH 403/687] Remove report_spam test, update tests with --logging-filter * report spam was being abused and started turning a 403 * try to only log messages from twython, oauthlib is exposing our secrets when tests fail :( --- .travis.yml | 2 +- test_twython.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 839a6d9..c5f5c32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ env: - PROTECTED_TWITTER_2=TwythonSecure2 - TEST_TWEET_ID=332992304010899457 - TEST_LIST_ID=574 -script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --cover-package="twython" --with-coverage +script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --logging-filter="twython" --cover-package="twython" --with-coverage install: pip install -r requirements.txt notifications: email: false diff --git a/test_twython.py b/test_twython.py index 3c9c341..759dcb4 100644 --- a/test_twython.py +++ b/test_twython.py @@ -411,11 +411,6 @@ class TwythonAPITestCase(unittest.TestCase): information for, closest to a specified location succeeds''' self.api.get_closest_trends(lat='37', long='-122') - # Spam Reporting - def test_report_spam(self): - '''Test reporting user succeeds''' - self.api.report_spam(screen_name='justinbieber') - if __name__ == '__main__': unittest.main() From f7f19dbdc3fa834a2bec216200db8bbeb5176263 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 18 May 2013 10:45:25 -0400 Subject: [PATCH 404/687] Updated app/oauth secure keys They we're exposed in Travis before, so we got to keep them secret ;) [ci skip] --- .travis.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index c5f5c32..6788276 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,21 +6,21 @@ python: env: global: - secure: |- - YZRZzjEVXWcKYWz+L+TI7Odun0ak8ORBHrg4UzrzzX6ok1cZAS4UxAT42DpS - xknP+sUVNziHpOVDVDnIZ7XG8+NISfBC+mwFmOLfxPiXFqC+/6XZWv4FlHJO - op5AummUolF8bX7SFOLjG6/Eox9mFOsw8+aIqhly44hlmJ2tpLY= + bLlM8JPXiqKIryMwExTEFoEo5op3FqvQQu0yn0xv1okrcH/MEvJAsm4zDd7p + yt0riXRpp9aOkU/SkL/1TVkq5E75uXYhaLuT/BgjDmLcw/Sp7Tgsk8b5KLDM + RqSrsGXKu7GQCI3MhhWJEAWyU2WVyhZERh5wOY+ic9JCiZloqMw= - secure: |- - NzPo81Xb2xuMwcT1JszIPSohGDdJvwim/8zALMSr2ZirL74O3Edsq0DXG6dm - vy9jcppjKsa8bq9SFrqikVMDUsP/ktW+VoTDbA/JvPBtjFpdDot53T//arsA - bd494n9a4kQG5gnAnqSRy9QRAXMAs4M3IuccjR0CuebbsXIzLZQ= + JlQjabb2tADza5cEmyWuwi5pECjknkiWXj4elTl/UrSYPLeTruTBYBlvtrOl + 4XF3RWcPwxcBr1JD/Ze1JxMVebYUpvZSTXZXFq6jbjcQTBa7QuH6rraxnj6W + /Abx+NYxSBcEex/RsZtSqshzCZGAOI0mdaSdQMd3k0Gxhsg+eRo= - secure: |- - Ex+cw8adFNwK7UJ1DQTz0FHprhkohsNJqRwNYrvHmaW9bIZIOAj+14uTHpN1 - u1zteyW91/Lro9JQ32b31TXWlLG0ftl+L8WdOp9+Mx36CbvwKSvxwuTkreg/ - UUdL2mBKMMdt+HCC8rvTnMuZhCkfqtpZIbSGQvIOZlhDdeULZFw= + kC9hGpdJJesmZZGMXEoPWK/lzIU6vUeguV/yI2jLgRin0EKPsgds0qR4737x + 2Z2q1+CFUlvHkl+povGcm0/A1rkNqU0KKBcxRBu/XXRxJ3DWp7gIGsmoyWUW + 68kdPOwxywZ+tj6BCD7zmStKn4I3mSzTmGKaWj8ZT0wQ91tl0Y8= - secure: |- - BAhyQxN6cQv9PQgtSYG2HjuOHbhfX8bePJ7QA5wQpqh5NG2jFviY/AQMAt3t - l7jd8ZFscac4nygpypX1T3VBd7ZjN94SND17KiLC98SSW8lJPh4Ef/+4/6L3 - /ZSFsZebbr3y5E8Dzm5bzVlOVDC+o2mTDF51ckr+0l4VhAu6vE0= + Y0M90wCpDWmSdBmgPCV2N9mMSaRMdEOis5r5sfUq/5aFTB/KDaSR9scM1g+L + 21OtvUBvaG1bdSzn0T+I5Fs/MkfbtTmuahogy83nsNDRpIZJmRIsHFmJw1fz + nEHD2Kbm4iLMYzrKto77KpxYSQMnc3sQKZjreaI31NLu+7raCAk= - SCREEN_NAME=__twython__ - PROTECTED_TWITTER_1=TwythonSecure1 - PROTECTED_TWITTER_2=TwythonSecure2 From 3dbef22cee9ea83c7e80756037209334da237d4c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 20 May 2013 10:31:32 -0400 Subject: [PATCH 405/687] Remove unused compat types from compat.py [ci skip] --- twython/compat.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/twython/compat.py b/twython/compat.py index 8da417e..e44b5b4 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -20,18 +20,10 @@ if is_py2: except ImportError: from cgi import parse_qsl - builtin_str = str - bytes = str - str = unicode basestring = basestring - numeric_types = (int, long, float) elif is_py3: from urllib.parse import urlencode, quote_plus, parse_qsl - builtin_str = str - str = str - bytes = bytes basestring = (str, bytes) - numeric_types = (int, float) From c7bce9189fa6291430b4cbf30ac9497dd11fb3db Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 11:41:43 -0400 Subject: [PATCH 406/687] Update requests dependency, add str py2/3 compat, __repr__ definition, removed unicode2utf8 & encode static methods, update HISTORY @ryanmcgrath Let me know if you're okay with the removal of Twython.unicode2utf8 and Twython.encode. I moved Twython.encode to _encode in helpers.py (only place being used is Twython.construct_api_url) If it's python 2 and unicode then we encode it, otherwise return the original value [ci skip] --- HISTORY.rst | 3 +++ requirements.txt | 2 +- setup.py | 2 +- twython/compat.py | 2 ++ twython/helpers.py | 8 +++++++- twython/twython.py | 31 +++++++++++++------------------ 6 files changed, 27 insertions(+), 21 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c824ff2..906b2cb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,9 @@ History - Added "transparent" parameters for making requests, meaning users can pass bool values (True, False) to Twython methods and we convert your params in the background to satisfy the Twitter API. Also, file objects can now be passed seamlessly (see examples in README and in /examples dir for details) - Callback URL is optional in ``get_authentication_tokens`` to accomedate those using OOB authorization (non web clients) - Not part of the python package, but tests are now available along with Travis CI hooks +- Added ``__repr__`` definition for Twython, when calling only returning +- Removed ``Twython.unicode2utf8`` and ``Twython.encode`` methods +- Cleaned up ``Twython.construct_api_url``, uses "transparent" parameters (see 4th bullet in this version for explaination) 2.9.1 (2013-05-04) ++++++++++++++++++ diff --git a/requirements.txt b/requirements.txt index 70bd0e7..edc9ff6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ coverage==3.6.0 -requests==1.2.0 +requests==1.2.1 requests_oauthlib==0.3.1 diff --git a/setup.py b/setup.py index 42de4b2..733e775 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['requests==1.2.0', 'requests_oauthlib==0.3.1'], + install_requires=['requests==1.2.1', 'requests_oauthlib==0.3.1'], # Metadata for PyPI. author='Ryan McGrath', diff --git a/twython/compat.py b/twython/compat.py index e44b5b4..79f9c2c 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -20,10 +20,12 @@ if is_py2: except ImportError: from cgi import parse_qsl + str = unicode basestring = basestring elif is_py3: from urllib.parse import urlencode, quote_plus, parse_qsl + str = str basestring = (str, bytes) diff --git a/twython/helpers.py b/twython/helpers.py index 7b8275b..76ca404 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -1,4 +1,4 @@ -from .compat import basestring +from .compat import basestring, is_py2, str def _transparent_params(_params): @@ -17,3 +17,9 @@ def _transparent_params(_params): else: continue return params, files + + +def _encode(value): + if is_py2 and isinstance(value, str): + value.encode('utf-8') + return value diff --git a/twython/twython.py b/twython/twython.py index af1e159..c877c5d 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -6,10 +6,10 @@ from requests_oauthlib import OAuth1 from . import __version__ from .advisory import TwythonDeprecationWarning -from .compat import json, urlencode, parse_qsl, quote_plus +from .compat import json, urlencode, parse_qsl, quote_plus, str from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError -from .helpers import _transparent_params +from .helpers import _encode, _transparent_params warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > @@ -106,6 +106,9 @@ class Twython(object): # create stash for last call intel self._last_call = None + def __repr__(self): + return '' % (self.app_key) + def _constructFunc(self, api_call, deprecated_key, **kwargs): # Go through and replace any mustaches that are in our API url. fn = api_table[api_call] @@ -346,7 +349,14 @@ class Twython(object): @staticmethod def construct_api_url(base_url, params): - return base_url + '?' + '&'.join(['%s=%s' % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()]) + querystring = [] + params, _ = _transparent_params(params) + params = requests.utils.to_key_val_list(params) + for (k, v) in params: + querystring.append( + '%s=%s' % (_encode(k), quote_plus(_encode(v))) + ) + return '%s?%s' % (base_url, '&'.join(querystring)) def searchGen(self, search_query, **kwargs): warnings.warn( @@ -383,18 +393,3 @@ class Twython(object): for tweet in self.searchGen(search_query, **kwargs): yield tweet - - @staticmethod - def unicode2utf8(text): - try: - if isinstance(text, unicode): - text = text.encode('utf-8') - except: - pass - return text - - @staticmethod - def encode(text): - if isinstance(text, (str, unicode)): - return Twython.unicode2utf8(text) - return str(text) From 126305d93db96029b14f234fdc27b42e220e1043 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 12:52:17 -0400 Subject: [PATCH 407/687] Revert removing unicode2utf8 and encode staticmethods [ci skip] --- HISTORY.rst | 1 - twython/helpers.py | 8 +------- twython/twython.py | 21 ++++++++++++++++++--- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 906b2cb..1a137a6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -10,7 +10,6 @@ History - Callback URL is optional in ``get_authentication_tokens`` to accomedate those using OOB authorization (non web clients) - Not part of the python package, but tests are now available along with Travis CI hooks - Added ``__repr__`` definition for Twython, when calling only returning -- Removed ``Twython.unicode2utf8`` and ``Twython.encode`` methods - Cleaned up ``Twython.construct_api_url``, uses "transparent" parameters (see 4th bullet in this version for explaination) 2.9.1 (2013-05-04) diff --git a/twython/helpers.py b/twython/helpers.py index 76ca404..7b8275b 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -1,4 +1,4 @@ -from .compat import basestring, is_py2, str +from .compat import basestring def _transparent_params(_params): @@ -17,9 +17,3 @@ def _transparent_params(_params): else: continue return params, files - - -def _encode(value): - if is_py2 and isinstance(value, str): - value.encode('utf-8') - return value diff --git a/twython/twython.py b/twython/twython.py index c877c5d..54c2979 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -6,10 +6,10 @@ from requests_oauthlib import OAuth1 from . import __version__ from .advisory import TwythonDeprecationWarning -from .compat import json, urlencode, parse_qsl, quote_plus, str +from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError -from .helpers import _encode, _transparent_params +from .helpers import _transparent_params warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > @@ -354,7 +354,7 @@ class Twython(object): params = requests.utils.to_key_val_list(params) for (k, v) in params: querystring.append( - '%s=%s' % (_encode(k), quote_plus(_encode(v))) + '%s=%s' % (Twython.encode(k), quote_plus(Twython.encode(v))) ) return '%s?%s' % (base_url, '&'.join(querystring)) @@ -393,3 +393,18 @@ class Twython(object): for tweet in self.searchGen(search_query, **kwargs): yield tweet + + @staticmethod + def unicode2utf8(text): + try: + if is_py2 and isinstance(text, str): + text = text.encode('utf-8') + except: + pass + return text + + @staticmethod + def encode(text): + if is_py2 and isinstance(text, (str)): + return Twython.unicode2utf8(text) + return str(text) From 050835e660e43580f0fade8c6d8e0d0c19856d01 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 15:20:17 -0400 Subject: [PATCH 408/687] construct_api_url params as kwarg Sometimes params isn't doesn't have to be passed [ci skip] --- twython/twython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 54c2979..286afd8 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -348,9 +348,9 @@ class Twython(object): return Twython.construct_api_url(base_url, params) @staticmethod - def construct_api_url(base_url, params): + def construct_api_url(base_url, params=None): querystring = [] - params, _ = _transparent_params(params) + params, _ = _transparent_params(params or {}) params = requests.utils.to_key_val_list(params) for (k, v) in params: querystring.append( From bf7b6727ddd76770a6c1bca6a4ecd0e2cb724d1a Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 18:22:30 -0400 Subject: [PATCH 409/687] Update requirements.txt and requirements in setup.py --- requirements.txt | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index edc9ff6..91aaa55 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ coverage==3.6.0 -requests==1.2.1 -requests_oauthlib==0.3.1 +requests==1.2.2 +requests_oauthlib==0.3.2 diff --git a/setup.py b/setup.py index 733e775..42f5a86 100755 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( include_package_data=True, # Package dependencies. - install_requires=['requests==1.2.1', 'requests_oauthlib==0.3.1'], + install_requires=['requests==1.2.2', 'requests_oauthlib==0.3.2'], # Metadata for PyPI. author='Ryan McGrath', From 48e7ccd39c65d0308f006adcb931d236d30d2f87 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 18:30:37 -0400 Subject: [PATCH 410/687] Add Travis Image to READMEs [ci skip] --- README.md | 2 +- README.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b1cf9c..7fee952 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Twython ======= -[![Downloads](https://pypip.in/d/twython/badge.png)](https://crate.io/packages/twython/) +[![Build Status](https://travis-ci.org/ryanmcgrath/twython.png?branch=master)](https://travis-ci.org/ryanmcgrath/twython) [![Downloads](https://pypip.in/d/twython/badge.png)](https://crate.io/packages/twython/) ```Twython``` is a library providing an easy (and up-to-date) way to access Twitter data in Python. Actively maintained and featuring support for both Python 2.6+ and Python 3, it's been battle tested by companies, educational institutions and individuals alike. Try it today! diff --git a/README.rst b/README.rst index 255be92..1fcb202 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ Twython ======= +.. image:: https://travis-ci.org/ryanmcgrath/twython.png?branch=master + :target: https://travis-ci.org/ryanmcgrath/twython .. image:: https://pypip.in/d/twython/badge.png :target: https://crate.io/packages/twython/ From 78c1a95dc30fa47b9665f8311d35ef7ce4cde9fa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 19:14:20 -0400 Subject: [PATCH 411/687] Update MANIFEST and HISTORY [ci skip] --- HISTORY.rst | 5 ++++- MANIFEST.in | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 1a137a6..df10007 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,9 @@ +.. :changelog: + History ------- -2.10.0 (2013-05-xx) +2.10.0 (2013-05-21) ++++++++++++++++++ - Added ``get_retweeters_ids`` method - Fixed ``TwythonDeprecationWarning`` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) @@ -11,6 +13,7 @@ History - Not part of the python package, but tests are now available along with Travis CI hooks - Added ``__repr__`` definition for Twython, when calling only returning - Cleaned up ``Twython.construct_api_url``, uses "transparent" parameters (see 4th bullet in this version for explaination) +- Update ``requests`` and ``requests-oauthlib`` requirements, fixing posting files AND post data together, making authenticated requests in general in Python 3.3 2.9.1 (2013-05-04) ++++++++++++++++++ diff --git a/MANIFEST.in b/MANIFEST.in index dd547fe..8be3760 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE README.md README.rst HISTORY.rst +include LICENSE README.md README.rst HISTORY.rst test_twython.py requirements.txt recursive-include examples * recursive-exclude examples *.pyc From 52609334bdd25eb89dbc67b75921029c37babd63 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 19:29:15 -0400 Subject: [PATCH 412/687] Update description and long description [ci skip] --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 42f5a86..d24a3de 100755 --- a/setup.py +++ b/setup.py @@ -32,9 +32,10 @@ setup( author_email='ryan@venodesigns.net', license='MIT License', url='http://github.com/ryanmcgrath/twython/tree/master', - keywords='twitter search api tweet twython', - description='An easy (and up to date) way to access Twitter data with Python.', - long_description=open('README.rst').read(), + keywords='twitter search api tweet twython stream', + description='Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs', + long_description=open('README.rst').read() + '\n\n' + + open('HISTORY.rst').read(), classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', From b99db3c90c4338c84f1d27d15d83c2784216fda1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 22:48:34 -0400 Subject: [PATCH 413/687] Fix README.rst [ci skip] --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 1fcb202..2779095 100644 --- a/README.rst +++ b/README.rst @@ -153,6 +153,7 @@ and Posting a Status with an Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: + from twython import Twython t = Twython(app_key, app_secret, @@ -170,6 +171,7 @@ Posting a Status with an Image Posting a Status with an Editing Image *(This example resizes an image)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: + from twython import Twython t = Twython(app_key, app_secret, From 464360c7f7d8bccad37eecb56e18ec9882826493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rube=CC=81n=20Varela=20Rosa?= Date: Sun, 26 May 2013 12:53:55 -0400 Subject: [PATCH 414/687] Fixed the for loop key and how to access the user name. --- examples/search_results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/search_results.py b/examples/search_results.py index 96109a4..8c4737a 100644 --- a/examples/search_results.py +++ b/examples/search_results.py @@ -7,6 +7,6 @@ try: except TwythonError as e: print e -for tweet in search_results['results']: - print 'Tweet from @%s Date: %s' % (tweet['from_user'].encode('utf-8'), tweet['created_at']) +for tweet in search_results['statuses']: + print 'Tweet from @%s Date: %s' % (tweet['user']['screen_name'].encode('utf-8'), tweet['created_at']) print tweet['text'].encode('utf-8'), '\n' From 11acb49295cffb8586378abc97c93313530645e9 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 18:33:37 -0500 Subject: [PATCH 415/687] Allow for long's as well as ints for request params _params['max_id'] 330122291755220993L type(_params['max_id']) isinstance(_params['max_id'], int) False isinstance(_params['max_id'], (long,int)) True --- twython/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twython/helpers.py b/twython/helpers.py index 7b8275b..049fb40 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -12,7 +12,7 @@ def _transparent_params(_params): params[k] = 'true' else: params[k] = 'false' - elif isinstance(v, basestring) or isinstance(v, int): + elif isinstance(v, basestring) or isinstance(v, (long,int)): params[k] = v else: continue From a246743698ba085074e584ebf0829ffaec5233fa Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 20:03:20 -0500 Subject: [PATCH 416/687] Added compat, numeric_types as allowed param type. --- twython/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twython/helpers.py b/twython/helpers.py index 049fb40..daa3370 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -1,4 +1,4 @@ -from .compat import basestring +from .compat import basestring, numeric_types def _transparent_params(_params): @@ -12,7 +12,7 @@ def _transparent_params(_params): params[k] = 'true' else: params[k] = 'false' - elif isinstance(v, basestring) or isinstance(v, (long,int)): + elif isinstance(v, basestring) or isinstance(v, numeric_types): params[k] = v else: continue From b0d801b7bb6a71ffb31d6c18092c8f1eb6b42506 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 20:05:04 -0500 Subject: [PATCH 417/687] Added numeric_types --- twython/compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/twython/compat.py b/twython/compat.py index 79f9c2c..26af6e8 100644 --- a/twython/compat.py +++ b/twython/compat.py @@ -22,6 +22,7 @@ if is_py2: str = unicode basestring = basestring + numeric_types = (int, long, float) elif is_py3: @@ -29,3 +30,4 @@ elif is_py3: str = str basestring = (str, bytes) + numeric_types = (int, float) From 13d4725fcad056e1f78ea3c1636720700eb3c17a Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 21:01:05 -0500 Subject: [PATCH 418/687] Update AUTHORS.rst --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 2db7027..e155804 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -39,3 +39,4 @@ Patches and Suggestions - `Paul Solbach `_, fixed requirement for oauth_verifier - `Greg Nofi `_, fixed using built-in Exception attributes for storing & retrieving error message - `Jonathan Vanasco `_, Debugging support, error_code tracking, Twitter error API tracking, other fixes +- `DevDave `_, quick fix for longs with helper._transparent_params From 894e94a4cd666bb32e3b0144a4935b0624e8e020 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 23 May 2013 23:32:32 -0400 Subject: [PATCH 419/687] 2.10.1 - More test coverage! - Fix ``search_gen`` - Fixed ``get_lastfunction_header`` to actually do what its docstring says, returns ``None`` if header is not found - Updated some internal API code, ``__init__`` didn't need to have ``self.auth`` and ``self.headers`` because they were never used anywhere else but the ``__init__`` --- HISTORY.rst | 7 ++++ setup.py | 4 ++- test_twython.py | 78 ++++++++++++++++++++++++++++++++++++++++++--- twython/__init__.py | 2 +- twython/twython.py | 35 ++++++++++---------- 5 files changed, 102 insertions(+), 24 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index df10007..4e3706d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ History ------- +2.10.1 (2013-05-xx) +++++++++++++++++++ +- More test coverage! +- Fix ``search_gen`` +- Fixed ``get_lastfunction_header`` to actually do what its docstring says, returns ``None`` if header is not found +- Updated some internal API code, ``__init__`` didn't need to have ``self.auth`` and ``self.headers`` because they were never used anywhere else but the ``__init__`` + 2.10.0 (2013-05-21) ++++++++++++++++++ - Added ``get_retweeters_ids`` method diff --git a/setup.py b/setup.py index d24a3de..045d9e0 100755 --- a/setup.py +++ b/setup.py @@ -1,10 +1,12 @@ +#!/usr/bin/env python + import os import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '2.10.0' +__version__ = '2.10.1' packages = [ 'twython', diff --git a/test_twython.py b/test_twython.py index 759dcb4..e5e7fa1 100644 --- a/test_twython.py +++ b/test_twython.py @@ -24,6 +24,7 @@ test_list_id = os.environ.get('TEST_LIST_ID', '574') # 574 is @twitter/team class TwythonAuthTestCase(unittest.TestCase): def setUp(self): self.api = Twython(app_key, app_secret) + self.bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET') def test_get_authentication_tokens(self): '''Test getting authentication tokens works''' @@ -31,11 +32,76 @@ class TwythonAuthTestCase(unittest.TestCase): force_login=True, screen_name=screen_name) + def test_get_authentication_tokens_bad_tokens(self): + '''Test getting authentication tokens with bad tokens + raises TwythonAuthError''' + self.assertRaises(TwythonAuthError, self.api.get_authentication_tokens, + callback_url='http://google.com/') + + def test_get_authorized_tokens_bad_tokens(self): + '''Test getting final tokens fails with wrong tokens''' + self.assertRaises(TwythonError, self.api.get_authorized_tokens, + 'BAD_OAUTH_VERIFIER') + class TwythonAPITestCase(unittest.TestCase): def setUp(self): self.api = Twython(app_key, app_secret, - oauth_token, oauth_token_secret) + oauth_token, oauth_token_secret, + headers={'User-Agent': '__twython__ Test'}) + + def test_construct_api_url(self): + '''Test constructing a Twitter API url works as we expect''' + url = 'https://api.twitter.com/1.1/search/tweets.json' + constructed_url = self.api.construct_api_url(url, {'q': '#twitter'}) + self.assertEqual(constructed_url, 'https://api.twitter.com/1.1/search/tweets.json?q=%23twitter') + + def test_shorten_url(self): + '''Test shortening a url works''' + self.api.shorten_url('http://google.com') + + def test_shorten_url_no_shortner(self): + '''Test shortening a url with no shortener provided raises TwythonError''' + self.assertRaises(TwythonError, self.api.shorten_url, + 'http://google.com', '') + + def test_get(self): + '''Test Twython generic GET request works''' + self.api.get('account/verify_credentials') + + def test_post(self): + '''Test Twython generic POST request works, with a full url and + with just an endpoint''' + update_url = 'https://api.twitter.com/1.1/statuses/update.json' + status = self.api.post(update_url, params={'status': 'I love Twython!'}) + self.api.post('statuses/destroy/%s' % status['id_str']) + + def test_get_lastfunction_header(self): + '''Test getting last specific header of the last API call works''' + self.api.get('statuses/home_timeline') + self.api.get_lastfunction_header('x-rate-limit-remaining') + + def test_get_lastfunction_header_not_present(self): + '''Test getting specific header that does not exist from the last call returns None''' + self.api.get('statuses/home_timeline') + header = self.api.get_lastfunction_header('does-not-exist') + self.assertEqual(header, None) + + def test_get_lastfunction_header_no_last_api_call(self): + '''Test attempting to get a header when no API call was made raises a TwythonError''' + self.assertRaises(TwythonError, self.api.get_lastfunction_header, + 'no-api-call-was-made') + + def test_search_gen(self): + '''Test looping through the generator results works, at least once that is''' + search = self.api.search_gen('python') + for result in search: + if result: + break + + def test_encode(self): + '''Test encoding UTF-8 works''' + self.api.encode('Twython is awesome!') # Timelines def test_get_mentions_timeline(self): @@ -84,9 +150,11 @@ class TwythonAPITestCase(unittest.TestCase): def test_retweet_twice(self): '''Test that trying to retweet a tweet twice raises a TwythonError''' - retweet = self.api.retweet(id='99530515043983360') - self.assertRaises(TwythonError, self.api.retweet, - id='99530515043983360') + tweets = self.api.search(q='twitter').get('statuses') + if tweets: + retweet = self.api.retweet(id=tweets[0]['id_str']) + self.assertRaises(TwythonError, self.api.retweet, + id=tweets[0]['id_str']) # Then clean up self.api.destroy_status(id=retweet['id_str']) @@ -132,7 +200,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_get_user_ids_of_blocked_retweets(self): '''Test that collection of user_ids that the authenticated user does not want to receive retweets from succeeds''' - self.api.get_user_ids_of_blocked_retweets(stringify_ids='true') + self.api.get_user_ids_of_blocked_retweets(stringify_ids=True) def test_get_friends_ids(self): '''Test returning ids of users the authenticated user and then a random diff --git a/twython/__init__.py b/twython/__init__.py index 6390bcb..ce1c200 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,7 +18,7 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '2.10.0' +__version__ = '2.10.1' from .twython import Twython from .streaming import TwythonStreamer diff --git a/twython/twython.py b/twython/twython.py index 286afd8..1ac04f8 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -59,27 +59,27 @@ class Twython(object): stacklevel=2 ) - self.headers = {'User-Agent': 'Twython v' + __version__} + req_headers = {'User-Agent': 'Twython v' + __version__} if headers: - self.headers.update(headers) + req_headers.update(headers) # Generate OAuth authentication object for the request - # If no keys/tokens are passed to __init__, self.auth=None allows for + # If no keys/tokens are passed to __init__, auth=None allows for # unauthenticated requests, although I think all v1.1 requests need auth - self.auth = None + auth = None if self.app_key is not None and self.app_secret is not None and \ self.oauth_token is None and self.oauth_token_secret is None: - self.auth = OAuth1(self.app_key, self.app_secret) + auth = OAuth1(self.app_key, self.app_secret) if self.app_key is not None and self.app_secret is not None and \ self.oauth_token is not None and self.oauth_token_secret is not None: - self.auth = OAuth1(self.app_key, self.app_secret, - self.oauth_token, self.oauth_token_secret) + auth = OAuth1(self.app_key, self.app_secret, + self.oauth_token, self.oauth_token_secret) self.client = requests.Session() - self.client.headers = self.headers + self.client.headers = req_headers self.client.proxies = proxies - self.client.auth = self.auth + self.client.auth = auth self.client.verify = ssl_verify # register available funcs to allow listing name when debugging. @@ -208,7 +208,7 @@ class Twython(object): def request(self, endpoint, method='GET', params=None, version='1.1'): # In case they want to pass a full Twitter URL - # i.e. https://search.twitter.com/ + # i.e. https://api.twitter.com/1.1/search/tweets.json if endpoint.startswith('http://') or endpoint.startswith('https://'): url = endpoint else: @@ -241,9 +241,11 @@ class Twython(object): """ if self._last_call is None: raise TwythonError('This function must be called after an API call. It delivers header information.') + if header in self._last_call['headers']: return self._last_call['headers'][header] - return self._last_call + else: + return None def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''): """Returns a dict including an authorization URL (auth_url) to direct a user to @@ -325,7 +327,7 @@ class Twython(object): stacklevel=2 ) - if shortener == '': + if not shortener: raise TwythonError('Please provide a URL shortening service.') request = requests.get(shortener, params={ @@ -336,7 +338,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('shorten_url failed with a %s error code.' % request.status_code) @staticmethod def constructApiURL(base_url, params): @@ -373,17 +375,16 @@ class Twython(object): See Twython.search() for acceptable parameters - e.g search = x.searchGen('python') + e.g search = x.search_gen('python') for result in search: print result """ - kwargs['q'] = search_query content = self.search(q=search_query, **kwargs) - if not content['results']: + if not content.get('statuses'): raise StopIteration - for tweet in content['results']: + for tweet in content['statuses']: yield tweet try: From 815393cc336a87d759b67deda47fabb0a51e43db Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 23 May 2013 23:36:05 -0400 Subject: [PATCH 420/687] Meant to use bad_api for these Tests --- test_twython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_twython.py b/test_twython.py index e5e7fa1..96a5b4c 100644 --- a/test_twython.py +++ b/test_twython.py @@ -35,12 +35,12 @@ class TwythonAuthTestCase(unittest.TestCase): def test_get_authentication_tokens_bad_tokens(self): '''Test getting authentication tokens with bad tokens raises TwythonAuthError''' - self.assertRaises(TwythonAuthError, self.api.get_authentication_tokens, + self.assertRaises(TwythonAuthError, self.bad_api.get_authentication_tokens, callback_url='http://google.com/') def test_get_authorized_tokens_bad_tokens(self): '''Test getting final tokens fails with wrong tokens''' - self.assertRaises(TwythonError, self.api.get_authorized_tokens, + self.assertRaises(TwythonError, self.bad_api.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') From c8b12028808f04f294b9452da6647877c81911e0 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 24 May 2013 15:19:19 -0400 Subject: [PATCH 421/687] Added disconnect to TwythonStreamer, more tests, update example * Stream and Twython core tests * Import TwythonStreamError from twython See more in 2.10.1 section of HISTORY.rst --- .travis.yml | 2 +- HISTORY.rst | 2 ++ examples/stream.py | 2 ++ test_twython.py | 67 ++++++++++++++++++++++++++++++++++---- twython/__init__.py | 5 ++- twython/exceptions.py | 30 ++++++++--------- twython/streaming/api.py | 62 ++++++++++++++++++++--------------- twython/streaming/types.py | 2 +- twython/twython.py | 5 +-- 9 files changed, 122 insertions(+), 55 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6788276..a5992e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ env: - PROTECTED_TWITTER_2=TwythonSecure2 - TEST_TWEET_ID=332992304010899457 - TEST_LIST_ID=574 -script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase --logging-filter="twython" --cover-package="twython" --with-coverage +script: nosetests -v test_twython:TwythonAPITestCase test_twython:TwythonAuthTestCase test_twython:TwythonStreamTestCase --logging-filter="twython" --cover-package="twython" --with-coverage install: pip install -r requirements.txt notifications: email: false diff --git a/HISTORY.rst b/HISTORY.rst index 4e3706d..a56f395 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -9,6 +9,8 @@ History - Fix ``search_gen`` - Fixed ``get_lastfunction_header`` to actually do what its docstring says, returns ``None`` if header is not found - Updated some internal API code, ``__init__`` didn't need to have ``self.auth`` and ``self.headers`` because they were never used anywhere else but the ``__init__`` +- Added ``disconnect`` method to ``TwythonStreamer``, allowing users to disconnect as they desire +- Updated ``TwythonStreamError`` docstring, also allow importing it from ``twython`` 2.10.0 (2013-05-21) ++++++++++++++++++ diff --git a/examples/stream.py b/examples/stream.py index f5c5f1a..0e23a09 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -4,6 +4,8 @@ from twython import TwythonStreamer class MyStreamer(TwythonStreamer): def on_success(self, data): print data + # Want to disconnect after the first result? + # self.disconnect() def on_error(self, status_code, data): print status_code, data diff --git a/test_twython.py b/test_twython.py index 96a5b4c..c867834 100644 --- a/test_twython.py +++ b/test_twython.py @@ -1,7 +1,11 @@ -import unittest -import os +from twython import( + Twython, TwythonStreamer, TwythonError, + TwythonAuthError, TwythonStreamError +) -from twython import Twython, TwythonError, TwythonAuthError +import os +import time +import unittest app_key = os.environ.get('APP_KEY') app_secret = os.environ.get('APP_SECRET') @@ -94,10 +98,17 @@ class TwythonAPITestCase(unittest.TestCase): def test_search_gen(self): '''Test looping through the generator results works, at least once that is''' - search = self.api.search_gen('python') - for result in search: - if result: - break + search = self.api.search_gen('twitter', count=1) + counter = 0 + while counter < 2: + counter += 1 + result = search.next() + new_id_str = int(result['id_str']) + if counter == 1: + prev_id_str = new_id_str + time.sleep(1) # Give time for another tweet to come into search + if counter == 2: + self.assertTrue(new_id_str > prev_id_str) def test_encode(self): '''Test encoding UTF-8 works''' @@ -480,5 +491,47 @@ class TwythonAPITestCase(unittest.TestCase): self.api.get_closest_trends(lat='37', long='-122') +class TwythonStreamTestCase(unittest.TestCase): + def setUp(self): + class MyStreamer(TwythonStreamer): + def on_success(self, data): + self.disconnect() + + def on_error(self, status_code, data): + raise TwythonStreamError(data) + + def on_delete(self, data): + return + + def on_limit(self, data): + return + + def on_disconnect(self, data): + return + + def on_timeout(self, data): + return + + self.api = MyStreamer(app_key, app_secret, + oauth_token, oauth_token_secret) + + def test_stream_status_filter(self): + self.api.statuses.filter(track='twitter') + + def test_stream_status_sample(self): + self.api.statuses.sample() + + def test_stream_status_firehose(self): + self.assertRaises(TwythonStreamError, self.api.statuses.firehose, + track='twitter') + + def test_stream_site(self): + self.assertRaises(TwythonStreamError, self.api.site, + follow='twitter') + + def test_stream_user(self): + self.api.user(track='twitter') + + if __name__ == '__main__': unittest.main() diff --git a/twython/__init__.py b/twython/__init__.py index ce1c200..5befefb 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -22,4 +22,7 @@ __version__ = '2.10.1' from .twython import Twython from .streaming import TwythonStreamer -from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError +from .exceptions import ( + TwythonError, TwythonRateLimitError, TwythonAuthError, + TwythonStreamError +) diff --git a/twython/exceptions.py b/twython/exceptions.py index 265356a..924a882 100644 --- a/twython/exceptions.py +++ b/twython/exceptions.py @@ -2,17 +2,15 @@ from .endpoints import twitter_http_status_codes class TwythonError(Exception): - """ - Generic error class, catch-all for most Twython issues. - Special cases are handled by TwythonAuthError & TwythonRateLimitError. + """Generic error class, catch-all for most Twython issues. + Special cases are handled by TwythonAuthError & TwythonRateLimitError. - Note: Syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: + Note: Syntax has changed as of Twython 1.3. To catch these, + you need to explicitly import them into your code, e.g: - from twython import ( - TwythonError, TwythonRateLimitError, TwythonAuthError - ) - """ + from twython import ( + TwythonError, TwythonRateLimitError, TwythonAuthError + )""" def __init__(self, msg, error_code=None, retry_after=None): self.error_code = error_code @@ -30,18 +28,16 @@ class TwythonError(Exception): 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.""" pass class TwythonRateLimitError(TwythonError): - """ Raised when you've hit a rate limit. + """Raised when you've hit a rate limit. - The amount of seconds to retry your request in will be appended - to the message. - """ + The amount of seconds to retry your request in will be appended + to the message.""" def __init__(self, msg, error_code, retry_after=None): if isinstance(retry_after, int): msg = '%s (Retry after %d seconds)' % (msg, retry_after) @@ -49,5 +45,5 @@ class TwythonRateLimitError(TwythonError): class TwythonStreamError(TwythonError): - """Test""" + """Raised when an invalid response from the Stream API is received""" pass diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 541aa07..1a77ec5 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -55,41 +55,48 @@ class TwythonStreamer(object): self.user = StreamTypes.user self.site = StreamTypes.site + self.connected = False + def _request(self, url, method='GET', params=None): """Internal stream request handling""" + self.connected = True retry_counter = 0 method = method.lower() func = getattr(self.client, method) def _send(retry_counter): - try: - if method == 'get': - response = func(url, params=params, timeout=self.timeout) - else: - response = func(url, data=params, timeout=self.timeout) - except requests.exceptions.Timeout: - self.on_timeout() - else: - if response.status_code != 200: - self.on_error(response.status_code, response.content) - - if self.retry_count and (self.retry_count - retry_counter) > 0: - time.sleep(self.retry_in) - retry_counter += 1 - _send(retry_counter) - - return response - - response = _send(retry_counter) - - for line in response.iter_lines(): - if line: + while self.connected: try: - self.on_success(json.loads(line)) - except ValueError: - raise TwythonStreamError('Response was not valid JSON, \ - unable to decode.') + if method == 'get': + response = func(url, params=params, timeout=self.timeout) + else: + response = func(url, data=params, timeout=self.timeout) + except requests.exceptions.Timeout: + self.on_timeout() + else: + if response.status_code != 200: + self.on_error(response.status_code, response.content) + + if self.retry_count and (self.retry_count - retry_counter) > 0: + time.sleep(self.retry_in) + retry_counter += 1 + _send(retry_counter) + + return response + + while self.connected: + response = _send(retry_counter) + + for line in response.iter_lines(): + if not self.connected: + break + if line: + try: + self.on_success(json.loads(line)) + except ValueError: + raise TwythonStreamError('Response was not valid JSON, \ + unable to decode.') def on_success(self, data): """Called when data has been successfull received from the stream @@ -161,3 +168,6 @@ class TwythonStreamer(object): def on_timeout(self): return + + def disconnect(self): + self.connected = False diff --git a/twython/streaming/types.py b/twython/streaming/types.py index fd02f81..c17cadf 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -61,7 +61,7 @@ class TwythonStreamerTypesStatuses(object): self.streamer._request(url, params=params) def firehose(self, **params): - """Stream statuses/filter + """Stream statuses/firehose Accepted params found at: https://dev.twitter.com/docs/api/1.1/get/statuses/firehose diff --git a/twython/twython.py b/twython/twython.py index 1ac04f8..d196a85 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -388,11 +388,12 @@ class Twython(object): yield tweet try: - kwargs['page'] = 2 if not 'page' in kwargs else (int(kwargs['page']) + 1) + if not 'since_id' in kwargs: + kwargs['since_id'] = (int(content['statuses'][0]['id_str']) + 1) except (TypeError, ValueError): raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - for tweet in self.searchGen(search_query, **kwargs): + for tweet in self.search_gen(search_query, **kwargs): yield tweet @staticmethod From 64b134999371cbb6a07e12597213d331ab2b16a9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 24 May 2013 16:17:50 -0400 Subject: [PATCH 422/687] Python 3 compat --- test_twython.py | 2 +- twython/streaming/api.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test_twython.py b/test_twython.py index c867834..1e706a5 100644 --- a/test_twython.py +++ b/test_twython.py @@ -102,7 +102,7 @@ class TwythonAPITestCase(unittest.TestCase): counter = 0 while counter < 2: counter += 1 - result = search.next() + result = next(search) new_id_str = int(result['id_str']) if counter == 1: prev_id_str = new_id_str diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 1a77ec5..6414020 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -1,5 +1,5 @@ from .. import __version__ -from ..compat import json +from ..compat import json, is_py3 from ..exceptions import TwythonStreamError from .types import TwythonStreamerTypes @@ -93,7 +93,11 @@ class TwythonStreamer(object): break if line: try: - self.on_success(json.loads(line)) + if not is_py3: + self.on_success(json.loads(line)) + else: + line = line.decode('utf-8') + self.on_success(json.loads(line)) except ValueError: raise TwythonStreamError('Response was not valid JSON, \ unable to decode.') From f879094ea196c655c7916359b45331ab81f603e9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 28 May 2013 17:42:41 -0400 Subject: [PATCH 423/687] Update stream example, update AUTHORS for future example fix Remove tests that usually caused Travis to fail Made it clear that Authenticaiton IS required for Streaming in the docstring --- AUTHORS.rst | 2 ++ HISTORY.rst | 1 + examples/stream.py | 3 ++- test_twython.py | 16 ---------------- twython/streaming/api.py | 6 ++++-- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index e155804..461d25d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -40,3 +40,5 @@ Patches and Suggestions - `Greg Nofi `_, fixed using built-in Exception attributes for storing & retrieving error message - `Jonathan Vanasco `_, Debugging support, error_code tracking, Twitter error API tracking, other fixes - `DevDave `_, quick fix for longs with helper._transparent_params +- `Ruben Varela Rosa `_, Fixed search example +>>>>>>> Update stream example, update AUTHORS for future example fix diff --git a/HISTORY.rst b/HISTORY.rst index a56f395..bdeb0fc 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -11,6 +11,7 @@ History - Updated some internal API code, ``__init__`` didn't need to have ``self.auth`` and ``self.headers`` because they were never used anywhere else but the ``__init__`` - Added ``disconnect`` method to ``TwythonStreamer``, allowing users to disconnect as they desire - Updated ``TwythonStreamError`` docstring, also allow importing it from ``twython`` +- No longer raise ``TwythonStreamError`` when stream line can't be decoded. Instead, sends signal to ``TwythonStreamer.on_error`` 2.10.0 (2013-05-21) ++++++++++++++++++ diff --git a/examples/stream.py b/examples/stream.py index 0e23a09..a571711 100644 --- a/examples/stream.py +++ b/examples/stream.py @@ -3,7 +3,8 @@ from twython import TwythonStreamer class MyStreamer(TwythonStreamer): def on_success(self, data): - print data + if 'text' in data: + print data['text'].encode('utf-8') # Want to disconnect after the first result? # self.disconnect() diff --git a/test_twython.py b/test_twython.py index 1e706a5..16598c1 100644 --- a/test_twython.py +++ b/test_twython.py @@ -154,22 +154,6 @@ class TwythonAPITestCase(unittest.TestCase): status = self.api.update_status(status='Test post just to get deleted :(') self.api.destroy_status(id=status['id_str']) - def test_retweet(self): - '''Test retweeting a status succeeds''' - retweet = self.api.retweet(id='99530515043983360') - self.api.destroy_status(id=retweet['id_str']) - - def test_retweet_twice(self): - '''Test that trying to retweet a tweet twice raises a TwythonError''' - tweets = self.api.search(q='twitter').get('statuses') - if tweets: - retweet = self.api.retweet(id=tweets[0]['id_str']) - self.assertRaises(TwythonError, self.api.retweet, - id=tweets[0]['id_str']) - - # Then clean up - self.api.destroy_status(id=retweet['id_str']) - def test_get_oembed_tweet(self): '''Test getting info to embed tweet on Third Party site succeeds''' self.api.get_oembed_tweet(id='99530515043983360') diff --git a/twython/streaming/api.py b/twython/streaming/api.py index 6414020..a246593 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -13,6 +13,7 @@ class TwythonStreamer(object): def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret, timeout=300, retry_count=None, retry_in=10, headers=None): """Streaming class for a friendly streaming user experience + Authentication IS required to use the Twitter Streaming API :param app_key: (required) Your applications key :param app_secret: (required) Your applications secret key @@ -99,8 +100,9 @@ class TwythonStreamer(object): line = line.decode('utf-8') self.on_success(json.loads(line)) except ValueError: - raise TwythonStreamError('Response was not valid JSON, \ - unable to decode.') + self.on_error(response.status_code, 'Unable to decode response, not vaild JSON.') + + response.close() def on_success(self, data): """Called when data has been successfull received from the stream From 71ea58cf6fb01c0030fd0c562e9b9ad5318578dc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 28 May 2013 17:47:03 -0400 Subject: [PATCH 424/687] Send a "different" message everytime for DM tests --- test_twython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_twython.py b/test_twython.py index 16598c1..f5372c7 100644 --- a/test_twython.py +++ b/test_twython.py @@ -180,7 +180,7 @@ class TwythonAPITestCase(unittest.TestCase): def test_send_get_and_destroy_direct_message(self): '''Test sending, getting, then destory a direct message succeeds''' message = self.api.send_direct_message(screen_name=protected_twitter_1, - text='Hey d00d!') + text='Hey d00d! %s' % int(time.time())) self.api.get_direct_message(id=message['id_str']) self.api.destroy_direct_message(id=message['id_str']) From 81a6802c639e8209557a48b99ec905e5712dfaca Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 11:40:14 -0400 Subject: [PATCH 425/687] Update HISTORY [ci skip] --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index bdeb0fc..7ac3c4e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,7 @@ History - Added ``disconnect`` method to ``TwythonStreamer``, allowing users to disconnect as they desire - Updated ``TwythonStreamError`` docstring, also allow importing it from ``twython`` - No longer raise ``TwythonStreamError`` when stream line can't be decoded. Instead, sends signal to ``TwythonStreamer.on_error`` +- Allow for (int, long, float) params to be passed to Twython Twitter API functions in Python 2, and (int, float) in Python 3 2.10.0 (2013-05-21) ++++++++++++++++++ From 14b268e560a77c6eeb5b7351ffffa8be67d58a75 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 11:50:23 -0400 Subject: [PATCH 426/687] README [ci skip] --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7fee952..ecb9d9f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ except TwythonError as e: print e ``` -#### Posting a Status with an Image +##### Posting a Status with an Image ```python from twython import Twython @@ -155,15 +155,14 @@ photo = open('/path/to/file/image.jpg', 'rb') t.update_status_with_media(media=photo, status='Check out my image!') ``` -#### Posting a Status with an Editing Image *(This example resizes an image)* +##### Posting a Status with an Editing Image *(This example resizes an image)* ```python from twython import Twython t = Twython(app_key, app_secret, oauth_token, oauth_token_secret) -# Like I said in the previous section, you can pass any object that has a -# read() method +# Like said in the previous section, you can pass any object that has a read() method # Assume you are working with a JPEG From 8d2dd80ae83b7b20cc8665d337276082834d717e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 13:37:42 -0400 Subject: [PATCH 427/687] Update HISTORY 2.10.1 date [ci skip] --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7ac3c4e..6338d83 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,7 +3,7 @@ History ------- -2.10.1 (2013-05-xx) +2.10.1 (2013-05-29) ++++++++++++++++++ - More test coverage! - Fix ``search_gen`` From 6eaa58cd0b67411646dd9bb90f212faf93f6ceb1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:02:44 -0400 Subject: [PATCH 428/687] Try and fix README.rst [ci skip] --- README.rst | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 2779095..ce1bc9f 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,7 @@ Twython ======= + .. image:: https://travis-ci.org/ryanmcgrath/twython.png?branch=master :target: https://travis-ci.org/ryanmcgrath/twython .. image:: https://pypip.in/d/twython/badge.png @@ -12,16 +13,16 @@ Features -------- * Query data for: - - User information - - Twitter lists - - Timelines - - Direct Messages - - and anything found in `the docs `_ + - User information + - Twitter lists + - Timelines + - Direct Messages + - and anything found in `the docs `_ * Image Uploading! - - **Update user status with an image** - - Change user avatar - - Change user background image - - Change user banner image + - **Update user status with an image** + - Change user avatar + - Change user background image + - Change user banner image * Support for Twitter's Streaming API * Seamless Python 3 support! @@ -31,7 +32,7 @@ Installation pip install twython -... or, you can clone the repo and install it the old fashioned way +or, you can clone the repo and install it the old fashioned way :: @@ -118,6 +119,7 @@ Catching exceptions Dynamic function arguments ~~~~~~~~~~~~~~~~~~~~~~~~~~ + Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. https://dev.twitter.com/docs/api/1.1/post/statuses/update says it takes "status" amongst other arguments @@ -134,11 +136,10 @@ Dynamic function arguments except TwythonError as e: print e -and - https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments - :: + # https://dev.twitter.com/docs/api/1.1/get/search/tweets says it takes "q" and "result_type" amongst other arguments + from twython import Twython, TwythonAuthError t = Twython(app_key, app_secret, @@ -152,6 +153,7 @@ and Posting a Status with an Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + :: from twython import Twython @@ -170,6 +172,7 @@ Posting a Status with an Image Posting a Status with an Editing Image *(This example resizes an image)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + :: from twython import Twython @@ -225,11 +228,13 @@ Streaming API Notes ----- + * Twython (as of 2.7.0) now supports ONLY Twitter v1.1 endpoints! Please see the **[Twitter v1.1 API Documentation](https://dev.twitter.com/docs/api/1.1)** to help migrate your API calls! * As of Twython 2.9.1, all method names conform to PEP8 standards. For backwards compatibility, we internally check and catch any calls made using the old (pre 2.9.1) camelCase method syntax. We will continue to support this for the foreseeable future for all old methods (raising a DeprecationWarning where appropriate), but you should update your code if you have the time. Questions, Comments, etc? ------------------------- + My hope is that Twython is so simple that you'd never *have* to ask any questions, but if you feel the need to contact me for this (or other) reasons, you can hit me up at ryan@venodesigns.net. Or if I'm to busy to answer, feel free to ping mikeh@ydekproductions.com as well. @@ -241,4 +246,5 @@ Follow us on Twitter: Want to help? ------------- + Twython is useful, but ultimately only as useful as the people using it (say that ten times fast!). If you'd like to help, write example code, contribute patches, document things on the wiki, tweet about it. Your help is always appreciated! From 57f8e6b22fe081e5227860b4fdd1d6bcca4ac782 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:04:42 -0400 Subject: [PATCH 429/687] Examples weren't being included in the pkg, and pyc's are excluded by default We'll figure out the example thing for next release! [ci skip] --- MANIFEST.in | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8be3760..b41c376 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1 @@ -include LICENSE README.md README.rst HISTORY.rst test_twython.py requirements.txt -recursive-include examples * -recursive-exclude examples *.pyc +include LICENSE README.md README.rst HISTORY.rst test_twython.py requirements.txt \ No newline at end of file From b0c7b74e3b575fd289304f6ad3889d12b1c84f26 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:05:33 -0400 Subject: [PATCH 430/687] And remove README.md, rst is fine Having both in MANIFEST might mess up reading stuff like on Crate, etc. [ci skip] --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b41c376..536f516 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include LICENSE README.md README.rst HISTORY.rst test_twython.py requirements.txt \ No newline at end of file +include LICENSE README.rst HISTORY.rst test_twython.py requirements.txt \ No newline at end of file From 4327ff30dffb1b17945b08655daf2b16998a239b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 17:48:47 -0400 Subject: [PATCH 431/687] Cleaning up a bit [ci skip] --- HISTORY.rst | 2 ++ MANIFEST.in | 2 +- setup.py | 15 ++++----------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 6338d83..d0b4a83 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,7 @@ History 2.10.1 (2013-05-29) ++++++++++++++++++ + - More test coverage! - Fix ``search_gen`` - Fixed ``get_lastfunction_header`` to actually do what its docstring says, returns ``None`` if header is not found @@ -16,6 +17,7 @@ History 2.10.0 (2013-05-21) ++++++++++++++++++ + - Added ``get_retweeters_ids`` method - Fixed ``TwythonDeprecationWarning`` on camelCase functions if the camelCase was the same as the PEP8 function (i.e. ``Twython.retweet`` did not change) - Fixed error message bubbling when error message returned from Twitter was not an array (i.e. if you try to retweet something twice, the error is not found at index 0) diff --git a/MANIFEST.in b/MANIFEST.in index 536f516..af9021f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include LICENSE README.rst HISTORY.rst test_twython.py requirements.txt \ No newline at end of file +include README.rst HISTORY.rst LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py index 045d9e0..1952b26 100755 --- a/setup.py +++ b/setup.py @@ -18,26 +18,19 @@ if sys.argv[-1] == 'publish': sys.exit() setup( - # Basic package information. name='twython', version=__version__, - packages=packages, - - # Packaging options. - include_package_data=True, - - # Package dependencies. install_requires=['requests==1.2.2', 'requests_oauthlib==0.3.2'], - - # Metadata for PyPI. author='Ryan McGrath', author_email='ryan@venodesigns.net', - license='MIT License', - url='http://github.com/ryanmcgrath/twython/tree/master', + license=open('LICENSE').read(), + url='https://github.com/ryanmcgrath/twython/tree/master', keywords='twitter search api tweet twython stream', description='Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs', long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), + include_package_data=True, + packages=packages, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', From 47e1b7c158d8c933ab2c3fc42efa965d5e393da9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 18:16:39 -0400 Subject: [PATCH 432/687] 3.0.0 ## 3.0.0 - Changed ``twython/twython.py`` to ``twython/api.py`` in attempt to make structure look a little neater - Removed all camelCase function access (anything like ``getHomeTimeline`` is now ``get_home_timeline``) Fixes #199 - Removed ``shorten_url``. With the ``requests`` library, shortening a URL on your own is simple enough - ``twitter_token``, ``twitter_secret`` and ``callback_url`` are no longer passed to ``Twython.__init__`` Fixes #185 - ``twitter_token`` and ``twitter_secret`` have been replaced with ``app_key`` and ``app_secret`` respectively - ``callback_url`` is now passed through ``Twython.get_authentication_tokens`` [ci skip] --- HISTORY.rst | 10 +++ setup.py | 2 +- twython/__init__.py | 4 +- twython/{twython.py => api.py} | 138 +++++---------------------------- 4 files changed, 33 insertions(+), 121 deletions(-) rename twython/{twython.py => api.py} (70%) diff --git a/HISTORY.rst b/HISTORY.rst index d0b4a83..390a447 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,16 @@ History ------- +3.0.0 (2013-xx-xx) +++++++++++++++++++ + +- Changed ``twython/twython.py`` to ``twython/api.py`` in attempt to make structure look a little neater +- Removed all camelCase function access (anything like ``getHomeTimeline`` is now ``get_home_timeline``) +- Removed ``shorten_url``. With the ``requests`` library, shortening a URL on your own is simple enough +- ``twitter_token``, ``twitter_secret`` and ``callback_url`` are no longer passed to ``Twython.__init__`` + - ``twitter_token`` and ``twitter_secret`` have been replaced with ``app_key`` and ``app_secret`` respectively + - ``callback_url`` is now passed through ``Twython.get_authentication_tokens`` + 2.10.1 (2013-05-29) ++++++++++++++++++ diff --git a/setup.py b/setup.py index 1952b26..6fbaf3d 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import sys from setuptools import setup __author__ = 'Ryan McGrath ' -__version__ = '2.10.1' +__version__ = '3.0.0' packages = [ 'twython', diff --git a/twython/__init__.py b/twython/__init__.py index 5befefb..52f4836 100644 --- a/twython/__init__.py +++ b/twython/__init__.py @@ -18,9 +18,9 @@ Questions, comments? ryan@venodesigns.net """ __author__ = 'Ryan McGrath ' -__version__ = '2.10.1' +__version__ = '3.0.0' -from .twython import Twython +from .api import Twython from .streaming import TwythonStreamer from .exceptions import ( TwythonError, TwythonRateLimitError, TwythonAuthError, diff --git a/twython/twython.py b/twython/api.py similarity index 70% rename from twython/twython.py rename to twython/api.py index d196a85..570b83e 100644 --- a/twython/twython.py +++ b/twython/api.py @@ -1,24 +1,19 @@ import re -import warnings import requests from requests_oauthlib import OAuth1 from . import __version__ -from .advisory import TwythonDeprecationWarning from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 from .endpoints import api_table from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params -warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 > - class Twython(object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, headers=None, proxies=None, - version='1.1', callback_url=None, ssl_verify=True, - twitter_token=None, twitter_secret=None): + api_version='1.1', ssl_verify=True): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). :param app_key: (optional) Your applications key @@ -26,39 +21,22 @@ class Twython(object): :param oauth_token: (optional) Used with oauth_token_secret to make authenticated calls :param oauth_token_secret: (optional) Used with oauth_token to make authenticated calls :param headers: (optional) Custom headers to send along with the request - :param callback_url: (optional) If set, will overwrite the callback url set in your application :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. :param ssl_verify: (optional) Turns off ssl verification when False. Useful if you have development server issues. """ # API urls, OAuth urls and API version; needed for hitting that there API. - self.api_version = version + self.api_version = api_version 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.authenticate_url = self.api_url % 'oauth/authenticate' - self.app_key = app_key or twitter_token - self.app_secret = app_secret or twitter_secret + self.app_key = app_key + self.app_secret = app_secret self.oauth_token = oauth_token self.oauth_token_secret = oauth_token_secret - self.callback_url = callback_url - - if twitter_token or twitter_secret: - warnings.warn( - 'Instead of twitter_token or twitter_secret, please use app_key or app_secret (respectively).', - TwythonDeprecationWarning, - stacklevel=2 - ) - - if callback_url: - warnings.warn( - 'Please pass callback_url to the get_authentication_tokens method rather than Twython.__init__', - TwythonDeprecationWarning, - stacklevel=2 - ) - req_headers = {'User-Agent': 'Twython v' + __version__} if headers: req_headers.update(headers) @@ -82,35 +60,21 @@ class Twython(object): self.client.auth = auth self.client.verify = ssl_verify - # register available funcs to allow listing name when debugging. - def setFunc(key, deprecated_key=None): - return lambda **kwargs: self._constructFunc(key, deprecated_key, **kwargs) - for key in api_table.keys(): - self.__dict__[key] = setFunc(key) - - # Allow for old camelCase functions until Twython 3.0.0 - if key == 'get_friend_ids': - deprecated_key = 'getFriendIDs' - elif key == 'get_followers_ids': - deprecated_key = 'getFollowerIDs' - elif key == 'get_incoming_friendship_ids': - deprecated_key = 'getIncomingFriendshipIDs' - elif key == 'get_outgoing_friendship_ids': - deprecated_key = 'getOutgoingFriendshipIDs' - else: - deprecated_key = key.title().replace('_', '') - deprecated_key = deprecated_key[0].lower() + deprecated_key[1:] - - self.__dict__[deprecated_key] = setFunc(key, deprecated_key) - - # create stash for last call intel self._last_call = None + def _setFunc(key): + '''Register functions, attaching them to the Twython instance''' + return lambda **kwargs: self._constructFunc(key, **kwargs) + + # Loop through all our Twitter API endpoints made available in endpoints.py + for key in api_table.keys(): + self.__dict__[key] = _setFunc(key) + def __repr__(self): return '' % (self.app_key) - def _constructFunc(self, api_call, deprecated_key, **kwargs): - # Go through and replace any mustaches that are in our API url. + def _constructFunc(self, api_call, **kwargs): + # Go through and replace any {{mustaches}} that are in our API url. fn = api_table[api_call] url = re.sub( '\{\{(?P[a-zA-Z_]+)\}\}', @@ -118,17 +82,7 @@ class Twython(object): self.api_url % self.api_version + fn['url'] ) - if deprecated_key and (deprecated_key != api_call): - # Until Twython 3.0.0 and the function is removed.. send deprecation warning - warnings.warn( - '`%s` is deprecated, please use `%s` instead.' % (deprecated_key, api_call), - TwythonDeprecationWarning, - stacklevel=2 - ) - - content = self._request(url, method=fn['method'], params=kwargs) - - return content + return self._request(url, method=fn['method'], params=kwargs) def _request(self, url, method='GET', params=None, api_call=None): '''Internal response generator, no sense in repeating the same @@ -156,8 +110,8 @@ class Twython(object): 'content': content, } - # wrap the json loads in a try, and defer an error - # why? twitter will return invalid json with an error code in the headers + # Wrap the json loads in a try, and defer an error + # Twitter will return invalid json with an error code in the headers json_error = False try: try: @@ -292,7 +246,7 @@ class Twython(object): def get_authorized_tokens(self, oauth_verifier): """Returns authorized tokens after they go through the auth_url phase. - :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring + :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring """ response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) @@ -307,48 +261,6 @@ class Twython(object): # but it's not high on the priority list at the moment. # ------------------------------------------------------------------------------------------------------------------------ - @staticmethod - def shortenURL(url_to_shorten, shortener='http://is.gd/create.php'): - return Twython.shorten_url(url_to_shorten, shortener) - - @staticmethod - def shorten_url(url_to_shorten, shortener='http://is.gd/create.php'): - """Shortens url specified by url_to_shorten. - Note: Twitter automatically shortens all URLs behind their own custom t.co shortener now, - but we keep this here for anyone who was previously using it for alternative purposes. ;) - - :param url_to_shorten: (required) The URL to shorten - :param shortener: (optional) In case you want to use a different - URL shortening service - """ - warnings.warn( - 'With requests it\'s easy enough for a developer to implement url shortenting themselves. Please see: https://github.com/ryanmcgrath/twython/issues/184', - TwythonDeprecationWarning, - stacklevel=2 - ) - - if not shortener: - raise TwythonError('Please provide a URL shortening service.') - - request = requests.get(shortener, params={ - 'format': 'json', - 'url': url_to_shorten - }) - - if request.status_code in [301, 201, 200]: - return request.text - else: - raise TwythonError('shorten_url failed with a %s error code.' % request.status_code) - - @staticmethod - def constructApiURL(base_url, params): - warnings.warn( - 'This method is deprecated, please use `Twython.construct_api_url` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return Twython.construct_api_url(base_url, params) - @staticmethod def construct_api_url(base_url, params=None): querystring = [] @@ -360,20 +272,10 @@ class Twython(object): ) return '%s?%s' % (base_url, '&'.join(querystring)) - def searchGen(self, search_query, **kwargs): - warnings.warn( - 'This method is deprecated, please use `search_gen` instead.', - TwythonDeprecationWarning, - stacklevel=2 - ) - return self.search_gen(search_query, **kwargs) - def search_gen(self, 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 - - See Twython.search() for acceptable parameters + Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets e.g search = x.search_gen('python') for result in search: From 9c6fe0d6b8f7addd95a5b9efe9d71df2aefa073d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 18:21:26 -0400 Subject: [PATCH 433/687] Update tests docstring [ci skip] --- HISTORY.rst | 1 + test_twython.py | 224 ++++++++++++++++++++++++------------------------ 2 files changed, 113 insertions(+), 112 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 390a447..9f8bd3e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -12,6 +12,7 @@ History - ``twitter_token``, ``twitter_secret`` and ``callback_url`` are no longer passed to ``Twython.__init__`` - ``twitter_token`` and ``twitter_secret`` have been replaced with ``app_key`` and ``app_secret`` respectively - ``callback_url`` is now passed through ``Twython.get_authentication_tokens`` +- Update ``test_twython.py`` docstrings per http://www.python.org/dev/peps/pep-0257/ 2.10.1 (2013-05-29) ++++++++++++++++++ diff --git a/test_twython.py b/test_twython.py index f5372c7..5b5f876 100644 --- a/test_twython.py +++ b/test_twython.py @@ -31,19 +31,19 @@ class TwythonAuthTestCase(unittest.TestCase): self.bad_api = Twython('BAD_APP_KEY', 'BAD_APP_SECRET') def test_get_authentication_tokens(self): - '''Test getting authentication tokens works''' + """Test getting authentication tokens works""" self.api.get_authentication_tokens(callback_url='http://google.com/', force_login=True, screen_name=screen_name) def test_get_authentication_tokens_bad_tokens(self): - '''Test getting authentication tokens with bad tokens - raises TwythonAuthError''' + """Test getting authentication tokens with bad tokens + raises TwythonAuthError""" self.assertRaises(TwythonAuthError, self.bad_api.get_authentication_tokens, callback_url='http://google.com/') def test_get_authorized_tokens_bad_tokens(self): - '''Test getting final tokens fails with wrong tokens''' + """Test getting final tokens fails with wrong tokens""" self.assertRaises(TwythonError, self.bad_api.get_authorized_tokens, 'BAD_OAUTH_VERIFIER') @@ -55,49 +55,49 @@ class TwythonAPITestCase(unittest.TestCase): headers={'User-Agent': '__twython__ Test'}) def test_construct_api_url(self): - '''Test constructing a Twitter API url works as we expect''' + """Test constructing a Twitter API url works as we expect""" url = 'https://api.twitter.com/1.1/search/tweets.json' constructed_url = self.api.construct_api_url(url, {'q': '#twitter'}) self.assertEqual(constructed_url, 'https://api.twitter.com/1.1/search/tweets.json?q=%23twitter') def test_shorten_url(self): - '''Test shortening a url works''' + """Test shortening a url works""" self.api.shorten_url('http://google.com') def test_shorten_url_no_shortner(self): - '''Test shortening a url with no shortener provided raises TwythonError''' + """Test shortening a url with no shortener provided raises TwythonError""" self.assertRaises(TwythonError, self.api.shorten_url, 'http://google.com', '') def test_get(self): - '''Test Twython generic GET request works''' + """Test Twython generic GET request works""" self.api.get('account/verify_credentials') def test_post(self): - '''Test Twython generic POST request works, with a full url and - with just an endpoint''' + """Test Twython generic POST request works, with a full url and + with just an endpoint""" update_url = 'https://api.twitter.com/1.1/statuses/update.json' status = self.api.post(update_url, params={'status': 'I love Twython!'}) self.api.post('statuses/destroy/%s' % status['id_str']) def test_get_lastfunction_header(self): - '''Test getting last specific header of the last API call works''' + """Test getting last specific header of the last API call works""" self.api.get('statuses/home_timeline') self.api.get_lastfunction_header('x-rate-limit-remaining') def test_get_lastfunction_header_not_present(self): - '''Test getting specific header that does not exist from the last call returns None''' + """Test getting specific header that does not exist from the last call returns None""" self.api.get('statuses/home_timeline') header = self.api.get_lastfunction_header('does-not-exist') self.assertEqual(header, None) def test_get_lastfunction_header_no_last_api_call(self): - '''Test attempting to get a header when no API call was made raises a TwythonError''' + """Test attempting to get a header when no API call was made raises a TwythonError""" self.assertRaises(TwythonError, self.api.get_lastfunction_header, 'no-api-call-was-made') def test_search_gen(self): - '''Test looping through the generator results works, at least once that is''' + """Test looping through the generator results works, at least once that is""" search = self.api.search_gen('twitter', count=1) counter = 0 while counter < 2: @@ -111,74 +111,74 @@ class TwythonAPITestCase(unittest.TestCase): self.assertTrue(new_id_str > prev_id_str) def test_encode(self): - '''Test encoding UTF-8 works''' + """Test encoding UTF-8 works""" self.api.encode('Twython is awesome!') # Timelines def test_get_mentions_timeline(self): - '''Test returning mentions timeline for authenticated user succeeds''' + """Test returning mentions timeline for authenticated user succeeds""" self.api.get_mentions_timeline() def test_get_user_timeline(self): - '''Test returning timeline for authenticated user and random user - succeeds''' + """Test returning timeline for authenticated user and random user + succeeds""" self.api.get_user_timeline() # Authenticated User Timeline self.api.get_user_timeline(screen_name='twitter') # Random User Timeline def test_get_protected_user_timeline_following(self): - '''Test returning a protected user timeline who you are following - succeeds''' + """Test returning a protected user timeline who you are following + succeeds""" self.api.get_user_timeline(screen_name=protected_twitter_1) def test_get_protected_user_timeline_not_following(self): - '''Test returning a protected user timeline who you are not following - fails and raise a TwythonAuthError''' + """Test returning a protected user timeline who you are not following + fails and raise a TwythonAuthError""" self.assertRaises(TwythonAuthError, self.api.get_user_timeline, screen_name=protected_twitter_2) def test_get_home_timeline(self): - '''Test returning home timeline for authenticated user succeeds''' + """Test returning home timeline for authenticated user succeeds""" self.api.get_home_timeline() # Tweets def test_get_retweets(self): - '''Test getting retweets of a specific tweet succeeds''' + """Test getting retweets of a specific tweet succeeds""" self.api.get_retweets(id=test_tweet_id) def test_show_status(self): - '''Test returning a single status details succeeds''' + """Test returning a single status details succeeds""" self.api.show_status(id=test_tweet_id) def test_update_and_destroy_status(self): - '''Test updating and deleting a status succeeds''' + """Test updating and deleting a status succeeds""" status = self.api.update_status(status='Test post just to get deleted :(') self.api.destroy_status(id=status['id_str']) def test_get_oembed_tweet(self): - '''Test getting info to embed tweet on Third Party site succeeds''' + """Test getting info to embed tweet on Third Party site succeeds""" self.api.get_oembed_tweet(id='99530515043983360') def test_get_retweeters_ids(self): - '''Test getting ids for people who retweeted a tweet succeeds''' + """Test getting ids for people who retweeted a tweet succeeds""" self.api.get_retweeters_ids(id='99530515043983360') # Search def test_search(self): - '''Test searching tweets succeeds''' + """Test searching tweets succeeds""" self.api.search(q='twitter') # Direct Messages def test_get_direct_messages(self): - '''Test getting the authenticated users direct messages succeeds''' + """Test getting the authenticated users direct messages succeeds""" self.api.get_direct_messages() def test_get_sent_messages(self): - '''Test getting the authenticated users direct messages they've - sent succeeds''' + """Test getting the authenticated users direct messages they've + sent succeeds""" self.api.get_sent_messages() def test_send_get_and_destroy_direct_message(self): - '''Test sending, getting, then destory a direct message succeeds''' + """Test sending, getting, then destory a direct message succeeds""" message = self.api.send_direct_message(screen_name=protected_twitter_1, text='Hey d00d! %s' % int(time.time())) @@ -186,53 +186,53 @@ class TwythonAPITestCase(unittest.TestCase): self.api.destroy_direct_message(id=message['id_str']) def test_send_direct_message_to_non_follower(self): - '''Test sending a direct message to someone who doesn't follow you - fails''' + """Test sending a direct message to someone who doesn't follow you + fails""" self.assertRaises(TwythonError, self.api.send_direct_message, screen_name=protected_twitter_2, text='Yo, man!') # Friends & Followers def test_get_user_ids_of_blocked_retweets(self): - '''Test that collection of user_ids that the authenticated user does - not want to receive retweets from succeeds''' + """Test that collection of user_ids that the authenticated user does + not want to receive retweets from succeeds""" self.api.get_user_ids_of_blocked_retweets(stringify_ids=True) def test_get_friends_ids(self): - '''Test returning ids of users the authenticated user and then a random - user is following succeeds''' + """Test returning ids of users the authenticated user and then a random + user is following succeeds""" self.api.get_friends_ids() self.api.get_friends_ids(screen_name='twitter') def test_get_followers_ids(self): - '''Test returning ids of users the authenticated user and then a random - user are followed by succeeds''' + """Test returning ids of users the authenticated user and then a random + user are followed by succeeds""" self.api.get_followers_ids() self.api.get_followers_ids(screen_name='twitter') def test_lookup_friendships(self): - '''Test returning relationships of the authenticating user to the + """Test returning relationships of the authenticating user to the comma-separated list of up to 100 screen_names or user_ids provided - succeeds''' + succeeds""" self.api.lookup_friendships(screen_name='twitter,ryanmcgrath') def test_get_incoming_friendship_ids(self): - '''Test returning incoming friendship ids succeeds''' + """Test returning incoming friendship ids succeeds""" self.api.get_incoming_friendship_ids() def test_get_outgoing_friendship_ids(self): - '''Test returning outgoing friendship ids succeeds''' + """Test returning outgoing friendship ids succeeds""" self.api.get_outgoing_friendship_ids() def test_create_friendship(self): - '''Test creating a friendship succeeds''' + """Test creating a friendship succeeds""" self.api.create_friendship(screen_name='justinbieber') def test_destroy_friendship(self): - '''Test destroying a friendship succeeds''' + """Test destroying a friendship succeeds""" self.api.destroy_friendship(screen_name='justinbieber') def test_update_friendship(self): - '''Test updating friendships succeeds''' + """Test updating friendships succeeds""" self.api.update_friendship(screen_name=protected_twitter_1, retweets='true') @@ -240,135 +240,135 @@ class TwythonAPITestCase(unittest.TestCase): retweets='false') def test_show_friendships(self): - '''Test showing specific friendship succeeds''' + """Test showing specific friendship succeeds""" self.api.show_friendship(target_screen_name=protected_twitter_1) def test_get_friends_list(self): - '''Test getting list of users authenticated user then random user is - following succeeds''' + """Test getting list of users authenticated user then random user is + following succeeds""" self.api.get_friends_list() self.api.get_friends_list(screen_name='twitter') def test_get_followers_list(self): - '''Test getting list of users authenticated user then random user are - followed by succeeds''' + """Test getting list of users authenticated user then random user are + followed by succeeds""" self.api.get_followers_list() self.api.get_followers_list(screen_name='twitter') # Users def test_get_account_settings(self): - '''Test getting the authenticated user account settings succeeds''' + """Test getting the authenticated user account settings succeeds""" self.api.get_account_settings() def test_verify_credentials(self): - '''Test representation of the authenticated user call succeeds''' + """Test representation of the authenticated user call succeeds""" self.api.verify_credentials() def test_update_account_settings(self): - '''Test updating a user account settings succeeds''' + """Test updating a user account settings succeeds""" self.api.update_account_settings(lang='en') def test_update_delivery_service(self): - '''Test updating delivery settings fails because we don't have - a mobile number on the account''' + """Test updating delivery settings fails because we don't have + a mobile number on the account""" self.assertRaises(TwythonError, self.api.update_delivery_service, device='none') def test_update_profile(self): - '''Test updating profile succeeds''' + """Test updating profile succeeds""" self.api.update_profile(include_entities='true') def test_update_profile_colors(self): - '''Test updating profile colors succeeds''' + """Test updating profile colors succeeds""" self.api.update_profile_colors(profile_background_color='3D3D3D') def test_list_blocks(self): - '''Test listing users who are blocked by the authenticated user - succeeds''' + """Test listing users who are blocked by the authenticated user + succeeds""" self.api.list_blocks() def test_list_block_ids(self): - '''Test listing user ids who are blocked by the authenticated user - succeeds''' + """Test listing user ids who are blocked by the authenticated user + succeeds""" self.api.list_block_ids() def test_create_block(self): - '''Test blocking a user succeeds''' + """Test blocking a user succeeds""" self.api.create_block(screen_name='justinbieber') def test_destroy_block(self): - '''Test unblocking a user succeeds''' + """Test unblocking a user succeeds""" self.api.destroy_block(screen_name='justinbieber') def test_lookup_user(self): - '''Test listing a number of user objects succeeds''' + """Test listing a number of user objects succeeds""" self.api.lookup_user(screen_name='twitter,justinbieber') def test_show_user(self): - '''Test showing one user works''' + """Test showing one user works""" self.api.show_user(screen_name='twitter') def test_search_users(self): - '''Test that searching for users succeeds''' + """Test that searching for users succeeds""" self.api.search_users(q='Twitter API') def test_get_contributees(self): - '''Test returning list of accounts the specified user can - contribute to succeeds''' + """Test returning list of accounts the specified user can + contribute to succeeds""" self.api.get_contributees(screen_name='TechCrunch') def test_get_contributors(self): - '''Test returning list of accounts that contribute to the - authenticated user fails because we are not a Contributor account''' + """Test returning list of accounts that contribute to the + authenticated user fails because we are not a Contributor account""" self.assertRaises(TwythonError, self.api.get_contributors, screen_name=screen_name) def test_remove_profile_banner(self): - '''Test removing profile banner succeeds''' + """Test removing profile banner succeeds""" self.api.remove_profile_banner() def test_get_profile_banner_sizes(self): - '''Test getting list of profile banner sizes fails because - we have not uploaded a profile banner''' + """Test getting list of profile banner sizes fails because + we have not uploaded a profile banner""" self.assertRaises(TwythonError, self.api.get_profile_banner_sizes) # Suggested Users def test_get_user_suggestions_by_slug(self): - '''Test getting user suggestions by slug succeeds''' + """Test getting user suggestions by slug succeeds""" self.api.get_user_suggestions_by_slug(slug='twitter') def test_get_user_suggestions(self): - '''Test getting user suggestions succeeds''' + """Test getting user suggestions succeeds""" self.api.get_user_suggestions() def test_get_user_suggestions_statuses_by_slug(self): - '''Test getting status of suggested users succeeds''' + """Test getting status of suggested users succeeds""" self.api.get_user_suggestions_statuses_by_slug(slug='funny') # Favorites def test_get_favorites(self): - '''Test getting list of favorites for the authenticated - user succeeds''' + """Test getting list of favorites for the authenticated + user succeeds""" self.api.get_favorites() def test_create_and_destroy_favorite(self): - '''Test creating and destroying a favorite on a tweet succeeds''' + """Test creating and destroying a favorite on a tweet succeeds""" self.api.create_favorite(id=test_tweet_id) self.api.destroy_favorite(id=test_tweet_id) # Lists def test_show_lists(self): - '''Test show lists for specified user''' + """Test show lists for specified user""" self.api.show_lists(screen_name='twitter') def test_get_list_statuses(self): - '''Test timeline of tweets authored by members of the - specified list succeeds''' + """Test timeline of tweets authored by members of the + specified list succeeds""" self.api.get_list_statuses(list_id=test_list_id) def test_create_update_destroy_list_add_remove_list_members(self): - '''Test create a list, adding and removing members then - deleting the list succeeds''' + """Test create a list, adding and removing members then + deleting the list succeeds""" the_list = self.api.create_list(name='Stuff') list_id = the_list['id_str'] @@ -387,15 +387,15 @@ class TwythonAPITestCase(unittest.TestCase): self.api.delete_list(list_id=list_id) def test_get_list_memberships(self): - '''Test list of lists the authenticated user is a member of succeeds''' + """Test list of lists the authenticated user is a member of succeeds""" self.api.get_list_memberships() def test_get_list_subscribers(self): - '''Test list of subscribers of a specific list succeeds''' + """Test list of subscribers of a specific list succeeds""" self.api.get_list_subscribers(list_id=test_list_id) def test_subscribe_is_subbed_and_unsubscribe_to_list(self): - '''Test subscribing, is a list sub and unsubbing to list succeeds''' + """Test subscribing, is a list sub and unsubbing to list succeeds""" self.api.subscribe_to_list(list_id=test_list_id) # Returns 404 if user is not a subscriber self.api.is_list_subscriber(list_id=test_list_id, @@ -403,36 +403,36 @@ class TwythonAPITestCase(unittest.TestCase): self.api.unsubscribe_from_list(list_id=test_list_id) def test_is_list_member(self): - '''Test returning if specified user is member of a list succeeds''' + """Test returning if specified user is member of a list succeeds""" # Returns 404 if not list member self.api.is_list_member(list_id=test_list_id, screen_name='jack') def test_get_list_members(self): - '''Test listing members of the specified list succeeds''' + """Test listing members of the specified list succeeds""" self.api.get_list_members(list_id=test_list_id) def test_get_specific_list(self): - '''Test getting specific list succeeds''' + """Test getting specific list succeeds""" self.api.get_specific_list(list_id=test_list_id) def test_get_list_subscriptions(self): - '''Test collection of the lists the specified user is - subscribed to succeeds''' + """Test collection of the lists the specified user is + subscribed to succeeds""" self.api.get_list_subscriptions(screen_name='twitter') def test_show_owned_lists(self): - '''Test collection of lists the specified user owns succeeds''' + """Test collection of lists the specified user owns succeeds""" self.api.show_owned_lists(screen_name='twitter') # Saved Searches def test_get_saved_searches(self): - '''Test getting list of saved searches for authenticated - user succeeds''' + """Test getting list of saved searches for authenticated + user succeeds""" self.api.get_saved_searches() def test_create_get_destroy_saved_search(self): - '''Test getting list of saved searches for authenticated - user succeeds''' + """Test getting list of saved searches for authenticated + user succeeds""" saved_search = self.api.create_saved_search(query='#Twitter') saved_search_id = saved_search['id_str'] @@ -441,37 +441,37 @@ class TwythonAPITestCase(unittest.TestCase): # Places & Geo def test_get_geo_info(self): - '''Test getting info about a geo location succeeds''' + """Test getting info about a geo location succeeds""" self.api.get_geo_info(place_id='df51dec6f4ee2b2c') def test_reverse_geo_code(self): - '''Test reversing geocode succeeds''' + """Test reversing geocode succeeds""" self.api.reverse_geocode(lat='37.76893497', long='-122.42284884') def test_search_geo(self): - '''Test search for places that can be attached - to a statuses/update succeeds''' + """Test search for places that can be attached + to a statuses/update succeeds""" self.api.search_geo(query='Toronto') def test_get_similar_places(self): - '''Test locates places near the given coordinates which - are similar in name succeeds''' + """Test locates places near the given coordinates which + are similar in name succeeds""" self.api.get_similar_places(lat='37', long='-122', name='Twitter HQ') # Trends def test_get_place_trends(self): - '''Test getting the top 10 trending topics for a specific - WOEID succeeds''' + """Test getting the top 10 trending topics for a specific + WOEID succeeds""" self.api.get_place_trends(id=1) def test_get_available_trends(self): - '''Test returning locations that Twitter has trending - topic information for succeeds''' + """Test returning locations that Twitter has trending + topic information for succeeds""" self.api.get_available_trends() def test_get_closest_trends(self): - '''Test getting the locations that Twitter has trending topic - information for, closest to a specified location succeeds''' + """Test getting the locations that Twitter has trending topic + information for, closest to a specified location succeeds""" self.api.get_closest_trends(lat='37', long='-122') From ff7e3fab9429efcb801acf57c6dc326fa309d7e8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:40:39 -0400 Subject: [PATCH 434/687] Updating a lot of docstrings, EndpointMixin replaces api_table dict [ci skip] --- twython/api.py | 160 +++--- twython/endpoints.py | 1088 ++++++++++++++++++++++++------------ twython/exceptions.py | 21 +- twython/streaming/api.py | 21 +- twython/streaming/types.py | 18 + 5 files changed, 858 insertions(+), 450 deletions(-) diff --git a/twython/api.py b/twython/api.py index 570b83e..a6cff08 100644 --- a/twython/api.py +++ b/twython/api.py @@ -1,28 +1,38 @@ -import re +# -*- coding: utf-8 -*- + +""" +twython.api +~~~~~~~~~~~ + +This module contains functionality for access to core Twitter API calls, +Twitter Authentication, and miscellaneous methods that are useful when +dealing with the Twitter API +""" import requests from requests_oauthlib import OAuth1 from . import __version__ from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2 -from .endpoints import api_table +from .endpoints import EndpointsMixin from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError from .helpers import _transparent_params -class Twython(object): +class Twython(EndpointsMixin, object): def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None, headers=None, proxies=None, api_version='1.1', ssl_verify=True): """Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below). - :param app_key: (optional) Your applications key - :param app_secret: (optional) Your applications secret key - :param oauth_token: (optional) Used with oauth_token_secret to make authenticated calls - :param oauth_token_secret: (optional) Used with oauth_token to make authenticated calls - :param headers: (optional) Custom headers to send along with the request - :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. - :param ssl_verify: (optional) Turns off ssl verification when False. Useful if you have development server issues. + :param app_key: (optional) Your applications key + :param app_secret: (optional) Your applications secret key + :param oauth_token: (optional) Used with oauth_token_secret to make authenticated calls + :param oauth_token_secret: (optional) Used with oauth_token to make authenticated calls + :param headers: (optional) Custom headers to send along with the request + :param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}. + :param ssl_verify: (optional) Turns off ssl verification when False. Useful if you have development server issues. + """ # API urls, OAuth urls and API version; needed for hitting that there API. @@ -62,32 +72,11 @@ class Twython(object): self._last_call = None - def _setFunc(key): - '''Register functions, attaching them to the Twython instance''' - return lambda **kwargs: self._constructFunc(key, **kwargs) - - # Loop through all our Twitter API endpoints made available in endpoints.py - for key in api_table.keys(): - self.__dict__[key] = _setFunc(key) - def __repr__(self): return '' % (self.app_key) - def _constructFunc(self, api_call, **kwargs): - # Go through and replace any {{mustaches}} that are in our API url. - fn = api_table[api_call] - url = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1)), - self.api_url % self.api_version + fn['url'] - ) - - return self._request(url, method=fn['method'], params=kwargs) - def _request(self, url, method='GET', params=None, api_call=None): - '''Internal response generator, no sense in repeating the same - code twice, right? ;) - ''' + """Internal request method""" method = method.lower() params = params or {} @@ -153,14 +142,21 @@ class Twython(object): return content - ''' - # 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, version='1.1'): + """Return dict of response received from Twitter's API + + :param endpoint: (required) Full url or Twitter API endpoint (e.g. search/tweets) + :type endpoint: string + :param method: (optional) Method of accessing data, either GET or POST. (default GET) + :type method: string + :param params: (optional) Dict of parameters (if any) accepted the by Twitter API endpoint you are trying to access (default None) + :type params: dict or None + :param version: (optional) Twitter API version to access (default 1.1) + :type version: string + + :rtype: dict + """ + # In case they want to pass a full Twitter URL # i.e. https://api.twitter.com/1.1/search/tweets.json if endpoint.startswith('http://') or endpoint.startswith('https://'): @@ -173,25 +169,25 @@ class Twython(object): return content def get(self, endpoint, params=None, version='1.1'): + """Shortcut for GET requests via :class:`request`""" return self.request(endpoint, params=params, version=version) def post(self, endpoint, params=None, version='1.1'): + """Shortcut for POST requests via :class:`request`""" return self.request(endpoint, 'POST', params=params, version=version) - # End Dynamic Request Methods - def get_lastfunction_header(self, header): - """Returns the header in the last function - This must be called after an API call, as it returns header based - information. + """Returns a specific header from the last API call + This will return None if the header is not present - This will return None if the header is not present + :param header: (required) The name of the header you want to get the value of + + Most useful for the following header information: + x-rate-limit-limit, + x-rate-limit-remaining, + x-rate-limit-class, + x-rate-limit-reset - Most useful for the following header information: - x-rate-limit-limit - x-rate-limit-remaining - x-rate-limit-class - x-rate-limit-reset """ if self._last_call is None: raise TwythonError('This function must be called after an API call. It delivers header information.') @@ -202,11 +198,12 @@ class Twython(object): return None def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''): - """Returns a dict including an authorization URL (auth_url) to direct a user to + """Returns a dict including an authorization URL, ``auth_url``, to direct a user to - :param callback_url: (optional) Url the user is returned to after they authorize your app (web clients only) - :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. - :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value + :param callback_url: (optional) Url the user is returned to after they authorize your app (web clients only) + :param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized. + :param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value + :rtype: dict """ callback_url = callback_url or self.callback_url request_args = {} @@ -244,9 +241,11 @@ class Twython(object): return request_tokens def get_authorized_tokens(self, oauth_verifier): - """Returns authorized tokens after they go through the auth_url phase. + """Returns a dict of authorized tokens after they go through the :class:`get_authentication_tokens` phase. + + :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring + :rtype: dict - :param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN for non web apps) retrieved from the callback url querystring """ response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier}) authorized_tokens = dict(parse_qsl(response.content.decode('utf-8'))) @@ -262,7 +261,24 @@ class Twython(object): # ------------------------------------------------------------------------------------------------------------------------ @staticmethod - def construct_api_url(base_url, params=None): + def construct_api_url(api_url, **params): + """Construct a Twitter API url, encoded, with parameters + + :param api_url: URL of the Twitter API endpoint you are attempting to construct + :param \*\*params: Parameters that are accepted by Twitter for the endpoint you're requesting + :rtype: string + + Usage:: + + >>> from twython import Twython + >>> twitter = Twython() + + >>> api_url = 'https://api.twitter.com/1.1/search/tweets.json' + >>> constructed_url = twitter.construct_api_url(api_url, q='python', result_type='popular') + >>> print constructed_url + https://api.twitter.com/1.1/search/tweets.json?q=python&result_type=popular + + """ querystring = [] params, _ = _transparent_params(params or {}) params = requests.utils.to_key_val_list(params) @@ -270,18 +286,28 @@ class Twython(object): querystring.append( '%s=%s' % (Twython.encode(k), quote_plus(Twython.encode(v))) ) - return '%s?%s' % (base_url, '&'.join(querystring)) + return '%s?%s' % (api_url, '&'.join(querystring)) - def search_gen(self, search_query, **kwargs): + def search_gen(self, search_query, **params): """Returns a generator of tweets that match a specified query. - Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets + Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets + + :param search_query: Query you intend to search Twitter for + :param \*\*params: Extra parameters to send with your search request + :rtype: generator + + Usage:: + + >>> from twython import Twython + >>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + >>> search = twitter.search_gen('python') + >>> for result in search: + >>> print result - e.g search = x.search_gen('python') - for result in search: - print result """ - content = self.search(q=search_query, **kwargs) + content = self.search(q=search_query, **params) if not content.get('statuses'): raise StopIteration @@ -290,12 +316,12 @@ class Twython(object): yield tweet try: - if not 'since_id' in kwargs: - kwargs['since_id'] = (int(content['statuses'][0]['id_str']) + 1) + if not 'since_id' in params: + params['since_id'] = (int(content['statuses'][0]['id_str']) + 1) except (TypeError, ValueError): raise TwythonError('Unable to generate next page of search results, `page` is not a number.') - for tweet in self.search_gen(search_query, **kwargs): + for tweet in self.search_gen(search_query, **params): yield tweet @staticmethod diff --git a/twython/endpoints.py b/twython/endpoints.py index 1246a3d..41151d4 100644 --- a/twython/endpoints.py +++ b/twython/endpoints.py @@ -1,415 +1,773 @@ +# -*- coding: utf-8 -*- + """ -A huge map of every Twitter API endpoint to a function definition in Twython. +twython.endpoints +~~~~~~~~~~~~~~~~~ -Parameters that need to be embedded in the URL are treated with mustaches, e.g: +This module provides a mixin for a :class:`Twython ` instance. +Parameters that need to be embedded in the API url just need to be passed as a keyword argument. -{{version}}, etc - -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. - -i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced -with 47, instead of defaulting to 1.1 (said defaulting takes place at conversion time). +e.g. Twython.retweet(id=12345) This map is organized the order functions are documented at: https://dev.twitter.com/docs/api/1.1 """ -api_table = { - # Timelines - 'get_mentions_timeline': { - 'url': '/statuses/mentions_timeline.json', - 'method': 'GET', - }, - 'get_user_timeline': { - 'url': '/statuses/user_timeline.json', - 'method': 'GET', - }, - 'get_home_timeline': { - 'url': '/statuses/home_timeline.json', - 'method': 'GET', - }, - 'retweeted_of_me': { - 'url': '/statuses/retweets_of_me.json', - 'method': 'GET', - }, +class EndpointsMixin(object): + # Timelines + def get_mentions_timeline(self, **params): + """Returns the 20 most recent mentions (tweets containing a users's + @screen_name) for the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/mentions_timeline + + """ + return self.get('statuses/mentions_timeline', params=params) + + def get_user_timeline(self, **params): + """Returns a collection of the most recent Tweets posted by the user + indicated by the screen_name or user_id parameters. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline + + """ + return self.get('statuses/user_timeline', params=params) + + def get_home_timline(self, **params): + """Returns a collection of the most recent Tweets and retweets + posted by the authenticating user and the users they follow. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline + + """ + return self.get('statuses/home_timline', params=params) + + def retweeted_of_me(self, **params): + """Returns the most recent tweets authored by the authenticating user + that have been retweeted by others. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets_of_me + + """ + return self.get('statuses/retweets_of_me', params=params) # Tweets - 'get_retweets': { - 'url': '/statuses/retweets/{{id}}.json', - 'method': 'GET', - }, - 'show_status': { - 'url': '/statuses/show/{{id}}.json', - 'method': 'GET', - }, - 'destroy_status': { - 'url': '/statuses/destroy/{{id}}.json', - 'method': 'POST', - }, - 'update_status': { - 'url': '/statuses/update.json', - 'method': 'POST', - }, - 'retweet': { - 'url': '/statuses/retweet/{{id}}.json', - 'method': 'POST', - }, - 'update_status_with_media': { - 'url': '/statuses/update_with_media.json', - 'method': 'POST', - }, - 'get_oembed_tweet': { - 'url': '/statuses/oembed.json', - 'method': 'GET', - }, - 'get_retweeters_ids': { - 'url': '/statuses/retweeters/ids.json', - 'method': 'GET', - }, + def get_retweets(self, **params): + """Returns up to 100 of the first retweets of a given tweet. + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweets/%3Aid + + """ + if not 'id' in params: + raise TwythonError('Parameter "id" is required') + return self.get('statuses/retweets/%s' % params['id'], params=params) + + def show_status(self, **params): + """Returns a single Tweet, specified by the id parameter + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + + """ + if not 'id' in params: + raise TwythonError('Parameter "id" is required') + return self.get('statuses/show/%s' % params['id'], params=params) + + def destroy_status(self, **params): + """Destroys the status specified by the required ID parameter + + Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/destroy/%3Aid + + """ + if not 'id' in params: + raise TwythonError('Parameter "id" is required') + return self.post('statuses/destroy/%s' % params['id']) + + def update_status(self, **params): + """Updates the authenticating user's current status, also known as tweeting + + Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/update + + """ + return self.post('statuses/update_status', params=params) + + def retweet(self, **params): + """Retweets a tweet specified by the id parameter + + Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/retweet/%3Aid + + """ + if not 'id' in params: + raise TwythonError('Parameter "id" is required') + return self.post('statuses/retweet/%s' % params['id']) + + def update_status_with_media(self, **params): + """Updates the authenticating user's current status and attaches media + for upload. In other words, it creates a Tweet with a picture attached. + + Docs: https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media + + """ + return self.post('statuses/update_with_media', params=params) + + def get_oembed_tweet(self, **params): + """Returns information allowing the creation of an embedded + representation of a Tweet on third party sites. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/oembed + + """ + return self.get('statuses/oembed', params=params) + + def get_retweeters_id(self, **params): + """Returns a collection of up to 100 user IDs belonging to users who + have retweeted the tweet specified by the id parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/get/statuses/retweeters/ids + + """ + return self.get('statuses/retweeters/ids', params=params) # Search - 'search': { - 'url': '/search/tweets.json', - 'method': 'GET', - }, + def search(self, **params): + """Returns a collection of relevant Tweets matching a specified query. + Docs: https://dev.twitter.com/docs/api/1.1/get/search/tweets + + """ + return self.get('search/tweets', params=params) # Direct Messages - 'get_direct_messages': { - 'url': '/direct_messages.json', - 'method': 'GET', - }, - 'get_sent_messages': { - 'url': '/direct_messages/sent.json', - 'method': 'GET', - }, - 'get_direct_message': { - 'url': '/direct_messages/show.json', - 'method': 'GET', - }, - 'destroy_direct_message': { - 'url': '/direct_messages/destroy.json', - 'method': 'POST', - }, - 'send_direct_message': { - 'url': '/direct_messages/new.json', - 'method': 'POST', - }, + def get_direct_messages(self, **params): + """Returns the 20 most recent direct messages sent to the authenticating user. + Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages + + """ + return self.get('direct_messages', params=params) + + def get_sent_messages(self, **params): + """Returns the 20 most recent direct messages sent by the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/sent + + """ + return self.get('direct_messages/sent', params=params) + + def get_direct_message(self, **params): + """Returns a single direct message, specified by an id parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/get/direct_messages/show + + """ + return self.get('direct_messages/show', params=params) + + def destroy_direct_message(self, **params): + """Destroys the direct message specified in the required id parameter + + Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/destroy + + """ + return self.post('direct_messages/destroy', params=params) + + def send_direct_message(self, **params): + """Sends a new direct message to the specified user from the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/direct_messages/new + + """ + return self.post('direct_messages/new', params=params) # Friends & Followers - 'get_user_ids_of_blocked_retweets': { - 'url': '/friendships/no_retweets/ids.json', - 'method': 'GET', - }, - 'get_friends_ids': { - 'url': '/friends/ids.json', - 'method': 'GET', - }, - 'get_followers_ids': { - 'url': '/followers/ids.json', - 'method': 'GET', - }, - 'lookup_friendships': { - 'url': '/friendships/lookup.json', - 'method': 'GET', - }, - 'get_incoming_friendship_ids': { - 'url': '/friendships/incoming.json', - 'method': 'GET', - }, - 'get_outgoing_friendship_ids': { - 'url': '/friendships/outgoing.json', - 'method': 'GET', - }, - 'create_friendship': { - 'url': '/friendships/create.json', - 'method': 'POST', - }, - 'destroy_friendship': { - 'url': '/friendships/destroy.json', - 'method': 'POST', - }, - 'update_friendship': { - 'url': '/friendships/update.json', - 'method': 'POST', - }, - 'show_friendship': { - 'url': '/friendships/show.json', - 'method': 'GET', - }, - 'get_friends_list': { - 'url': '/friends/list.json', - 'method': 'GET', - }, - 'get_followers_list': { - 'url': '/followers/list.json', - 'method': 'GET', - }, + def get_user_ids_of_blocked_retweets(self, **params): + """Returns a collection of user_ids that the currently authenticated + user does not want to receive retweets from. + Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/no_retweets/ids + + """ + return self.get('friendships/no_retweets/ids', params=params) + + def get_friends_ids(self, **params): + """Returns a cursored collection of user IDs for every user the + specified user is following (otherwise known as their "friends"). + + Docs: https://dev.twitter.com/docs/api/1.1/get/friends/ids + + """ + return self.get('friends/ids', params=params) + + def get_followers_ids(self, **params): + """Returns a cursored collection of user IDs for every user + following the specified user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/followers/ids + + """ + return self.get('followers/ids', params=params) + + def lookup_friendships(self, **params): + """Returns the relationships of the authenticating user to the + comma-separated list of up to 100 screen_names or user_ids provided. + + Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/lookup + + """ + return self.get('friendships/lookup', params=params) + + def get_incoming_friendship_ids(self, **params): + """Returns a collection of numeric IDs for every user who has a + pending request to follow the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/incoming + + """ + return self.get('friendships/incoming', params=params) + + def get_outgoing_friendship_ids(self, **params): + """Returns a collection of numeric IDs for every protected user for + whom the authenticating user has a pending follow request. + + Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/outgoing + + """ + return self.get('friendships/outgoing', params=params) + + def create_friendship(self, **params): + """Allows the authenticating users to follow the user specified + in the ID parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/create + + """ + return self.post('friendships/create', params=params) + + def destroy_friendship(self, **params): + """Allows the authenticating user to unfollow the user specified + in the ID parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/destroy + + """ + return self.post('friendships/destroy', params=params) + + def update_friendship(self, **params): + """Allows one to enable or disable retweets and device notifications + from the specified user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/friendships/update + + """ + return self.post('friendships/update', params=params) + + def show_friendship(self, **params): + """Returns detailed information about the relationship between two + arbitrary users. + + Docs: https://dev.twitter.com/docs/api/1.1/get/friendships/show + + """ + return self.get('friendships/show', params=params) + + def get_friends_list(self, **params): + """Returns a cursored collection of user objects for every user the + specified user is following (otherwise known as their "friends"). + + Docs: https://dev.twitter.com/docs/api/1.1/get/friends/list + + """ + return self.get('friends/list', params=params) + + def get_followers_list(self, **params): + """Returns a cursored collection of user objects for users + following the specified user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/followers/list + + """ + return self.get('followers/list', params=params) # Users - 'get_account_settings': { - 'url': '/account/settings.json', - 'method': 'GET', - }, - 'verify_credentials': { - 'url': '/account/verify_credentials.json', - 'method': 'GET', - }, - 'update_account_settings': { - 'url': '/account/settings.json', - 'method': 'POST', - }, - 'update_delivery_service': { - 'url': '/account/update_delivery_device.json', - 'method': 'POST', - }, - 'update_profile': { - 'url': '/account/update_profile.json', - 'method': 'POST', - }, - 'update_profile_background_image': { - 'url': '/account/update_profile_banner.json', - 'method': 'POST', - }, - 'update_profile_colors': { - 'url': '/account/update_profile_colors.json', - 'method': 'POST', - }, - 'update_profile_image': { - 'url': '/account/update_profile_image.json', - 'method': 'POST', - }, - 'list_blocks': { - 'url': '/blocks/list.json', - 'method': 'GET', - }, - 'list_block_ids': { - 'url': '/blocks/ids.json', - 'method': 'GET', - }, - 'create_block': { - 'url': '/blocks/create.json', - 'method': 'POST', - }, - 'destroy_block': { - 'url': '/blocks/destroy.json', - 'method': 'POST', - }, - 'lookup_user': { - 'url': '/users/lookup.json', - 'method': 'GET', - }, - 'show_user': { - 'url': '/users/show.json', - 'method': 'GET', - }, - 'search_users': { - 'url': '/users/search.json', - 'method': 'GET', - }, - 'get_contributees': { - 'url': '/users/contributees.json', - 'method': 'GET', - }, - 'get_contributors': { - 'url': '/users/contributors.json', - 'method': 'GET', - }, - 'remove_profile_banner': { - 'url': '/account/remove_profile_banner.json', - 'method': 'POST', - }, - 'update_profile_background_image': { - 'url': '/account/update_profile_background_image.json', - 'method': 'POST', - }, - 'get_profile_banner_sizes': { - 'url': '/users/profile_banner.json', - 'method': 'GET', - }, + def get_account_settings(self, **params): + """Returns settings (including current trend, geo and sleep time + information) for the authenticating user. + Docs: https://dev.twitter.com/docs/api/1.1/get/account/settings + + """ + return self.get('account/settings', params=params) + + def verify_Credentials(self, **params): + """Returns an HTTP 200 OK response code and a representation of the + requesting user if authentication was successful; returns a 401 status + code and an error message if not. + + Docs: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials + + """ + return self.get('account/verify_credentials', params=params) + + def update_account_settings(self, **params): + """Updates the authenticating user's settings. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/settings + + """ + return self.post('account/settings', params=params) + + def update_delivery_service(self, **params): + """Sets which device Twitter delivers updates to for the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_delivery_device + + """ + return self.post('account/update_delivery_device', params=params) + + def update_profile(self, **params): + """Sets values that users are able to set under the "Account" tab of their settings page. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile + + """ + return self.post('account/update_profile', params=params) + + def update_profile_background_image(self, **params): + """Updates the authenticating user's profile background image. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_background_image + + """ + return self.post('account/update_profile_banner', params=params) + + def update_profile_colors(self, **params): + """Sets one or more hex values that control the color scheme of the + authenticating user's profile page on twitter.com. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_colors + + """ + return self.post('account/update_profile_colors', params=params) + + def update_profile_image(self, **params): + """Updates the authenticating user's profile image. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_image + + """ + return self.post('account/update_profile_image', params=params) + + def list_blocks(self, **params): + """Returns a collection of user objects that the authenticating user is blocking. + + Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/list + + """ + return self.get('blocks/list', params=params) + + def list_block_ids(self, **params): + """Returns an array of numeric user ids the authenticating user is blocking. + + Docs: https://dev.twitter.com/docs/api/1.1/get/blocks/ids + + """ + return self.get('blocks/ids', params=params) + + def create_block(self, **params): + """Blocks the specified user from following the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/create + + """ + return self.post('blocks/create', params=params) + + def destroy_block(self, **params): + """Un-blocks the user specified in the ID parameter for the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/blocks/destroy + + """ + return self.post('blocks/destroy', params=params) + + def lookup_user(self, **params): + """Returns fully-hydrated user objects for up to 100 users per request, + as specified by comma-separated values passed to the user_id and/or screen_name parameters. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/lookup + + """ + return self.get('users/lookup', params=params) + + def show_user(self, **params): + """Returns a variety of information about the user specified by the + required user_id or screen_name parameter. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/show + + """ + return self.get('users/show', params=params) + + def search_users(self, **params): + """Provides a simple, relevance-based search interface to public user accounts on Twitter. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/search + + """ + return self.get('users/search', params=params) + + def get_contributees(self, **params): + """Returns a collection of users that the specified user can "contribute" to. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/contributees + + """ + return self.get('users/contributees', params=params) + + def get_contributors(self, **params): + """Returns a collection of users who can contribute to the specified account. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/contributors + + """ + return self.get('users/contributors', params=params) + + def remove_profile_banner(self, **params): + """Removes the uploaded profile banner for the authenticating user. + Returns HTTP 200 upon success. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/remove_profile_banner + + """ + return self.post('account/remove_profile_banner', params=params) + + def update_profile_Background_image(self, **params): + """Uploads a profile banner on behalf of the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner + + """ + return self.post('ccount/update_profile_background_image', params=params) + + def get_profile_banner_sizes(self, **params): + """Returns a map of the available size variations of the specified user's profile banner. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/profile_banner + + """ + return self.get('users/profile_banner', params=params) # Suggested Users - 'get_user_suggestions_by_slug': { - 'url': '/users/suggestions/{{slug}}.json', - 'method': 'GET', - }, - 'get_user_suggestions': { - 'url': '/users/suggestions.json', - 'method': 'GET', - }, - 'get_user_suggestions_statuses_by_slug': { - 'url': '/users/suggestions/{{slug}}/members.json', - 'method': 'GET', - }, + def get_user_suggestions_by_slug(self, **params): + """Access the users in a given category of the Twitter suggested user list. + Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug + + """ + return self.get('users/suggestions/%s' % params['slug'], params=params) + + def get_user_suggestions(self, **params): + """Access to Twitter's suggested user list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions + + """ + return self.get('users/suggestions', params=params) + + def get_user_suggestions_statuses_by_slug(self, **params): + """Access the users in a given category of the Twitter suggested user + list and return their most recent status if they are not a protected user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/users/suggestions/%3Aslug/members + + """ + return self.get('users/suggestions/%s/members' % param['slug'], params=params) # Favorites - 'get_favorites': { - 'url': '/favorites/list.json', - 'method': 'GET', - }, - 'destroy_favorite': { - 'url': '/favorites/destroy.json', - 'method': 'POST', - }, - 'create_favorite': { - 'url': '/favorites/create.json', - 'method': 'POST', - }, + def get_favorites(self, **params): + """Returns the 20 most recent Tweets favorited by the authenticating or specified user. + Docs: https://dev.twitter.com/docs/api/1.1/get/favorites/list + + """ + return self.get('favorites/list', params=params) + + def destroy_favorite(self, **params): + """Un-favorites the status specified in the ID parameter as the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/destroy + + """ + return self.post('favorites/destroy', params=params) + + def create_favorite(self, **params): + """Favorites the status specified in the ID parameter as the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/favorites/create + + """ + return self.post('favorites/create', params=params) # Lists - 'show_lists': { - 'url': '/lists/list.json', - 'method': 'GET', - }, - 'get_list_statuses': { - 'url': '/lists/statuses.json', - 'method': 'GET' - }, - 'delete_list_member': { - 'url': '/lists/members/destroy.json', - 'method': 'POST', - }, - 'get_list_memberships': { - 'url': '/lists/memberships.json', - 'method': 'GET', - }, - 'get_list_subscribers': { - 'url': '/lists/subscribers.json', - 'method': 'GET', - }, - 'subscribe_to_list': { - 'url': '/lists/subscribers/create.json', - 'method': 'POST', - }, - 'is_list_subscriber': { - 'url': '/lists/subscribers/show.json', - 'method': 'GET', - }, - 'unsubscribe_from_list': { - 'url': '/lists/subscribers/destroy.json', - 'method': 'POST', - }, - 'create_list_members': { - 'url': '/lists/members/create_all.json', - 'method': 'POST' - }, - 'is_list_member': { - 'url': '/lists/members/show.json', - 'method': 'GET', - }, - 'get_list_members': { - 'url': '/lists/members.json', - 'method': 'GET', - }, - 'add_list_member': { - 'url': '/lists/members/create.json', - 'method': 'POST', - }, - 'delete_list': { - 'url': '/lists/destroy.json', - 'method': 'POST', - }, - 'update_list': { - 'url': '/lists/update.json', - 'method': 'POST', - }, - 'create_list': { - 'url': '/lists/create.json', - 'method': 'POST', - }, - 'get_specific_list': { - 'url': '/lists/show.json', - 'method': 'GET', - }, - 'get_list_subscriptions': { - 'url': '/lists/subscriptions.json', - 'method': 'GET', - }, - 'delete_list_members': { - 'url': '/lists/members/destroy_all.json', - 'method': 'POST' - }, - 'show_owned_lists': { - 'url': '/lists/ownerships.json', - 'method': 'GET' - }, + def show_lists(self, **params): + """Returns all lists the authenticating or specified user subscribes to, including their own. + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/list + + """ + return self.get('lists/list', params=params) + + def get_list_statuses(self, **params): + """Returns a timeline of tweets authored by members of the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/statuses + + """ + return self.get('lists/statuses', params=params) + + def delete_list_member(self, **params): + """Removes the specified member from the list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy + + """ + return self.post('lists/members/destroy', params=params) + + + def get_list_subscribers(self, **params): + """Returns the subscribers of the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers + + """ + return self.get('lists/subscribers', params=params) + + def subscribe_to_list(self, **params): + """Subscribes the authenticated user to the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/create + + """ + return self.post('lists/subscribers/create', params=params) + + def is_list_subscriber(self, **params): + """Check if the specified user is a subscriber of the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscribers/show + + """ + return self.get('lists/subscribers/show', params=params) + + def unsubscribe_from_list(self, **params): + """Unsubscribes the authenticated user from the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/subscribers/destroy + + """ + return self.get('lists/subscribers/destroy', params=params) + + def create_list_members(self, **params): + """Adds multiple members to a list, by specifying a comma-separated + list of member ids or screen names. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/create_all + + """ + return self.post('lists/members/create_all', params=params) + + def is_list_member(self, **params): + """Check if the specified user is a member of the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members/show + + """ + return self.get('lists/members/show', params=params) + + def get_list_members(self, **params): + """Returns the members of the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/members + + """ + return self.get('lists/members', params=params) + + def add_list_member(self, **params): + """Add a member to a list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/create + + """ + return self.post('lists/members/create', params=params) + + def delete_list(self, **params): + """Deletes the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/destroy + + """ + return self.post('lists/destroy', params=params) + + def update_list(self, **params): + """Updates the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/update + + """ + return self.post('lists/update', params=params) + + def create_list(self, **params): + """Creates a new list for the authenticated user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/create + + """ + return self.post('lists/create', params=params) + + def get_specific_list(self, **params): + """Returns the specified list. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/show + + """ + return self.get('lists/show', params=params) + + def get_list_subscriptions(self, **params): + """Obtain a collection of the lists the specified user is subscribed to. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/subscriptions + + """ + return self.get('lists/subscriptions', params=params) + + def delete_list_members(self, **params): + """Removes multiple members from a list, by specifying a + comma-separated list of member ids or screen names. + + Docs: https://dev.twitter.com/docs/api/1.1/post/lists/members/destroy_all + + """ + return self.post('lists/members/destroy_all', params=params) + + def show_owned_lists(self, **params): + """Returns the lists owned by the specified Twitter user. + + Docs: https://dev.twitter.com/docs/api/1.1/get/lists/ownerships + + """ + return self.get('lists/ownerships', params=params) # Saved Searches - 'get_saved_searches': { - 'url': '/saved_searches/list.json', - 'method': 'GET', - }, - 'show_saved_search': { - 'url': '/saved_searches/show/{{id}}.json', - 'method': 'GET', - }, - 'create_saved_search': { - 'url': '/saved_searches/create.json', - 'method': 'POST', - }, - 'destroy_saved_search': { - 'url': '/saved_searches/destroy/{{id}}.json', - 'method': 'POST', - }, + def get_saved_searches(self, **params): + """Returns the authenticated user's saved search queries. + Docs: https://dev.twitter.com/docs/api/1.1/get/saved_searches/list + + """ + return self.get('saved_searches/list', params=params) + + def show_saved_search(self, **params): + """Retrieve the information for the saved search represented by the given id. + + Docs: https://dev.twitter.com/docs/api/1.1/get/saved_searches/show/%3Aid + + """ + return self.get('saved_searches/show/%s' % params['id'], params=params) + + def create_saved_search(self, **params): + """Create a new saved search for the authenticated user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/saved_searches/create + + """ + return self.post('saved_searches/create', params=params) + + def destroy_saved_search(self, **params): + """Destroys a saved search for the authenticating user. + + Docs: https://dev.twitter.com/docs/api/1.1/post/saved_searches/destroy/%3Aid + + """ + return self.post('saved_searches/destroy/%s' % params['id'], params=params) # Places & Geo - 'get_geo_info': { - 'url': '/geo/id/{{place_id}}.json', - 'method': 'GET', - }, - 'reverse_geocode': { - 'url': '/geo/reverse_geocode.json', - 'method': 'GET', - }, - 'search_geo': { - 'url': '/geo/search.json', - 'method': 'GET', - }, - 'get_similar_places': { - 'url': '/geo/similar_places.json', - 'method': 'GET', - }, - 'create_place': { - 'url': '/geo/place.json', - 'method': 'POST', - }, + def get_geo_info(self, **params): + """Returns all the information about a known place. + Docs: https://dev.twitter.com/docs/api/1.1/get/geo/id/%3Aplace_id + + """ + return self.get('geo/id/%s' % params['place_id'], params=params) + + def reverse_geocode(self, **params): + """Given a latitude and a longitude, searches for up to 20 places + that can be used as a place_id when updating a status. + + Docs: https://dev.twitter.com/docs/api/1.1/get/geo/reverse_geocode + + """ + return self.get('geo/reverse_geocode', params=params) + + def search_geo(self, **params): + """Search for places that can be attached to a statuses/update. + + Docs: https://dev.twitter.com/docs/api/1.1/get/geo/search + + """ + return self.get('geo/search', params=params) + + def get_similar_places(self, **params): + """Locates places near the given coordinates which are similar in name. + + Docs: https://dev.twitter.com/docs/api/1.1/get/geo/similar_places + + """ + return self.get('geo/similar_places', params=params) + + def create_place(self, **params): + """Creates a new place object at the given latitude and longitude. + + Docs: https://dev.twitter.com/docs/api/1.1/post/geo/place + + """ + return self.post('geo/place', params=params) # Trends - 'get_place_trends': { - 'url': '/trends/place.json', - 'method': 'GET', - }, - 'get_available_trends': { - 'url': '/trends/available.json', - 'method': 'GET', - }, - 'get_closest_trends': { - 'url': '/trends/closest.json', - 'method': 'GET', - }, + def get_place_trends(self, **params): + """Returns the top 10 trending topics for a specific WOEID, if + trending information is available for it. + Docs: https://dev.twitter.com/docs/api/1.1/get/trends/place + + """ + return self.get('trends/place', params=params) + + def get_available_trends(self, **params): + """Returns the locations that Twitter has trending topic information for. + + Docs: https://dev.twitter.com/docs/api/1.1/get/trends/available + + """ + return self.get('trends/available', params=params) + + def get_closest_trends(self, **params): + """Returns the locations that Twitter has trending topic information + for, closest to a specified location. + + Docs: https://dev.twitter.com/docs/api/1.1/get/trends/closest + + """ + return self.get('trends/closest', params=params) # Spam Reporting - 'report_spam': { - 'url': '/users/report_spam.json', - 'method': 'POST', - }, -} + def report_spam(self, **params): + """Report the specified user as a spam account to Twitter. + + Docs: https://dev.twitter.com/docs/api/1.1/post/users/report_spam + + """ + return self.post('users/report_spam', params=params) # from https://dev.twitter.com/docs/error-codes-responses -twitter_http_status_codes = { +TWITTER_HTTP_STATUS_CODE = { 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.'), diff --git a/twython/exceptions.py b/twython/exceptions.py index 924a882..be418a2 100644 --- a/twython/exceptions.py +++ b/twython/exceptions.py @@ -1,23 +1,20 @@ -from .endpoints import twitter_http_status_codes +from .endpoints import TWITTER_HTTP_STATUS_CODE class TwythonError(Exception): """Generic error class, catch-all for most Twython issues. Special cases are handled by TwythonAuthError & TwythonRateLimitError. - Note: Syntax has changed as of Twython 1.3. To catch these, - you need to explicitly import them into your code, e.g: + from twython import TwythonError, TwythonRateLimitError, TwythonAuthError - from twython import ( - TwythonError, TwythonRateLimitError, TwythonAuthError - )""" + """ def __init__(self, msg, error_code=None, retry_after=None): self.error_code = error_code - if error_code is not None and error_code in twitter_http_status_codes: + if error_code is not None and error_code in TWITTER_HTTP_STATUS_CODE: msg = 'Twitter API returned a %s (%s), %s' % \ (error_code, - twitter_http_status_codes[error_code][0], + TWITTER_HTTP_STATUS_CODE[error_code][0], msg) super(TwythonError, self).__init__(msg) @@ -29,7 +26,9 @@ class TwythonError(Exception): class TwythonAuthError(TwythonError): """Raised when you try to access a protected resource and it fails due to - some issue with your authentication.""" + some issue with your authentication. + + """ pass @@ -37,7 +36,9 @@ class TwythonRateLimitError(TwythonError): """Raised when you've hit a rate limit. The amount of seconds to retry your request in will be appended - to the message.""" + to the message. + + """ def __init__(self, msg, error_code, retry_after=None): if isinstance(retry_after, int): msg = '%s (Retry after %d seconds)' % (msg, retry_after) diff --git a/twython/streaming/api.py b/twython/streaming/api.py index a246593..05eafdb 100644 --- a/twython/streaming/api.py +++ b/twython/streaming/api.py @@ -1,6 +1,5 @@ from .. import __version__ from ..compat import json, is_py3 -from ..exceptions import TwythonStreamError from .types import TwythonStreamerTypes import requests @@ -112,7 +111,8 @@ class TwythonStreamer(object): See https://dev.twitter.com/docs/streaming-apis/messages for messages sent along in stream responses. - :param data: dict of data recieved from the stream + :param data: data recieved from the stream + :type data: dict """ if 'delete' in data: @@ -129,7 +129,10 @@ class TwythonStreamer(object): want it handled. :param status_code: Non-200 status code sent from stream + :type status_code: int + :param data: Error message sent from stream + :type data: dict """ return @@ -141,8 +144,8 @@ class TwythonStreamer(object): Twitter docs for deletion notices: http://spen.se/8qujd - :param data: dict of data from the 'delete' key recieved from - the stream + :param data: data from the 'delete' key recieved from the stream + :type data: dict """ return @@ -154,8 +157,8 @@ class TwythonStreamer(object): Twitter docs for limit notices: http://spen.se/hzt0b - :param data: dict of data from the 'limit' key recieved from - the stream + :param data: data from the 'limit' key recieved from the stream + :type data: dict """ return @@ -167,13 +170,15 @@ class TwythonStreamer(object): Twitter docs for disconnect notices: http://spen.se/xb6mm - :param data: dict of data from the 'disconnect' key recieved from - the stream + :param data: data from the 'disconnect' key recieved from the stream + :type data: dict """ return def on_timeout(self): + """ Called when the request has timed out """ return def disconnect(self): + """Used to disconnect the streaming client manually""" self.connected = False diff --git a/twython/streaming/types.py b/twython/streaming/types.py index c17cadf..e96adef 100644 --- a/twython/streaming/types.py +++ b/twython/streaming/types.py @@ -1,9 +1,20 @@ +# -*- coding: utf-8 -*- + +""" +twython.streaming.types +~~~~~~~~~~~~~~~~~~~~~~~ + +This module contains classes and methods for :class:`TwythonStreamer` to mak +""" + + class TwythonStreamerTypes(object): """Class for different stream endpoints Not all streaming endpoints have nested endpoints. User Streams and Site Streams are single streams with no nested endpoints Status Streams include filter, sample and firehose endpoints + """ def __init__(self, streamer): self.streamer = streamer @@ -36,6 +47,7 @@ class TwythonStreamerTypesStatuses(object): Available so TwythonStreamer.statuses.filter() is available. Just a bit cleaner than TwythonStreamer.statuses_filter(), statuses_sample(), etc. all being single methods in TwythonStreamer + """ def __init__(self, streamer): self.streamer = streamer @@ -43,6 +55,8 @@ class TwythonStreamerTypesStatuses(object): def filter(self, **params): """Stream statuses/filter + :param \*\*params: Paramters to send with your stream request + Accepted params found at: https://dev.twitter.com/docs/api/1.1/post/statuses/filter """ @@ -53,6 +67,8 @@ class TwythonStreamerTypesStatuses(object): def sample(self, **params): """Stream statuses/sample + :param \*\*params: Paramters to send with your stream request + Accepted params found at: https://dev.twitter.com/docs/api/1.1/get/statuses/sample """ @@ -63,6 +79,8 @@ class TwythonStreamerTypesStatuses(object): def firehose(self, **params): """Stream statuses/firehose + :param \*\*params: Paramters to send with your stream request + Accepted params found at: https://dev.twitter.com/docs/api/1.1/get/statuses/firehose """ From ec2bd7d6860a9f3d7673876c33133a5ac7bc764a Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:41:44 -0400 Subject: [PATCH 435/687] Automatically join kwargs passed as lists into comma-separated string [ci skip] --- HISTORY.rst | 3 +++ twython/helpers.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 9f8bd3e..08eb725 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,6 +13,9 @@ History - ``twitter_token`` and ``twitter_secret`` have been replaced with ``app_key`` and ``app_secret`` respectively - ``callback_url`` is now passed through ``Twython.get_authentication_tokens`` - Update ``test_twython.py`` docstrings per http://www.python.org/dev/peps/pep-0257/ +- Removed ``get_list_memberships``, method is Twitter API 1.0 deprecated +- Developers can now pass an array as a parameter to Twitter API methods and they will be automatically joined by a comma and converted to a string +- ``endpoints.py`` now contains ``EndpointsMixin`` (rather than the previous ``api_table`` dict) for Twython, which enables Twython to use functions declared in the Mixin. 2.10.1 (2013-05-29) ++++++++++++++++++ diff --git a/twython/helpers.py b/twython/helpers.py index daa3370..b19d869 100644 --- a/twython/helpers.py +++ b/twython/helpers.py @@ -14,6 +14,8 @@ def _transparent_params(_params): params[k] = 'false' elif isinstance(v, basestring) or isinstance(v, numeric_types): params[k] = v + elif isinstance(v, list): + params[k] = ','.join(v) else: continue return params, files From 55641a1966be67d359ac50f63c99729894fda53e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:41:56 -0400 Subject: [PATCH 436/687] Begin docs [ci skip] --- .gitignore | 2 +- docs/Makefile | 177 + docs/_build/doctrees/api.doctree | Bin 0 -> 260668 bytes docs/_build/doctrees/environment.pickle | Bin 0 -> 28917 bytes docs/_build/doctrees/index.doctree | Bin 0 -> 10461 bytes .../_build/doctrees/usage/basic_usage.doctree | Bin 0 -> 12478 bytes docs/_build/doctrees/usage/install.doctree | Bin 0 -> 9191 bytes docs/_build/doctrees/usage/quickstart.doctree | Bin 0 -> 3848 bytes .../doctrees/usage/starting_out.doctree | Bin 0 -> 16717 bytes docs/_build/html/.buildinfo | 4 + docs/_build/html/_sources/api.txt | 43 + docs/_build/html/_sources/index.txt | 44 + .../html/_sources/usage/basic_usage.txt | 66 + docs/_build/html/_sources/usage/install.txt | 42 + .../_build/html/_sources/usage/quickstart.txt | 8 + .../html/_sources/usage/starting_out.txt | 79 + docs/_build/html/_static/ajax-loader.gif | Bin 0 -> 673 bytes docs/_build/html/_static/basic.css | 540 + .../css/bootstrap-responsive.css | 1109 ++ .../css/bootstrap-responsive.min.css | 9 + .../_static/bootstrap-2.3.1/css/bootstrap.css | 6158 +++++++++++ .../bootstrap-2.3.1/css/bootstrap.min.css | 9 + .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../img/glyphicons-halflings.png | Bin 0 -> 12799 bytes .../_static/bootstrap-2.3.1/js/bootstrap.js | 2276 ++++ .../bootstrap-2.3.1/js/bootstrap.min.js | 6 + docs/_build/html/_static/bootstrap-sphinx.css | 42 + docs/_build/html/_static/bootstrap-sphinx.js | 132 + docs/_build/html/_static/comment-bright.png | Bin 0 -> 3500 bytes docs/_build/html/_static/comment-close.png | Bin 0 -> 3578 bytes docs/_build/html/_static/comment.png | Bin 0 -> 3445 bytes docs/_build/html/_static/custom.css | 4 + docs/_build/html/_static/default.css | 256 + docs/_build/html/_static/doctools.js | 235 + docs/_build/html/_static/down-pressed.png | Bin 0 -> 368 bytes docs/_build/html/_static/down.png | Bin 0 -> 363 bytes docs/_build/html/_static/file.png | Bin 0 -> 392 bytes docs/_build/html/_static/jquery.js | 4 + docs/_build/html/_static/js/jquery-1.9.1.js | 9597 +++++++++++++++++ .../html/_static/js/jquery-1.9.1.min.js | 5 + docs/_build/html/_static/js/jquery-fix.js | 2 + docs/_build/html/_static/minus.png | Bin 0 -> 199 bytes docs/_build/html/_static/my-styles.css | 4 + docs/_build/html/_static/plus.png | Bin 0 -> 199 bytes docs/_build/html/_static/pygments.css | 62 + docs/_build/html/_static/searchtools.js | 622 ++ docs/_build/html/_static/sidebar.js | 159 + docs/_build/html/_static/underscore.js | 31 + docs/_build/html/_static/up-pressed.png | Bin 0 -> 372 bytes docs/_build/html/_static/up.png | Bin 0 -> 363 bytes docs/_build/html/_static/websupport.js | 808 ++ docs/_build/html/api.html | 1207 +++ docs/_build/html/genindex.html | 697 ++ docs/_build/html/index.html | 200 + docs/_build/html/objects.inv | Bin 0 -> 1217 bytes docs/_build/html/py-modindex.html | 114 + docs/_build/html/search.html | 107 + docs/_build/html/searchindex.js | 1 + docs/_build/html/usage/basic_usage.html | 177 + docs/_build/html/usage/install.html | 154 + docs/_build/html/usage/quickstart.html | 128 + docs/_build/html/usage/starting_out.html | 192 + docs/_static/custom.css | 4 + docs/_static/my-styles.css | 4 + docs/_templates/layout.html | 5 + docs/api.rst | 43 + docs/conf.py | 260 + docs/index.rst | 44 + docs/make.bat | 242 + docs/usage/basic_usage.rst | 66 + docs/usage/install.rst | 42 + docs/usage/starting_out.rst | 79 + 72 files changed, 26300 insertions(+), 1 deletion(-) create mode 100644 docs/Makefile create mode 100644 docs/_build/doctrees/api.doctree create mode 100644 docs/_build/doctrees/environment.pickle create mode 100644 docs/_build/doctrees/index.doctree create mode 100644 docs/_build/doctrees/usage/basic_usage.doctree create mode 100644 docs/_build/doctrees/usage/install.doctree create mode 100644 docs/_build/doctrees/usage/quickstart.doctree create mode 100644 docs/_build/doctrees/usage/starting_out.doctree create mode 100644 docs/_build/html/.buildinfo create mode 100644 docs/_build/html/_sources/api.txt create mode 100644 docs/_build/html/_sources/index.txt create mode 100644 docs/_build/html/_sources/usage/basic_usage.txt create mode 100644 docs/_build/html/_sources/usage/install.txt create mode 100644 docs/_build/html/_sources/usage/quickstart.txt create mode 100644 docs/_build/html/_sources/usage/starting_out.txt create mode 100644 docs/_build/html/_static/ajax-loader.gif create mode 100644 docs/_build/html/_static/basic.css create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.css create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.min.css create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.css create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.min.css create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings-white.png create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings.png create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/js/bootstrap.js create mode 100644 docs/_build/html/_static/bootstrap-2.3.1/js/bootstrap.min.js create mode 100644 docs/_build/html/_static/bootstrap-sphinx.css create mode 100644 docs/_build/html/_static/bootstrap-sphinx.js create mode 100644 docs/_build/html/_static/comment-bright.png create mode 100644 docs/_build/html/_static/comment-close.png create mode 100644 docs/_build/html/_static/comment.png create mode 100644 docs/_build/html/_static/custom.css create mode 100644 docs/_build/html/_static/default.css create mode 100644 docs/_build/html/_static/doctools.js create mode 100644 docs/_build/html/_static/down-pressed.png create mode 100644 docs/_build/html/_static/down.png create mode 100644 docs/_build/html/_static/file.png create mode 100644 docs/_build/html/_static/jquery.js create mode 100644 docs/_build/html/_static/js/jquery-1.9.1.js create mode 100644 docs/_build/html/_static/js/jquery-1.9.1.min.js create mode 100644 docs/_build/html/_static/js/jquery-fix.js create mode 100644 docs/_build/html/_static/minus.png create mode 100644 docs/_build/html/_static/my-styles.css create mode 100644 docs/_build/html/_static/plus.png create mode 100644 docs/_build/html/_static/pygments.css create mode 100644 docs/_build/html/_static/searchtools.js create mode 100644 docs/_build/html/_static/sidebar.js create mode 100644 docs/_build/html/_static/underscore.js create mode 100644 docs/_build/html/_static/up-pressed.png create mode 100644 docs/_build/html/_static/up.png create mode 100644 docs/_build/html/_static/websupport.js create mode 100644 docs/_build/html/api.html create mode 100644 docs/_build/html/genindex.html create mode 100644 docs/_build/html/index.html create mode 100644 docs/_build/html/objects.inv create mode 100644 docs/_build/html/py-modindex.html create mode 100644 docs/_build/html/search.html create mode 100644 docs/_build/html/searchindex.js create mode 100644 docs/_build/html/usage/basic_usage.html create mode 100644 docs/_build/html/usage/install.html create mode 100644 docs/_build/html/usage/quickstart.html create mode 100644 docs/_build/html/usage/starting_out.html create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/my-styles.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/usage/basic_usage.rst create mode 100644 docs/usage/install.rst create mode 100644 docs/usage/starting_out.rst diff --git a/.gitignore b/.gitignore index 7146bfd..66ff3de 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,6 @@ nosetests.xml .mr.developer.cfg .project .pydevproject -twython/.DS_Store +*.DS_Store test.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..8f0c0a1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Twython.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Twython.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Twython" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Twython" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_build/doctrees/api.doctree b/docs/_build/doctrees/api.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0e5c6a362bf39c2cf2c71934eacefc56a4fa752f GIT binary patch literal 260668 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 E2O9`5i~s-t literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..60d72aba7ee4c3df065b6de013c12541339b5c61 GIT binary patch literal 10461 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 X9_SNz(eya3@<5qB$@fDJ)-C=YQuGQ4 literal 0 HcmV?d00001 diff --git a/docs/_build/doctrees/usage/quickstart.doctree b/docs/_build/doctrees/usage/quickstart.doctree new file mode 100644 index 0000000000000000000000000000000000000000..b44fe0c660882e34a15005e698aa38db1172206d GIT binary patch literal 3848 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 Yq2Gc30Tw0(CIA2c literal 0 HcmV?d00001 diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo new file mode 100644 index 0000000..eab9447 --- /dev/null +++ b/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: bfb67ab370c0e278534d9b496b14ab4f +tags: a205e9ed8462ae86fdd2f73488852ba9 diff --git a/docs/_build/html/_sources/api.txt b/docs/_build/html/_sources/api.txt new file mode 100644 index 0000000..48a5133 --- /dev/null +++ b/docs/_build/html/_sources/api.txt @@ -0,0 +1,43 @@ +.. _api: + +Developer Interface +=================== + +.. module:: twython + +This page of the documentation will cover all methods and classes available to the developer. + +Twython, currently, has two main interfaces: + +- Twitter's Core API (updating statuses, getting timelines, direct messaging, etc) +- Twitter's Streaming API + +Core Interface +-------------- + +.. autoclass:: Twython + :special-members: __init__ + :inherited-members: + +Streaming Interface +------------------- + +.. autoclass:: TwythonStreamer + :special-members: __init__ + :inherited-members: + +Streaming Types +~~~~~~~~~~~~~~~ + +.. autoclass:: twython.streaming.types.TwythonStreamerTypes + :inherited-members: + +.. autoclass:: twython.streaming.types.TwythonStreamerTypesStatuses + :inherited-members: + +Exceptions +---------- + +.. autoexception:: TwythonError +.. autoexception:: TwythonAuthError +.. autoexception:: TwythonRateLimitError \ No newline at end of file diff --git a/docs/_build/html/_sources/index.txt b/docs/_build/html/_sources/index.txt new file mode 100644 index 0000000..41fd1ec --- /dev/null +++ b/docs/_build/html/_sources/index.txt @@ -0,0 +1,44 @@ +.. Twython documentation master file, created by + sphinx-quickstart on Thu May 30 22:31:25 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Twython +======= + + | Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs + + +Features +-------- +- Query data for: + - User information + - Twitter lists + - Timelines + - Direct Messages + - and anything found in `the Twitter API docs `_. +- Image Uploading! + - **Update user status with an image** + - Change user avatar + - Change user background image + - Change user banner image +- Support for Twitter's Streaming API +- Seamless Python 3 support! + +Usage +----- + +.. toctree:: + :maxdepth: 2 + + usage/install + usage/starting_out + usage/basic_usage + +Twython API Documentation +------------------------- + +.. toctree:: + :maxdepth: 2 + + api diff --git a/docs/_build/html/_sources/usage/basic_usage.txt b/docs/_build/html/_sources/usage/basic_usage.txt new file mode 100644 index 0000000..570466b --- /dev/null +++ b/docs/_build/html/_sources/usage/basic_usage.txt @@ -0,0 +1,66 @@ +.. _basic-usage: + +Basic Usage +=========== + +This section will cover how to use Twython and interact with some basic Twitter API calls + +Before you make any API calls, make sure you :ref:`authenticated ` the user! + +Create a Twython instance with your application keys and the users OAuth tokens:: + + from twython import Twython + twitter = Twython(APP_KEY, APP_SECRET + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + +.. admonition:: Important + + All sections on this page will assume you're using a Twython instance + +What Twython Returns +-------------------- + +Twython returns a dictionary of JSON response from Twitter + +User Information +---------------- + +Documentation: https://dev.twitter.com/docs/api/1.1/get/account/verify_credentials + +:: + + twitter.verify_credentials() + +Authenticated Users Home Timeline +--------------------------------- + +Documentation: https://dev.twitter.com/docs/api/1.1/get/statuses/home_timeline + +:: + + twitter.get_home_timeline() + +Search +------ + +Documentation: https://dev.twitter.com/docs/api/1.1/get/search/tweets + +:: + + twitter.search(q='python') + +To help explain :ref:`dynamic function arguments ` a little more, you can see that the previous call used the keyword argument ``q``, that is because Twitter specifies in their `search documentation `_ that the search call accepts the parameter "q". You can pass mutiple keyword arguments. The search documentation also specifies that the call accepts the parameter "result_type" + +:: + + twitter.search(q='python', result_type='popular') + +Updating Status +--------------- + +Documentation: https://dev.twitter.com/docs/api/1/post/statuses/update + +:: + + twitter.update_status(status='See how easy using Twython is!') + diff --git a/docs/_build/html/_sources/usage/install.txt b/docs/_build/html/_sources/usage/install.txt new file mode 100644 index 0000000..9b54d21 --- /dev/null +++ b/docs/_build/html/_sources/usage/install.txt @@ -0,0 +1,42 @@ +.. _install: + +Installation +============ + +Information on how to properly install Twython + + +Pip or Easy Install +------------------- + +Install Twython via `pip `_:: + + $ pip install twython + +or, with `easy_install `_:: + + $ easy_install twython + +But, hey... `that's up to you `_. + + +Source Code +----------- + +Twython is actively maintained on GitHub + +Feel free to clone the repository:: + + git clone git://github.com/ryanmcgrath/twython.git + +`tarball `_:: + + $ curl -OL https://github.com/ryanmcgrath/twython/tarball/master + +`zipball `_:: + + $ curl -OL https://github.com/ryanmcgrath/twython/zipball/master + +Now that you have the source code, install it into your site-packages directory:: + + $ python setup.py install \ No newline at end of file diff --git a/docs/_build/html/_sources/usage/quickstart.txt b/docs/_build/html/_sources/usage/quickstart.txt new file mode 100644 index 0000000..8cc4549 --- /dev/null +++ b/docs/_build/html/_sources/usage/quickstart.txt @@ -0,0 +1,8 @@ +.. _quickstart: + +Quickstart +========== + +.. module:: twython.api + +Eager \ No newline at end of file diff --git a/docs/_build/html/_sources/usage/starting_out.txt b/docs/_build/html/_sources/usage/starting_out.txt new file mode 100644 index 0000000..a000319 --- /dev/null +++ b/docs/_build/html/_sources/usage/starting_out.txt @@ -0,0 +1,79 @@ +.. _starting-out: + +Starting Out +============ + +This section is going to help you understand creating a Twitter Application, authenticating a user, and making basic API calls + +Beginning +--------- + +First, you'll want to head over to https://dev.twitter.com/apps and register an application! + +After you register, grab your applications ``Consumer Key`` and ``Consumer Secret`` from the application details tab. + +Now you're ready to start authentication! + +Authentication +-------------- + +First, you'll want to import Twython:: + + from twython import Twython + +Now, you'll want to create a Twython instance with your ``Consumer Key`` and ``Consumer Secert`` + +:: + + APP_KEY = 'YOUR_APP_KEY' + APP_SECET = 'YOUR_APP_SECRET' + + twitter = Twython(APP_KEY, APP_SECRET) + auth = twitter.get_authentication_tokens(callback_url='http://mysite.com/callback') + +From the ``auth`` variable, save the ``oauth_token_secret`` for later use. In Django or other web frameworks, you might want to store it to a session variable:: + + OAUTH_TOKEN_SECRET = auth['oauth_token_secret'] + +Send the user to the authentication url, you can obtain it by accessing:: + + auth['auth_url'] + +Handling the Callback +--------------------- + +After they authorize your application to access some of their account details, they'll be redirected to the callback url you specified in ``get_autentication_tokens`` + +You'll want to extract the ``oauth_token`` and ``oauth_verifier`` from the url. + +Django example: +:: + + OAUTH_TOKEN = request.GET['oauth_token'] + oauth_verifier = request.GET['oauth_verifier'] + +Now that you have the ``oauth_token`` and ``oauth_verifier`` stored to variables, you'll want to create a new instance of Twython and grab the final user tokens:: + + twitter = Twython(APP_KEY, APP_SECRET, + OAUTH_TOKEN, OAUTH_TOKEN_SECRET) + + final_step = twitter.get_authorized_tokens(oauth_verifier) + +Once you have the final user tokens, store them in a database for later use!:: + + OAUTH_TOKEN = final_step['oauth_token'] + OAUTH_TOKEN_SECERT = final_step['oauth_token_secret'] + +The Twython API Table +--------------------- + +In the Twython package is a file called ``endpoints.py`` which holds a dictionary of all Twitter API endpoints. This is so Twython's core ``api.py`` isn't cluttered with 50+ methods. We dynamically register these funtions when a Twython object is initiated. + +Dynamic Function Arguments +-------------------------- + +Keyword arguments to functions are mapped to the functions available for each endpoint in the Twitter API docs. Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up from you using them by this library. + +----------------------- + +Now that you have your application tokens and user tokens, check out the :ref:`basic usage ` section. diff --git a/docs/_build/html/_static/ajax-loader.gif b/docs/_build/html/_static/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..61faf8cab23993bd3e1560bff0668bd628642330 GIT binary patch literal 673 zcmZ?wbhEHb6krfw_{6~Q|Nno%(3)e{?)x>&1u}A`t?OF7Z|1gRivOgXi&7IyQd1Pl zGfOfQ60;I3a`F>X^fL3(@);C=vM_KlFfb_o=k{|A33hf2a5d61U}gjg=>Rd%XaNQW zW@Cw{|b%Y*pl8F?4B9 zlo4Fz*0kZGJabY|>}Okf0}CCg{u4`zEPY^pV?j2@h+|igy0+Kz6p;@SpM4s6)XEMg z#3Y4GX>Hjlml5ftdH$4x0JGdn8~MX(U~_^d!Hi)=HU{V%g+mi8#UGbE-*ao8f#h+S z2a0-5+vc7MU$e-NhmBjLIC1v|)9+Im8x1yacJ7{^tLX(ZhYi^rpmXm0`@ku9b53aN zEXH@Y3JaztblgpxbJt{AtE1ad1Ca>{v$rwwvK(>{m~Gf_=-Ro7Fk{#;i~+{{>QtvI yb2P8Zac~?~=sRA>$6{!(^3;ZP0TPFR(G_-UDU(8Jl0?(IXu$~#4A!880|o%~Al1tN literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 0000000..a04c8e1 --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,540 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 170px; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + width: 30px; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.css b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.css new file mode 100644 index 0000000..fcd72f7 --- /dev/null +++ b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.css @@ -0,0 +1,1109 @@ +/*! + * Bootstrap Responsive v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +@-ms-viewport { + width: device-width; +} + +.hidden { + display: none; + visibility: hidden; +} + +.visible-phone { + display: none !important; +} + +.visible-tablet { + display: none !important; +} + +.hidden-desktop { + display: none !important; +} + +.visible-desktop { + display: inherit !important; +} + +@media (min-width: 768px) and (max-width: 979px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important ; + } + .visible-tablet { + display: inherit !important; + } + .hidden-tablet { + display: none !important; + } +} + +@media (max-width: 767px) { + .hidden-desktop { + display: inherit !important; + } + .visible-desktop { + display: none !important; + } + .visible-phone { + display: inherit !important; + } + .hidden-phone { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: inherit !important; + } + .hidden-print { + display: none !important; + } +} + +@media (min-width: 1200px) { + .row { + margin-left: -30px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 30px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 1170px; + } + .span12 { + width: 1170px; + } + .span11 { + width: 1070px; + } + .span10 { + width: 970px; + } + .span9 { + width: 870px; + } + .span8 { + width: 770px; + } + .span7 { + width: 670px; + } + .span6 { + width: 570px; + } + .span5 { + width: 470px; + } + .span4 { + width: 370px; + } + .span3 { + width: 270px; + } + .span2 { + width: 170px; + } + .span1 { + width: 70px; + } + .offset12 { + margin-left: 1230px; + } + .offset11 { + margin-left: 1130px; + } + .offset10 { + margin-left: 1030px; + } + .offset9 { + margin-left: 930px; + } + .offset8 { + margin-left: 830px; + } + .offset7 { + margin-left: 730px; + } + .offset6 { + margin-left: 630px; + } + .offset5 { + margin-left: 530px; + } + .offset4 { + margin-left: 430px; + } + .offset3 { + margin-left: 330px; + } + .offset2 { + margin-left: 230px; + } + .offset1 { + margin-left: 130px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.564102564102564%; + *margin-left: 2.5109110747408616%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.564102564102564%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.45299145299145%; + *width: 91.39979996362975%; + } + .row-fluid .span10 { + width: 82.90598290598291%; + *width: 82.8527914166212%; + } + .row-fluid .span9 { + width: 74.35897435897436%; + *width: 74.30578286961266%; + } + .row-fluid .span8 { + width: 65.81196581196582%; + *width: 65.75877432260411%; + } + .row-fluid .span7 { + width: 57.26495726495726%; + *width: 57.21176577559556%; + } + .row-fluid .span6 { + width: 48.717948717948715%; + *width: 48.664757228587014%; + } + .row-fluid .span5 { + width: 40.17094017094017%; + *width: 40.11774868157847%; + } + .row-fluid .span4 { + width: 31.623931623931625%; + *width: 31.570740134569924%; + } + .row-fluid .span3 { + width: 23.076923076923077%; + *width: 23.023731587561375%; + } + .row-fluid .span2 { + width: 14.52991452991453%; + *width: 14.476723040552828%; + } + .row-fluid .span1 { + width: 5.982905982905983%; + *width: 5.929714493544281%; + } + .row-fluid .offset12 { + margin-left: 105.12820512820512%; + *margin-left: 105.02182214948171%; + } + .row-fluid .offset12:first-child { + margin-left: 102.56410256410257%; + *margin-left: 102.45771958537915%; + } + .row-fluid .offset11 { + margin-left: 96.58119658119658%; + *margin-left: 96.47481360247316%; + } + .row-fluid .offset11:first-child { + margin-left: 94.01709401709402%; + *margin-left: 93.91071103837061%; + } + .row-fluid .offset10 { + margin-left: 88.03418803418803%; + *margin-left: 87.92780505546462%; + } + .row-fluid .offset10:first-child { + margin-left: 85.47008547008548%; + *margin-left: 85.36370249136206%; + } + .row-fluid .offset9 { + margin-left: 79.48717948717949%; + *margin-left: 79.38079650845607%; + } + .row-fluid .offset9:first-child { + margin-left: 76.92307692307693%; + *margin-left: 76.81669394435352%; + } + .row-fluid .offset8 { + margin-left: 70.94017094017094%; + *margin-left: 70.83378796144753%; + } + .row-fluid .offset8:first-child { + margin-left: 68.37606837606839%; + *margin-left: 68.26968539734497%; + } + .row-fluid .offset7 { + margin-left: 62.393162393162385%; + *margin-left: 62.28677941443899%; + } + .row-fluid .offset7:first-child { + margin-left: 59.82905982905982%; + *margin-left: 59.72267685033642%; + } + .row-fluid .offset6 { + margin-left: 53.84615384615384%; + *margin-left: 53.739770867430444%; + } + .row-fluid .offset6:first-child { + margin-left: 51.28205128205128%; + *margin-left: 51.175668303327875%; + } + .row-fluid .offset5 { + margin-left: 45.299145299145295%; + *margin-left: 45.1927623204219%; + } + .row-fluid .offset5:first-child { + margin-left: 42.73504273504273%; + *margin-left: 42.62865975631933%; + } + .row-fluid .offset4 { + margin-left: 36.75213675213675%; + *margin-left: 36.645753773413354%; + } + .row-fluid .offset4:first-child { + margin-left: 34.18803418803419%; + *margin-left: 34.081651209310785%; + } + .row-fluid .offset3 { + margin-left: 28.205128205128204%; + *margin-left: 28.0987452264048%; + } + .row-fluid .offset3:first-child { + margin-left: 25.641025641025642%; + *margin-left: 25.53464266230224%; + } + .row-fluid .offset2 { + margin-left: 19.65811965811966%; + *margin-left: 19.551736679396257%; + } + .row-fluid .offset2:first-child { + margin-left: 17.094017094017094%; + *margin-left: 16.98763411529369%; + } + .row-fluid .offset1 { + margin-left: 11.11111111111111%; + *margin-left: 11.004728132387708%; + } + .row-fluid .offset1:first-child { + margin-left: 8.547008547008547%; + *margin-left: 8.440625568285142%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 30px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 1156px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 1056px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 956px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 856px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 756px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 656px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 556px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 456px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 356px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 256px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 156px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 56px; + } + .thumbnails { + margin-left: -30px; + } + .thumbnails > li { + margin-left: 30px; + } + .row-fluid .thumbnails { + margin-left: 0; + } +} + +@media (min-width: 768px) and (max-width: 979px) { + .row { + margin-left: -20px; + *zoom: 1; + } + .row:before, + .row:after { + display: table; + line-height: 0; + content: ""; + } + .row:after { + clear: both; + } + [class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; + } + .container, + .navbar-static-top .container, + .navbar-fixed-top .container, + .navbar-fixed-bottom .container { + width: 724px; + } + .span12 { + width: 724px; + } + .span11 { + width: 662px; + } + .span10 { + width: 600px; + } + .span9 { + width: 538px; + } + .span8 { + width: 476px; + } + .span7 { + width: 414px; + } + .span6 { + width: 352px; + } + .span5 { + width: 290px; + } + .span4 { + width: 228px; + } + .span3 { + width: 166px; + } + .span2 { + width: 104px; + } + .span1 { + width: 42px; + } + .offset12 { + margin-left: 764px; + } + .offset11 { + margin-left: 702px; + } + .offset10 { + margin-left: 640px; + } + .offset9 { + margin-left: 578px; + } + .offset8 { + margin-left: 516px; + } + .offset7 { + margin-left: 454px; + } + .offset6 { + margin-left: 392px; + } + .offset5 { + margin-left: 330px; + } + .offset4 { + margin-left: 268px; + } + .offset3 { + margin-left: 206px; + } + .offset2 { + margin-left: 144px; + } + .offset1 { + margin-left: 82px; + } + .row-fluid { + width: 100%; + *zoom: 1; + } + .row-fluid:before, + .row-fluid:after { + display: table; + line-height: 0; + content: ""; + } + .row-fluid:after { + clear: both; + } + .row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.7624309392265194%; + *margin-left: 2.709239449864817%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="span"]:first-child { + margin-left: 0; + } + .row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.7624309392265194%; + } + .row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; + } + .row-fluid .span11 { + width: 91.43646408839778%; + *width: 91.38327259903608%; + } + .row-fluid .span10 { + width: 82.87292817679558%; + *width: 82.81973668743387%; + } + .row-fluid .span9 { + width: 74.30939226519337%; + *width: 74.25620077583166%; + } + .row-fluid .span8 { + width: 65.74585635359117%; + *width: 65.69266486422946%; + } + .row-fluid .span7 { + width: 57.18232044198895%; + *width: 57.12912895262725%; + } + .row-fluid .span6 { + width: 48.61878453038674%; + *width: 48.56559304102504%; + } + .row-fluid .span5 { + width: 40.05524861878453%; + *width: 40.00205712942283%; + } + .row-fluid .span4 { + width: 31.491712707182323%; + *width: 31.43852121782062%; + } + .row-fluid .span3 { + width: 22.92817679558011%; + *width: 22.87498530621841%; + } + .row-fluid .span2 { + width: 14.3646408839779%; + *width: 14.311449394616199%; + } + .row-fluid .span1 { + width: 5.801104972375691%; + *width: 5.747913483013988%; + } + .row-fluid .offset12 { + margin-left: 105.52486187845304%; + *margin-left: 105.41847889972962%; + } + .row-fluid .offset12:first-child { + margin-left: 102.76243093922652%; + *margin-left: 102.6560479605031%; + } + .row-fluid .offset11 { + margin-left: 96.96132596685082%; + *margin-left: 96.8549429881274%; + } + .row-fluid .offset11:first-child { + margin-left: 94.1988950276243%; + *margin-left: 94.09251204890089%; + } + .row-fluid .offset10 { + margin-left: 88.39779005524862%; + *margin-left: 88.2914070765252%; + } + .row-fluid .offset10:first-child { + margin-left: 85.6353591160221%; + *margin-left: 85.52897613729868%; + } + .row-fluid .offset9 { + margin-left: 79.8342541436464%; + *margin-left: 79.72787116492299%; + } + .row-fluid .offset9:first-child { + margin-left: 77.07182320441989%; + *margin-left: 76.96544022569647%; + } + .row-fluid .offset8 { + margin-left: 71.2707182320442%; + *margin-left: 71.16433525332079%; + } + .row-fluid .offset8:first-child { + margin-left: 68.50828729281768%; + *margin-left: 68.40190431409427%; + } + .row-fluid .offset7 { + margin-left: 62.70718232044199%; + *margin-left: 62.600799341718584%; + } + .row-fluid .offset7:first-child { + margin-left: 59.94475138121547%; + *margin-left: 59.838368402492065%; + } + .row-fluid .offset6 { + margin-left: 54.14364640883978%; + *margin-left: 54.037263430116376%; + } + .row-fluid .offset6:first-child { + margin-left: 51.38121546961326%; + *margin-left: 51.27483249088986%; + } + .row-fluid .offset5 { + margin-left: 45.58011049723757%; + *margin-left: 45.47372751851417%; + } + .row-fluid .offset5:first-child { + margin-left: 42.81767955801105%; + *margin-left: 42.71129657928765%; + } + .row-fluid .offset4 { + margin-left: 37.01657458563536%; + *margin-left: 36.91019160691196%; + } + .row-fluid .offset4:first-child { + margin-left: 34.25414364640884%; + *margin-left: 34.14776066768544%; + } + .row-fluid .offset3 { + margin-left: 28.45303867403315%; + *margin-left: 28.346655695309746%; + } + .row-fluid .offset3:first-child { + margin-left: 25.69060773480663%; + *margin-left: 25.584224756083227%; + } + .row-fluid .offset2 { + margin-left: 19.88950276243094%; + *margin-left: 19.783119783707537%; + } + .row-fluid .offset2:first-child { + margin-left: 17.12707182320442%; + *margin-left: 17.02068884448102%; + } + .row-fluid .offset1 { + margin-left: 11.32596685082873%; + *margin-left: 11.219583872105325%; + } + .row-fluid .offset1:first-child { + margin-left: 8.56353591160221%; + *margin-left: 8.457152932878806%; + } + input, + textarea, + .uneditable-input { + margin-left: 0; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; + } + input.span12, + textarea.span12, + .uneditable-input.span12 { + width: 710px; + } + input.span11, + textarea.span11, + .uneditable-input.span11 { + width: 648px; + } + input.span10, + textarea.span10, + .uneditable-input.span10 { + width: 586px; + } + input.span9, + textarea.span9, + .uneditable-input.span9 { + width: 524px; + } + input.span8, + textarea.span8, + .uneditable-input.span8 { + width: 462px; + } + input.span7, + textarea.span7, + .uneditable-input.span7 { + width: 400px; + } + input.span6, + textarea.span6, + .uneditable-input.span6 { + width: 338px; + } + input.span5, + textarea.span5, + .uneditable-input.span5 { + width: 276px; + } + input.span4, + textarea.span4, + .uneditable-input.span4 { + width: 214px; + } + input.span3, + textarea.span3, + .uneditable-input.span3 { + width: 152px; + } + input.span2, + textarea.span2, + .uneditable-input.span2 { + width: 90px; + } + input.span1, + textarea.span1, + .uneditable-input.span1 { + width: 28px; + } +} + +@media (max-width: 767px) { + body { + padding-right: 20px; + padding-left: 20px; + } + .navbar-fixed-top, + .navbar-fixed-bottom, + .navbar-static-top { + margin-right: -20px; + margin-left: -20px; + } + .container-fluid { + padding: 0; + } + .dl-horizontal dt { + float: none; + width: auto; + clear: none; + text-align: left; + } + .dl-horizontal dd { + margin-left: 0; + } + .container { + width: auto; + } + .row-fluid { + width: 100%; + } + .row, + .thumbnails { + margin-left: 0; + } + .thumbnails > li { + float: none; + margin-left: 0; + } + [class*="span"], + .uneditable-input[class*="span"], + .row-fluid [class*="span"] { + display: block; + float: none; + width: 100%; + margin-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .span12, + .row-fluid .span12 { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .row-fluid [class*="offset"]:first-child { + margin-left: 0; + } + .input-large, + .input-xlarge, + .input-xxlarge, + input[class*="span"], + select[class*="span"], + textarea[class*="span"], + .uneditable-input { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + } + .input-prepend input, + .input-append input, + .input-prepend input[class*="span"], + .input-append input[class*="span"] { + display: inline-block; + width: auto; + } + .controls-row [class*="span"] + [class*="span"] { + margin-left: 0; + } + .modal { + position: fixed; + top: 20px; + right: 20px; + left: 20px; + width: auto; + margin: 0; + } + .modal.fade { + top: -100px; + } + .modal.fade.in { + top: 20px; + } +} + +@media (max-width: 480px) { + .nav-collapse { + -webkit-transform: translate3d(0, 0, 0); + } + .page-header h1 small { + display: block; + line-height: 20px; + } + input[type="checkbox"], + input[type="radio"] { + border: 1px solid #ccc; + } + .form-horizontal .control-label { + float: none; + width: auto; + padding-top: 0; + text-align: left; + } + .form-horizontal .controls { + margin-left: 0; + } + .form-horizontal .control-list { + padding-top: 0; + } + .form-horizontal .form-actions { + padding-right: 10px; + padding-left: 10px; + } + .media .pull-left, + .media .pull-right { + display: block; + float: none; + margin-bottom: 10px; + } + .media-object { + margin-right: 0; + margin-left: 0; + } + .modal { + top: 10px; + right: 10px; + left: 10px; + } + .modal-header .close { + padding: 10px; + margin: -10px; + } + .carousel-caption { + position: static; + } +} + +@media (max-width: 979px) { + body { + padding-top: 0; + } + .navbar-fixed-top, + .navbar-fixed-bottom { + position: static; + } + .navbar-fixed-top { + margin-bottom: 20px; + } + .navbar-fixed-bottom { + margin-top: 20px; + } + .navbar-fixed-top .navbar-inner, + .navbar-fixed-bottom .navbar-inner { + padding: 5px; + } + .navbar .container { + width: auto; + padding: 0; + } + .navbar .brand { + padding-right: 10px; + padding-left: 10px; + margin: 0 0 0 -5px; + } + .nav-collapse { + clear: both; + } + .nav-collapse .nav { + float: none; + margin: 0 0 10px; + } + .nav-collapse .nav > li { + float: none; + } + .nav-collapse .nav > li > a { + margin-bottom: 2px; + } + .nav-collapse .nav > .divider-vertical { + display: none; + } + .nav-collapse .nav .nav-header { + color: #777777; + text-shadow: none; + } + .nav-collapse .nav > li > a, + .nav-collapse .dropdown-menu a { + padding: 9px 15px; + font-weight: bold; + color: #777777; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + .nav-collapse .btn { + padding: 4px 10px 4px; + font-weight: normal; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } + .nav-collapse .dropdown-menu li + li a { + margin-bottom: 2px; + } + .nav-collapse .nav > li > a:hover, + .nav-collapse .nav > li > a:focus, + .nav-collapse .dropdown-menu a:hover, + .nav-collapse .dropdown-menu a:focus { + background-color: #f2f2f2; + } + .navbar-inverse .nav-collapse .nav > li > a, + .navbar-inverse .nav-collapse .dropdown-menu a { + color: #999999; + } + .navbar-inverse .nav-collapse .nav > li > a:hover, + .navbar-inverse .nav-collapse .nav > li > a:focus, + .navbar-inverse .nav-collapse .dropdown-menu a:hover, + .navbar-inverse .nav-collapse .dropdown-menu a:focus { + background-color: #111111; + } + .nav-collapse.in .btn-group { + padding: 0; + margin-top: 5px; + } + .nav-collapse .dropdown-menu { + position: static; + top: auto; + left: auto; + display: none; + float: none; + max-width: none; + padding: 0; + margin: 0 15px; + background-color: transparent; + border: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + } + .nav-collapse .open > .dropdown-menu { + display: block; + } + .nav-collapse .dropdown-menu:before, + .nav-collapse .dropdown-menu:after { + display: none; + } + .nav-collapse .dropdown-menu .divider { + display: none; + } + .nav-collapse .nav > li > .dropdown-menu:before, + .nav-collapse .nav > li > .dropdown-menu:after { + display: none; + } + .nav-collapse .navbar-form, + .nav-collapse .navbar-search { + float: none; + padding: 10px 15px; + margin: 10px 0; + border-top: 1px solid #f2f2f2; + border-bottom: 1px solid #f2f2f2; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-inverse .nav-collapse .navbar-form, + .navbar-inverse .nav-collapse .navbar-search { + border-top-color: #111111; + border-bottom-color: #111111; + } + .navbar .nav-collapse .nav.pull-right { + float: none; + margin-left: 0; + } + .nav-collapse, + .nav-collapse.collapse { + height: 0; + overflow: hidden; + } + .navbar .btn-navbar { + display: block; + } + .navbar-static .navbar-inner { + padding-right: 10px; + padding-left: 10px; + } +} + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + overflow: visible !important; + } +} diff --git a/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.min.css b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.min.css new file mode 100644 index 0000000..d1b7f4b --- /dev/null +++ b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap-responsive.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap Responsive v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} diff --git a/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.css b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.css new file mode 100644 index 0000000..2f56af3 --- /dev/null +++ b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.css @@ -0,0 +1,6158 @@ +/*! + * Bootstrap v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ + +.clearfix { + *zoom: 1; +} + +.clearfix:before, +.clearfix:after { + display: table; + line-height: 0; + content: ""; +} + +.clearfix:after { + clear: both; +} + +.hide-text { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.input-block-level { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +audio:not([controls]) { + display: none; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +a:hover, +a:active { + outline: 0; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + width: auto\9; + height: auto; + max-width: 100%; + vertical-align: middle; + border: 0; + -ms-interpolation-mode: bicubic; +} + +#map_canvas img, +.google-maps img { + max-width: none; +} + +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} + +button, +input { + *overflow: visible; + line-height: normal; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +label, +select, +button, +input[type="button"], +input[type="reset"], +input[type="submit"], +input[type="radio"], +input[type="checkbox"] { + cursor: pointer; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 0.5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } +} + +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 20px; + color: #333333; + background-color: #ffffff; +} + +a { + color: #0088cc; + text-decoration: none; +} + +a:hover, +a:focus { + color: #005580; + text-decoration: underline; +} + +.img-rounded { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.img-polaroid { + padding: 4px; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.img-circle { + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + border-radius: 500px; +} + +.row { + margin-left: -20px; + *zoom: 1; +} + +.row:before, +.row:after { + display: table; + line-height: 0; + content: ""; +} + +.row:after { + clear: both; +} + +[class*="span"] { + float: left; + min-height: 1px; + margin-left: 20px; +} + +.container, +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.span12 { + width: 940px; +} + +.span11 { + width: 860px; +} + +.span10 { + width: 780px; +} + +.span9 { + width: 700px; +} + +.span8 { + width: 620px; +} + +.span7 { + width: 540px; +} + +.span6 { + width: 460px; +} + +.span5 { + width: 380px; +} + +.span4 { + width: 300px; +} + +.span3 { + width: 220px; +} + +.span2 { + width: 140px; +} + +.span1 { + width: 60px; +} + +.offset12 { + margin-left: 980px; +} + +.offset11 { + margin-left: 900px; +} + +.offset10 { + margin-left: 820px; +} + +.offset9 { + margin-left: 740px; +} + +.offset8 { + margin-left: 660px; +} + +.offset7 { + margin-left: 580px; +} + +.offset6 { + margin-left: 500px; +} + +.offset5 { + margin-left: 420px; +} + +.offset4 { + margin-left: 340px; +} + +.offset3 { + margin-left: 260px; +} + +.offset2 { + margin-left: 180px; +} + +.offset1 { + margin-left: 100px; +} + +.row-fluid { + width: 100%; + *zoom: 1; +} + +.row-fluid:before, +.row-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.row-fluid:after { + clear: both; +} + +.row-fluid [class*="span"] { + display: block; + float: left; + width: 100%; + min-height: 30px; + margin-left: 2.127659574468085%; + *margin-left: 2.074468085106383%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.row-fluid [class*="span"]:first-child { + margin-left: 0; +} + +.row-fluid .controls-row [class*="span"] + [class*="span"] { + margin-left: 2.127659574468085%; +} + +.row-fluid .span12 { + width: 100%; + *width: 99.94680851063829%; +} + +.row-fluid .span11 { + width: 91.48936170212765%; + *width: 91.43617021276594%; +} + +.row-fluid .span10 { + width: 82.97872340425532%; + *width: 82.92553191489361%; +} + +.row-fluid .span9 { + width: 74.46808510638297%; + *width: 74.41489361702126%; +} + +.row-fluid .span8 { + width: 65.95744680851064%; + *width: 65.90425531914893%; +} + +.row-fluid .span7 { + width: 57.44680851063829%; + *width: 57.39361702127659%; +} + +.row-fluid .span6 { + width: 48.93617021276595%; + *width: 48.88297872340425%; +} + +.row-fluid .span5 { + width: 40.42553191489362%; + *width: 40.37234042553192%; +} + +.row-fluid .span4 { + width: 31.914893617021278%; + *width: 31.861702127659576%; +} + +.row-fluid .span3 { + width: 23.404255319148934%; + *width: 23.351063829787233%; +} + +.row-fluid .span2 { + width: 14.893617021276595%; + *width: 14.840425531914894%; +} + +.row-fluid .span1 { + width: 6.382978723404255%; + *width: 6.329787234042553%; +} + +.row-fluid .offset12 { + margin-left: 104.25531914893617%; + *margin-left: 104.14893617021275%; +} + +.row-fluid .offset12:first-child { + margin-left: 102.12765957446808%; + *margin-left: 102.02127659574467%; +} + +.row-fluid .offset11 { + margin-left: 95.74468085106382%; + *margin-left: 95.6382978723404%; +} + +.row-fluid .offset11:first-child { + margin-left: 93.61702127659574%; + *margin-left: 93.51063829787232%; +} + +.row-fluid .offset10 { + margin-left: 87.23404255319149%; + *margin-left: 87.12765957446807%; +} + +.row-fluid .offset10:first-child { + margin-left: 85.1063829787234%; + *margin-left: 84.99999999999999%; +} + +.row-fluid .offset9 { + margin-left: 78.72340425531914%; + *margin-left: 78.61702127659572%; +} + +.row-fluid .offset9:first-child { + margin-left: 76.59574468085106%; + *margin-left: 76.48936170212764%; +} + +.row-fluid .offset8 { + margin-left: 70.2127659574468%; + *margin-left: 70.10638297872339%; +} + +.row-fluid .offset8:first-child { + margin-left: 68.08510638297872%; + *margin-left: 67.9787234042553%; +} + +.row-fluid .offset7 { + margin-left: 61.70212765957446%; + *margin-left: 61.59574468085106%; +} + +.row-fluid .offset7:first-child { + margin-left: 59.574468085106375%; + *margin-left: 59.46808510638297%; +} + +.row-fluid .offset6 { + margin-left: 53.191489361702125%; + *margin-left: 53.085106382978715%; +} + +.row-fluid .offset6:first-child { + margin-left: 51.063829787234035%; + *margin-left: 50.95744680851063%; +} + +.row-fluid .offset5 { + margin-left: 44.68085106382979%; + *margin-left: 44.57446808510638%; +} + +.row-fluid .offset5:first-child { + margin-left: 42.5531914893617%; + *margin-left: 42.4468085106383%; +} + +.row-fluid .offset4 { + margin-left: 36.170212765957444%; + *margin-left: 36.06382978723405%; +} + +.row-fluid .offset4:first-child { + margin-left: 34.04255319148936%; + *margin-left: 33.93617021276596%; +} + +.row-fluid .offset3 { + margin-left: 27.659574468085104%; + *margin-left: 27.5531914893617%; +} + +.row-fluid .offset3:first-child { + margin-left: 25.53191489361702%; + *margin-left: 25.425531914893618%; +} + +.row-fluid .offset2 { + margin-left: 19.148936170212764%; + *margin-left: 19.04255319148936%; +} + +.row-fluid .offset2:first-child { + margin-left: 17.02127659574468%; + *margin-left: 16.914893617021278%; +} + +.row-fluid .offset1 { + margin-left: 10.638297872340425%; + *margin-left: 10.53191489361702%; +} + +.row-fluid .offset1:first-child { + margin-left: 8.51063829787234%; + *margin-left: 8.404255319148938%; +} + +[class*="span"].hide, +.row-fluid [class*="span"].hide { + display: none; +} + +[class*="span"].pull-right, +.row-fluid [class*="span"].pull-right { + float: right; +} + +.container { + margin-right: auto; + margin-left: auto; + *zoom: 1; +} + +.container:before, +.container:after { + display: table; + line-height: 0; + content: ""; +} + +.container:after { + clear: both; +} + +.container-fluid { + padding-right: 20px; + padding-left: 20px; + *zoom: 1; +} + +.container-fluid:before, +.container-fluid:after { + display: table; + line-height: 0; + content: ""; +} + +.container-fluid:after { + clear: both; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 21px; + font-weight: 200; + line-height: 30px; +} + +small { + font-size: 85%; +} + +strong { + font-weight: bold; +} + +em { + font-style: italic; +} + +cite { + font-style: normal; +} + +.muted { + color: #999999; +} + +a.muted:hover, +a.muted:focus { + color: #808080; +} + +.text-warning { + color: #c09853; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #a47e3c; +} + +.text-error { + color: #b94a48; +} + +a.text-error:hover, +a.text-error:focus { + color: #953b39; +} + +.text-info { + color: #3a87ad; +} + +a.text-info:hover, +a.text-info:focus { + color: #2d6987; +} + +.text-success { + color: #468847; +} + +a.text-success:hover, +a.text-success:focus { + color: #356635; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 10px 0; + font-family: inherit; + font-weight: bold; + line-height: 20px; + color: inherit; + text-rendering: optimizelegibility; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + line-height: 40px; +} + +h1 { + font-size: 38.5px; +} + +h2 { + font-size: 31.5px; +} + +h3 { + font-size: 24.5px; +} + +h4 { + font-size: 17.5px; +} + +h5 { + font-size: 14px; +} + +h6 { + font-size: 11.9px; +} + +h1 small { + font-size: 24.5px; +} + +h2 small { + font-size: 17.5px; +} + +h3 small { + font-size: 14px; +} + +h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 20px 0 30px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + padding: 0; + margin: 0 0 10px 25px; +} + +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} + +li { + line-height: 20px; +} + +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} + +ul.inline, +ol.inline { + margin-left: 0; + list-style: none; +} + +ul.inline > li, +ol.inline > li { + display: inline-block; + *display: inline; + padding-right: 5px; + padding-left: 5px; + *zoom: 1; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 20px; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 10px; +} + +.dl-horizontal { + *zoom: 1; +} + +.dl-horizontal:before, +.dl-horizontal:after { + display: table; + line-height: 0; + content: ""; +} + +.dl-horizontal:after { + clear: both; +} + +.dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dl-horizontal dd { + margin-left: 180px; +} + +hr { + margin: 20px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 0 0 0 15px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + margin-bottom: 0; + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote small { + display: block; + line-height: 20px; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + float: right; + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 20px; +} + +code, +pre { + padding: 0 3px 2px; + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +code { + padding: 2px 4px; + color: #d14; + white-space: nowrap; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 20px; + word-break: break-all; + word-wrap: break-word; + white-space: pre; + white-space: pre-wrap; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + color: inherit; + white-space: pre; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +form { + margin: 0 0 20px; +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: 40px; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +legend small { + font-size: 15px; + color: #999999; +} + +label, +input, +button, +select, +textarea { + font-size: 14px; + font-weight: normal; + line-height: 20px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +label { + display: block; + margin-bottom: 5px; +} + +select, +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + display: inline-block; + height: 20px; + padding: 4px 6px; + margin-bottom: 10px; + font-size: 14px; + line-height: 20px; + color: #555555; + vertical-align: middle; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +input, +textarea, +.uneditable-input { + width: 206px; +} + +textarea { + height: auto; +} + +textarea, +input[type="text"], +input[type="password"], +input[type="datetime"], +input[type="datetime-local"], +input[type="date"], +input[type="month"], +input[type="time"], +input[type="week"], +input[type="number"], +input[type="email"], +input[type="url"], +input[type="search"], +input[type="tel"], +input[type="color"], +.uneditable-input { + background-color: #ffffff; + border: 1px solid #cccccc; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="datetime"]:focus, +input[type="datetime-local"]:focus, +input[type="date"]:focus, +input[type="month"]:focus, +input[type="time"]:focus, +input[type="week"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="url"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="color"]:focus, +.uneditable-input:focus { + border-color: rgba(82, 168, 236, 0.8); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + *margin-top: 0; + line-height: normal; +} + +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { + width: auto; +} + +select, +input[type="file"] { + height: 30px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 30px; +} + +select { + width: 220px; + background-color: #ffffff; + border: 1px solid #cccccc; +} + +select[multiple], +select[size] { + height: auto; +} + +select:focus, +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.uneditable-input, +.uneditable-textarea { + color: #999999; + cursor: not-allowed; + background-color: #fcfcfc; + border-color: #cccccc; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); +} + +.uneditable-input { + overflow: hidden; + white-space: nowrap; +} + +.uneditable-textarea { + width: auto; + height: auto; +} + +input:-moz-placeholder, +textarea:-moz-placeholder { + color: #999999; +} + +input:-ms-input-placeholder, +textarea:-ms-input-placeholder { + color: #999999; +} + +input::-webkit-input-placeholder, +textarea::-webkit-input-placeholder { + color: #999999; +} + +.radio, +.checkbox { + min-height: 20px; + padding-left: 20px; +} + +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} + +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} + +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} + +.input-mini { + width: 60px; +} + +.input-small { + width: 90px; +} + +.input-medium { + width: 150px; +} + +.input-large { + width: 210px; +} + +.input-xlarge { + width: 270px; +} + +.input-xxlarge { + width: 530px; +} + +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"] { + float: none; + margin-left: 0; +} + +.input-append input[class*="span"], +.input-append .uneditable-input[class*="span"], +.input-prepend input[class*="span"], +.input-prepend .uneditable-input[class*="span"], +.row-fluid input[class*="span"], +.row-fluid select[class*="span"], +.row-fluid textarea[class*="span"], +.row-fluid .uneditable-input[class*="span"], +.row-fluid .input-prepend [class*="span"], +.row-fluid .input-append [class*="span"] { + display: inline-block; +} + +input, +textarea, +.uneditable-input { + margin-left: 0; +} + +.controls-row [class*="span"] + [class*="span"] { + margin-left: 20px; +} + +input.span12, +textarea.span12, +.uneditable-input.span12 { + width: 926px; +} + +input.span11, +textarea.span11, +.uneditable-input.span11 { + width: 846px; +} + +input.span10, +textarea.span10, +.uneditable-input.span10 { + width: 766px; +} + +input.span9, +textarea.span9, +.uneditable-input.span9 { + width: 686px; +} + +input.span8, +textarea.span8, +.uneditable-input.span8 { + width: 606px; +} + +input.span7, +textarea.span7, +.uneditable-input.span7 { + width: 526px; +} + +input.span6, +textarea.span6, +.uneditable-input.span6 { + width: 446px; +} + +input.span5, +textarea.span5, +.uneditable-input.span5 { + width: 366px; +} + +input.span4, +textarea.span4, +.uneditable-input.span4 { + width: 286px; +} + +input.span3, +textarea.span3, +.uneditable-input.span3 { + width: 206px; +} + +input.span2, +textarea.span2, +.uneditable-input.span2 { + width: 126px; +} + +input.span1, +textarea.span1, +.uneditable-input.span1 { + width: 46px; +} + +.controls-row { + *zoom: 1; +} + +.controls-row:before, +.controls-row:after { + display: table; + line-height: 0; + content: ""; +} + +.controls-row:after { + clear: both; +} + +.controls-row [class*="span"], +.row-fluid .controls-row [class*="span"] { + float: left; +} + +.controls-row .checkbox[class*="span"], +.controls-row .radio[class*="span"] { + padding-top: 5px; +} + +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + cursor: not-allowed; + background-color: #eeeeee; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { + background-color: transparent; +} + +.control-group.warning .control-label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} + +.control-group.warning .checkbox, +.control-group.warning .radio, +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; +} + +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.control-group.error .control-label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} + +.control-group.error .checkbox, +.control-group.error .radio, +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; +} + +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.control-group.success .control-label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} + +.control-group.success .checkbox, +.control-group.success .radio, +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; +} + +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.control-group.info .control-label, +.control-group.info .help-block, +.control-group.info .help-inline { + color: #3a87ad; +} + +.control-group.info .checkbox, +.control-group.info .radio, +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + color: #3a87ad; +} + +.control-group.info input, +.control-group.info select, +.control-group.info textarea { + border-color: #3a87ad; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.control-group.info input:focus, +.control-group.info select:focus, +.control-group.info textarea:focus { + border-color: #2d6987; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3; +} + +.control-group.info .input-prepend .add-on, +.control-group.info .input-append .add-on { + color: #3a87ad; + background-color: #d9edf7; + border-color: #3a87ad; +} + +input:focus:invalid, +textarea:focus:invalid, +select:focus:invalid { + color: #b94a48; + border-color: #ee5f5b; +} + +input:focus:invalid:focus, +textarea:focus:invalid:focus, +select:focus:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} + +.form-actions { + padding: 19px 20px 20px; + margin-top: 20px; + margin-bottom: 20px; + background-color: #f5f5f5; + border-top: 1px solid #e5e5e5; + *zoom: 1; +} + +.form-actions:before, +.form-actions:after { + display: table; + line-height: 0; + content: ""; +} + +.form-actions:after { + clear: both; +} + +.help-block, +.help-inline { + color: #595959; +} + +.help-block { + display: block; + margin-bottom: 10px; +} + +.help-inline { + display: inline-block; + *display: inline; + padding-left: 5px; + vertical-align: middle; + *zoom: 1; +} + +.input-append, +.input-prepend { + display: inline-block; + margin-bottom: 10px; + font-size: 0; + white-space: nowrap; + vertical-align: middle; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input, +.input-append .dropdown-menu, +.input-prepend .dropdown-menu, +.input-append .popover, +.input-prepend .popover { + font-size: 14px; +} + +.input-append input, +.input-prepend input, +.input-append select, +.input-prepend select, +.input-append .uneditable-input, +.input-prepend .uneditable-input { + position: relative; + margin-bottom: 0; + *margin-left: 0; + vertical-align: top; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append input:focus, +.input-prepend input:focus, +.input-append select:focus, +.input-prepend select:focus, +.input-append .uneditable-input:focus, +.input-prepend .uneditable-input:focus { + z-index: 2; +} + +.input-append .add-on, +.input-prepend .add-on { + display: inline-block; + width: auto; + height: 20px; + min-width: 16px; + padding: 4px 5px; + font-size: 14px; + font-weight: normal; + line-height: 20px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #eeeeee; + border: 1px solid #ccc; +} + +.input-append .add-on, +.input-prepend .add-on, +.input-append .btn, +.input-prepend .btn, +.input-append .btn-group > .dropdown-toggle, +.input-prepend .btn-group > .dropdown-toggle { + vertical-align: top; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-append .active, +.input-prepend .active { + background-color: #a9dba9; + border-color: #46a546; +} + +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} + +.input-prepend .add-on:first-child, +.input-prepend .btn:first-child { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input, +.input-append select, +.input-append .uneditable-input { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-append input + .btn-group .btn:last-child, +.input-append select + .btn-group .btn:last-child, +.input-append .uneditable-input + .btn-group .btn:last-child { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-append .add-on, +.input-append .btn, +.input-append .btn-group { + margin-left: -1px; +} + +.input-append .add-on:last-child, +.input-append .btn:last-child, +.input-append .btn-group:last-child > .dropdown-toggle { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.input-prepend.input-append input + .btn-group .btn, +.input-prepend.input-append select + .btn-group .btn, +.input-prepend.input-append .uneditable-input + .btn-group .btn { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.input-prepend.input-append .btn-group:first-child { + margin-left: 0; +} + +input.search-query { + padding-right: 14px; + padding-right: 4px \9; + padding-left: 14px; + padding-left: 4px \9; + /* IE7-8 doesn't have border-radius, so don't indent the padding */ + + margin-bottom: 0; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +/* Allow for input prepend/append in search forms */ + +.form-search .input-append .search-query, +.form-search .input-prepend .search-query { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.form-search .input-append .search-query { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search .input-append .btn { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .search-query { + -webkit-border-radius: 0 14px 14px 0; + -moz-border-radius: 0 14px 14px 0; + border-radius: 0 14px 14px 0; +} + +.form-search .input-prepend .btn { + -webkit-border-radius: 14px 0 0 14px; + -moz-border-radius: 14px 0 0 14px; + border-radius: 14px 0 0 14px; +} + +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + *display: inline; + margin-bottom: 0; + vertical-align: middle; + *zoom: 1; +} + +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} + +.form-search label, +.form-inline label, +.form-search .btn-group, +.form-inline .btn-group { + display: inline-block; +} + +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} + +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} + +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-right: 3px; + margin-left: 0; +} + +.control-group { + margin-bottom: 10px; +} + +legend + .control-group { + margin-top: 20px; + -webkit-margin-top-collapse: separate; +} + +.form-horizontal .control-group { + margin-bottom: 20px; + *zoom: 1; +} + +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + line-height: 0; + content: ""; +} + +.form-horizontal .control-group:after { + clear: both; +} + +.form-horizontal .control-label { + float: left; + width: 160px; + padding-top: 5px; + text-align: right; +} + +.form-horizontal .controls { + *display: inline-block; + *padding-left: 20px; + margin-left: 180px; + *margin-left: 0; +} + +.form-horizontal .controls:first-child { + *padding-left: 180px; +} + +.form-horizontal .help-block { + margin-bottom: 0; +} + +.form-horizontal input + .help-block, +.form-horizontal select + .help-block, +.form-horizontal textarea + .help-block, +.form-horizontal .uneditable-input + .help-block, +.form-horizontal .input-prepend + .help-block, +.form-horizontal .input-append + .help-block { + margin-top: 10px; +} + +.form-horizontal .form-actions { + padding-left: 180px; +} + +table { + max-width: 100%; + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table th, +.table td { + padding: 8px; + line-height: 20px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table th { + font-weight: bold; +} + +.table thead th { + vertical-align: bottom; +} + +.table caption + thead tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} + +.table-bordered { + border: 1px solid #dddddd; + border-collapse: separate; + *border-collapse: collapse; + border-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} + +.table-bordered caption + thead tr:first-child th, +.table-bordered caption + tbody tr:first-child th, +.table-bordered caption + tbody tr:first-child td, +.table-bordered colgroup + thead tr:first-child th, +.table-bordered colgroup + tbody tr:first-child th, +.table-bordered colgroup + tbody tr:first-child td, +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} + +.table-bordered thead:first-child tr:first-child > th:first-child, +.table-bordered tbody:first-child tr:first-child > td:first-child, +.table-bordered tbody:first-child tr:first-child > th:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered thead:first-child tr:first-child > th:last-child, +.table-bordered tbody:first-child tr:first-child > td:last-child, +.table-bordered tbody:first-child tr:first-child > th:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:first-child, +.table-bordered tbody:last-child tr:last-child > td:first-child, +.table-bordered tbody:last-child tr:last-child > th:first-child, +.table-bordered tfoot:last-child tr:last-child > td:first-child, +.table-bordered tfoot:last-child tr:last-child > th:first-child { + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.table-bordered thead:last-child tr:last-child > th:last-child, +.table-bordered tbody:last-child tr:last-child > td:last-child, +.table-bordered tbody:last-child tr:last-child > th:last-child, +.table-bordered tfoot:last-child tr:last-child > td:last-child, +.table-bordered tfoot:last-child tr:last-child > th:last-child { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:first-child { + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; + -moz-border-radius-bottomleft: 0; +} + +.table-bordered tfoot + tbody:last-child tr:last-child td:last-child { + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; + -moz-border-radius-bottomright: 0; +} + +.table-bordered caption + thead tr:first-child th:first-child, +.table-bordered caption + tbody tr:first-child td:first-child, +.table-bordered colgroup + thead tr:first-child th:first-child, +.table-bordered colgroup + tbody tr:first-child td:first-child { + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; +} + +.table-bordered caption + thead tr:first-child th:last-child, +.table-bordered caption + tbody tr:first-child td:last-child, +.table-bordered colgroup + thead tr:first-child th:last-child, +.table-bordered colgroup + tbody tr:first-child td:last-child { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; +} + +.table-striped tbody > tr:nth-child(odd) > td, +.table-striped tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover tbody tr:hover > td, +.table-hover tbody tr:hover > th { + background-color: #f5f5f5; +} + +table td[class*="span"], +table th[class*="span"], +.row-fluid table td[class*="span"], +.row-fluid table th[class*="span"] { + display: table-cell; + float: none; + margin-left: 0; +} + +.table td.span1, +.table th.span1 { + float: none; + width: 44px; + margin-left: 0; +} + +.table td.span2, +.table th.span2 { + float: none; + width: 124px; + margin-left: 0; +} + +.table td.span3, +.table th.span3 { + float: none; + width: 204px; + margin-left: 0; +} + +.table td.span4, +.table th.span4 { + float: none; + width: 284px; + margin-left: 0; +} + +.table td.span5, +.table th.span5 { + float: none; + width: 364px; + margin-left: 0; +} + +.table td.span6, +.table th.span6 { + float: none; + width: 444px; + margin-left: 0; +} + +.table td.span7, +.table th.span7 { + float: none; + width: 524px; + margin-left: 0; +} + +.table td.span8, +.table th.span8 { + float: none; + width: 604px; + margin-left: 0; +} + +.table td.span9, +.table th.span9 { + float: none; + width: 684px; + margin-left: 0; +} + +.table td.span10, +.table th.span10 { + float: none; + width: 764px; + margin-left: 0; +} + +.table td.span11, +.table th.span11 { + float: none; + width: 844px; + margin-left: 0; +} + +.table td.span12, +.table th.span12 { + float: none; + width: 924px; + margin-left: 0; +} + +.table tbody tr.success > td { + background-color: #dff0d8; +} + +.table tbody tr.error > td { + background-color: #f2dede; +} + +.table tbody tr.warning > td { + background-color: #fcf8e3; +} + +.table tbody tr.info > td { + background-color: #d9edf7; +} + +.table-hover tbody tr.success:hover > td { + background-color: #d0e9c6; +} + +.table-hover tbody tr.error:hover > td { + background-color: #ebcccc; +} + +.table-hover tbody tr.warning:hover > td { + background-color: #faf2cc; +} + +.table-hover tbody tr.info:hover > td { + background-color: #c4e3f3; +} + +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + margin-top: 1px; + *margin-right: .3em; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; +} + +/* White icons with optional class, or on hover/focus/active states of certain elements */ + +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { + background-image: url("../img/glyphicons-halflings-white.png"); +} + +.icon-glass { + background-position: 0 0; +} + +.icon-music { + background-position: -24px 0; +} + +.icon-search { + background-position: -48px 0; +} + +.icon-envelope { + background-position: -72px 0; +} + +.icon-heart { + background-position: -96px 0; +} + +.icon-star { + background-position: -120px 0; +} + +.icon-star-empty { + background-position: -144px 0; +} + +.icon-user { + background-position: -168px 0; +} + +.icon-film { + background-position: -192px 0; +} + +.icon-th-large { + background-position: -216px 0; +} + +.icon-th { + background-position: -240px 0; +} + +.icon-th-list { + background-position: -264px 0; +} + +.icon-ok { + background-position: -288px 0; +} + +.icon-remove { + background-position: -312px 0; +} + +.icon-zoom-in { + background-position: -336px 0; +} + +.icon-zoom-out { + background-position: -360px 0; +} + +.icon-off { + background-position: -384px 0; +} + +.icon-signal { + background-position: -408px 0; +} + +.icon-cog { + background-position: -432px 0; +} + +.icon-trash { + background-position: -456px 0; +} + +.icon-home { + background-position: 0 -24px; +} + +.icon-file { + background-position: -24px -24px; +} + +.icon-time { + background-position: -48px -24px; +} + +.icon-road { + background-position: -72px -24px; +} + +.icon-download-alt { + background-position: -96px -24px; +} + +.icon-download { + background-position: -120px -24px; +} + +.icon-upload { + background-position: -144px -24px; +} + +.icon-inbox { + background-position: -168px -24px; +} + +.icon-play-circle { + background-position: -192px -24px; +} + +.icon-repeat { + background-position: -216px -24px; +} + +.icon-refresh { + background-position: -240px -24px; +} + +.icon-list-alt { + background-position: -264px -24px; +} + +.icon-lock { + background-position: -287px -24px; +} + +.icon-flag { + background-position: -312px -24px; +} + +.icon-headphones { + background-position: -336px -24px; +} + +.icon-volume-off { + background-position: -360px -24px; +} + +.icon-volume-down { + background-position: -384px -24px; +} + +.icon-volume-up { + background-position: -408px -24px; +} + +.icon-qrcode { + background-position: -432px -24px; +} + +.icon-barcode { + background-position: -456px -24px; +} + +.icon-tag { + background-position: 0 -48px; +} + +.icon-tags { + background-position: -25px -48px; +} + +.icon-book { + background-position: -48px -48px; +} + +.icon-bookmark { + background-position: -72px -48px; +} + +.icon-print { + background-position: -96px -48px; +} + +.icon-camera { + background-position: -120px -48px; +} + +.icon-font { + background-position: -144px -48px; +} + +.icon-bold { + background-position: -167px -48px; +} + +.icon-italic { + background-position: -192px -48px; +} + +.icon-text-height { + background-position: -216px -48px; +} + +.icon-text-width { + background-position: -240px -48px; +} + +.icon-align-left { + background-position: -264px -48px; +} + +.icon-align-center { + background-position: -288px -48px; +} + +.icon-align-right { + background-position: -312px -48px; +} + +.icon-align-justify { + background-position: -336px -48px; +} + +.icon-list { + background-position: -360px -48px; +} + +.icon-indent-left { + background-position: -384px -48px; +} + +.icon-indent-right { + background-position: -408px -48px; +} + +.icon-facetime-video { + background-position: -432px -48px; +} + +.icon-picture { + background-position: -456px -48px; +} + +.icon-pencil { + background-position: 0 -72px; +} + +.icon-map-marker { + background-position: -24px -72px; +} + +.icon-adjust { + background-position: -48px -72px; +} + +.icon-tint { + background-position: -72px -72px; +} + +.icon-edit { + background-position: -96px -72px; +} + +.icon-share { + background-position: -120px -72px; +} + +.icon-check { + background-position: -144px -72px; +} + +.icon-move { + background-position: -168px -72px; +} + +.icon-step-backward { + background-position: -192px -72px; +} + +.icon-fast-backward { + background-position: -216px -72px; +} + +.icon-backward { + background-position: -240px -72px; +} + +.icon-play { + background-position: -264px -72px; +} + +.icon-pause { + background-position: -288px -72px; +} + +.icon-stop { + background-position: -312px -72px; +} + +.icon-forward { + background-position: -336px -72px; +} + +.icon-fast-forward { + background-position: -360px -72px; +} + +.icon-step-forward { + background-position: -384px -72px; +} + +.icon-eject { + background-position: -408px -72px; +} + +.icon-chevron-left { + background-position: -432px -72px; +} + +.icon-chevron-right { + background-position: -456px -72px; +} + +.icon-plus-sign { + background-position: 0 -96px; +} + +.icon-minus-sign { + background-position: -24px -96px; +} + +.icon-remove-sign { + background-position: -48px -96px; +} + +.icon-ok-sign { + background-position: -72px -96px; +} + +.icon-question-sign { + background-position: -96px -96px; +} + +.icon-info-sign { + background-position: -120px -96px; +} + +.icon-screenshot { + background-position: -144px -96px; +} + +.icon-remove-circle { + background-position: -168px -96px; +} + +.icon-ok-circle { + background-position: -192px -96px; +} + +.icon-ban-circle { + background-position: -216px -96px; +} + +.icon-arrow-left { + background-position: -240px -96px; +} + +.icon-arrow-right { + background-position: -264px -96px; +} + +.icon-arrow-up { + background-position: -289px -96px; +} + +.icon-arrow-down { + background-position: -312px -96px; +} + +.icon-share-alt { + background-position: -336px -96px; +} + +.icon-resize-full { + background-position: -360px -96px; +} + +.icon-resize-small { + background-position: -384px -96px; +} + +.icon-plus { + background-position: -408px -96px; +} + +.icon-minus { + background-position: -433px -96px; +} + +.icon-asterisk { + background-position: -456px -96px; +} + +.icon-exclamation-sign { + background-position: 0 -120px; +} + +.icon-gift { + background-position: -24px -120px; +} + +.icon-leaf { + background-position: -48px -120px; +} + +.icon-fire { + background-position: -72px -120px; +} + +.icon-eye-open { + background-position: -96px -120px; +} + +.icon-eye-close { + background-position: -120px -120px; +} + +.icon-warning-sign { + background-position: -144px -120px; +} + +.icon-plane { + background-position: -168px -120px; +} + +.icon-calendar { + background-position: -192px -120px; +} + +.icon-random { + width: 16px; + background-position: -216px -120px; +} + +.icon-comment { + background-position: -240px -120px; +} + +.icon-magnet { + background-position: -264px -120px; +} + +.icon-chevron-up { + background-position: -288px -120px; +} + +.icon-chevron-down { + background-position: -313px -119px; +} + +.icon-retweet { + background-position: -336px -120px; +} + +.icon-shopping-cart { + background-position: -360px -120px; +} + +.icon-folder-close { + width: 16px; + background-position: -384px -120px; +} + +.icon-folder-open { + width: 16px; + background-position: -408px -120px; +} + +.icon-resize-vertical { + background-position: -432px -119px; +} + +.icon-resize-horizontal { + background-position: -456px -118px; +} + +.icon-hdd { + background-position: 0 -144px; +} + +.icon-bullhorn { + background-position: -24px -144px; +} + +.icon-bell { + background-position: -48px -144px; +} + +.icon-certificate { + background-position: -72px -144px; +} + +.icon-thumbs-up { + background-position: -96px -144px; +} + +.icon-thumbs-down { + background-position: -120px -144px; +} + +.icon-hand-right { + background-position: -144px -144px; +} + +.icon-hand-left { + background-position: -168px -144px; +} + +.icon-hand-up { + background-position: -192px -144px; +} + +.icon-hand-down { + background-position: -216px -144px; +} + +.icon-circle-arrow-right { + background-position: -240px -144px; +} + +.icon-circle-arrow-left { + background-position: -264px -144px; +} + +.icon-circle-arrow-up { + background-position: -288px -144px; +} + +.icon-circle-arrow-down { + background-position: -312px -144px; +} + +.icon-globe { + background-position: -336px -144px; +} + +.icon-wrench { + background-position: -360px -144px; +} + +.icon-tasks { + background-position: -384px -144px; +} + +.icon-filter { + background-position: -408px -144px; +} + +.icon-briefcase { + background-position: -432px -144px; +} + +.icon-fullscreen { + background-position: -456px -144px; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle { + *margin-bottom: -3px; +} + +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 20px; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + outline: 0; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0); +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open { + *z-index: 1000; +} + +.open > .dropdown-menu { + display: block; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu > .dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover > .dropdown-menu { + display: block; +} + +.dropup .dropdown-submenu > .dropdown-menu { + top: auto; + bottom: 0; + margin-top: 0; + margin-bottom: -2px; + -webkit-border-radius: 5px 5px 5px 0; + -moz-border-radius: 5px 5px 5px 0; + border-radius: 5px 5px 5px 0; +} + +.dropdown-submenu > a:after { + display: block; + float: right; + width: 0; + height: 0; + margin-top: 5px; + margin-right: -10px; + border-color: transparent; + border-left-color: #cccccc; + border-style: solid; + border-width: 5px 0 5px 5px; + content: " "; +} + +.dropdown-submenu:hover > a:after { + border-left-color: #ffffff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left > .dropdown-menu { + left: -100%; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.dropdown .dropdown-menu .nav-header { + padding-right: 20px; + padding-left: 20px; +} + +.typeahead { + z-index: 1051; + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +.collapse.in { + height: auto; +} + +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 20px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.btn { + display: inline-block; + *display: inline; + padding: 4px 12px; + margin-bottom: 0; + *margin-left: .3em; + font-size: 14px; + line-height: 20px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + cursor: pointer; + background-color: #f5f5f5; + *background-color: #e6e6e6; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border: 1px solid #cccccc; + *border: 0; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn:hover, +.btn:focus, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + color: #333333; + background-color: #e6e6e6; + *background-color: #d9d9d9; +} + +.btn:active, +.btn.active { + background-color: #cccccc \9; +} + +.btn:first-child { + *margin-left: 0; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn.active, +.btn:active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-large { + padding: 11px 19px; + font-size: 17.5px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.btn-large [class^="icon-"], +.btn-large [class*=" icon-"] { + margin-top: 4px; +} + +.btn-small { + padding: 2px 10px; + font-size: 11.9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-small [class^="icon-"], +.btn-small [class*=" icon-"] { + margin-top: 0; +} + +.btn-mini [class^="icon-"], +.btn-mini [class*=" icon-"] { + margin-top: -1px; +} + +.btn-mini { + padding: 0 6px; + font-size: 10.5px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} + +.btn-primary { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #006dcc; + *background-color: #0044cc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(to bottom, #0088cc, #0044cc); + background-repeat: repeat-x; + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + color: #ffffff; + background-color: #0044cc; + *background-color: #003bb3; +} + +.btn-primary:active, +.btn-primary.active { + background-color: #003399 \9; +} + +.btn-warning { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #faa732; + *background-color: #f89406; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + color: #ffffff; + background-color: #f89406; + *background-color: #df8505; +} + +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} + +.btn-danger { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #da4f49; + *background-color: #bd362f; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); + background-repeat: repeat-x; + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + color: #ffffff; + background-color: #bd362f; + *background-color: #a9302a; +} + +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} + +.btn-success { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #5bb75b; + *background-color: #51a351; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(to bottom, #62c462, #51a351); + background-repeat: repeat-x; + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + color: #ffffff; + background-color: #51a351; + *background-color: #499249; +} + +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} + +.btn-info { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #49afcd; + *background-color: #2f96b4; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); + background-repeat: repeat-x; + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + color: #ffffff; + background-color: #2f96b4; + *background-color: #2a85a0; +} + +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} + +.btn-inverse { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #363636; + *background-color: #222222; + background-image: -moz-linear-gradient(top, #444444, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); + background-image: -webkit-linear-gradient(top, #444444, #222222); + background-image: -o-linear-gradient(top, #444444, #222222); + background-image: linear-gradient(to bottom, #444444, #222222); + background-repeat: repeat-x; + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.btn-inverse:hover, +.btn-inverse:focus, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + color: #ffffff; + background-color: #222222; + *background-color: #151515; +} + +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} + +button.btn, +input[type="submit"].btn { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} + +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} + +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} + +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled] { + background-color: transparent; + background-image: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} + +.btn-link { + color: #0088cc; + cursor: pointer; + border-color: transparent; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-link:hover, +.btn-link:focus { + color: #005580; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +.btn-link[disabled]:focus { + color: #333333; + text-decoration: none; +} + +.btn-group { + position: relative; + display: inline-block; + *display: inline; + *margin-left: .3em; + font-size: 0; + white-space: nowrap; + vertical-align: middle; + *zoom: 1; +} + +.btn-group:first-child { + *margin-left: 0; +} + +.btn-group + .btn-group { + margin-left: 5px; +} + +.btn-toolbar { + margin-top: 10px; + margin-bottom: 10px; + font-size: 0; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn { + position: relative; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group > .btn + .btn { + margin-left: -1px; +} + +.btn-group > .btn, +.btn-group > .dropdown-menu, +.btn-group > .popover { + font-size: 14px; +} + +.btn-group > .btn-mini { + font-size: 10.5px; +} + +.btn-group > .btn-small { + font-size: 11.9px; +} + +.btn-group > .btn-large { + font-size: 17.5px; +} + +.btn-group > .btn:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.btn-group > .btn.large:first-child { + margin-left: 0; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.btn-group > .btn.large:last-child, +.btn-group > .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.btn-group > .btn:hover, +.btn-group > .btn:focus, +.btn-group > .btn:active, +.btn-group > .btn.active { + z-index: 2; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + *padding-top: 5px; + padding-right: 8px; + *padding-bottom: 5px; + padding-left: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group > .btn-mini + .dropdown-toggle { + *padding-top: 2px; + padding-right: 5px; + *padding-bottom: 2px; + padding-left: 5px; +} + +.btn-group > .btn-small + .dropdown-toggle { + *padding-top: 5px; + *padding-bottom: 4px; +} + +.btn-group > .btn-large + .dropdown-toggle { + *padding-top: 7px; + padding-right: 12px; + *padding-bottom: 7px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.btn-group.open .btn.dropdown-toggle { + background-color: #e6e6e6; +} + +.btn-group.open .btn-primary.dropdown-toggle { + background-color: #0044cc; +} + +.btn-group.open .btn-warning.dropdown-toggle { + background-color: #f89406; +} + +.btn-group.open .btn-danger.dropdown-toggle { + background-color: #bd362f; +} + +.btn-group.open .btn-success.dropdown-toggle { + background-color: #51a351; +} + +.btn-group.open .btn-info.dropdown-toggle { + background-color: #2f96b4; +} + +.btn-group.open .btn-inverse.dropdown-toggle { + background-color: #222222; +} + +.btn .caret { + margin-top: 8px; + margin-left: 0; +} + +.btn-large .caret { + margin-top: 6px; +} + +.btn-large .caret { + border-top-width: 5px; + border-right-width: 5px; + border-left-width: 5px; +} + +.btn-mini .caret, +.btn-small .caret { + margin-top: 8px; +} + +.dropup .btn-large .caret { + border-bottom-width: 5px; +} + +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.btn-group-vertical { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} + +.btn-group-vertical > .btn { + display: block; + float: none; + max-width: 100%; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.btn-group-vertical > .btn + .btn { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:first-child { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.btn-group-vertical > .btn:last-child { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.btn-group-vertical > .btn-large:first-child { + -webkit-border-radius: 6px 6px 0 0; + -moz-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; +} + +.btn-group-vertical > .btn-large:last-child { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 20px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.alert, +.alert h4 { + color: #c09853; +} + +.alert h4 { + margin: 0; +} + +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 20px; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success h4 { + color: #468847; +} + +.alert-danger, +.alert-error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger h4, +.alert-error h4 { + color: #b94a48; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info h4 { + color: #3a87ad; +} + +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} + +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} + +.alert-block p + p { + margin-top: 5px; +} + +.nav { + margin-bottom: 20px; + margin-left: 0; + list-style: none; +} + +.nav > li > a { + display: block; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li > a > img { + max-width: none; +} + +.nav > .pull-right { + float: right; +} + +.nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.nav li + .nav-header { + margin-top: 9px; +} + +.nav-list { + padding-right: 15px; + padding-left: 15px; + margin-bottom: 0; +} + +.nav-list > li > a, +.nav-list .nav-header { + margin-right: -15px; + margin-left: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.nav-list > li > a { + padding: 3px 15px; +} + +.nav-list > .active > a, +.nav-list > .active > a:hover, +.nav-list > .active > a:focus { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} + +.nav-list [class^="icon-"], +.nav-list [class*=" icon-"] { + margin-right: 2px; +} + +.nav-list .divider { + *width: 100%; + height: 1px; + margin: 9px 1px; + *margin: -5px 0 5px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; +} + +.nav-tabs, +.nav-pills { + *zoom: 1; +} + +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + line-height: 0; + content: ""; +} + +.nav-tabs:after, +.nav-pills:after { + clear: both; +} + +.nav-tabs > li, +.nav-pills > li { + float: left; +} + +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + margin-bottom: -1px; +} + +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 20px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover, +.nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover, +.nav-tabs > .active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.nav-pills > .active > a, +.nav-pills > .active > a:hover, +.nav-pills > .active > a:focus { + color: #ffffff; + background-color: #0088cc; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li > a { + margin-right: 0; +} + +.nav-tabs.nav-stacked { + border-bottom: 0; +} + +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-topleft: 4px; +} + +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -moz-border-radius-bottomright: 4px; + -moz-border-radius-bottomleft: 4px; +} + +.nav-tabs.nav-stacked > li > a:hover, +.nav-tabs.nav-stacked > li > a:focus { + z-index: 2; + border-color: #ddd; +} + +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} + +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} + +.nav-tabs .dropdown-menu { + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; +} + +.nav-pills .dropdown-menu { + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.nav .dropdown-toggle .caret { + margin-top: 6px; + border-top-color: #0088cc; + border-bottom-color: #0088cc; +} + +.nav .dropdown-toggle:hover .caret, +.nav .dropdown-toggle:focus .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} + +/* move down carets for tabs */ + +.nav-tabs .dropdown-toggle .caret { + margin-top: 8px; +} + +.nav .active .dropdown-toggle .caret { + border-top-color: #fff; + border-bottom-color: #fff; +} + +.nav-tabs .active .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.nav > .dropdown.active > a:hover, +.nav > .dropdown.active > a:focus { + cursor: pointer; +} + +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > li.dropdown.open.active > a:hover, +.nav > li.dropdown.open.active > a:focus { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} + +.nav li.dropdown.open .caret, +.nav li.dropdown.open.active .caret, +.nav li.dropdown.open a:hover .caret, +.nav li.dropdown.open a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} + +.tabs-stacked .open > a:hover, +.tabs-stacked .open > a:focus { + border-color: #999999; +} + +.tabbable { + *zoom: 1; +} + +.tabbable:before, +.tabbable:after { + display: table; + line-height: 0; + content: ""; +} + +.tabbable:after { + clear: both; +} + +.tab-content { + overflow: auto; +} + +.tabs-below > .nav-tabs, +.tabs-right > .nav-tabs, +.tabs-left > .nav-tabs { + border-bottom: 0; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.tabs-below > .nav-tabs { + border-top: 1px solid #ddd; +} + +.tabs-below > .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} + +.tabs-below > .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} + +.tabs-below > .nav-tabs > li > a:hover, +.tabs-below > .nav-tabs > li > a:focus { + border-top-color: #ddd; + border-bottom-color: transparent; +} + +.tabs-below > .nav-tabs > .active > a, +.tabs-below > .nav-tabs > .active > a:hover, +.tabs-below > .nav-tabs > .active > a:focus { + border-color: transparent #ddd #ddd #ddd; +} + +.tabs-left > .nav-tabs > li, +.tabs-right > .nav-tabs > li { + float: none; +} + +.tabs-left > .nav-tabs > li > a, +.tabs-right > .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} + +.tabs-left > .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} + +.tabs-left > .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.tabs-left > .nav-tabs > li > a:hover, +.tabs-left > .nav-tabs > li > a:focus { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} + +.tabs-left > .nav-tabs .active > a, +.tabs-left > .nav-tabs .active > a:hover, +.tabs-left > .nav-tabs .active > a:focus { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} + +.tabs-right > .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} + +.tabs-right > .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.tabs-right > .nav-tabs > li > a:hover, +.tabs-right > .nav-tabs > li > a:focus { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} + +.tabs-right > .nav-tabs .active > a, +.tabs-right > .nav-tabs .active > a:hover, +.tabs-right > .nav-tabs .active > a:focus { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} + +.nav > .disabled > a { + color: #999999; +} + +.nav > .disabled > a:hover, +.nav > .disabled > a:focus { + text-decoration: none; + cursor: default; + background-color: transparent; +} + +.navbar { + *position: relative; + *z-index: 2; + margin-bottom: 20px; + overflow: visible; +} + +.navbar-inner { + min-height: 40px; + padding-right: 20px; + padding-left: 20px; + background-color: #fafafa; + background-image: -moz-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2)); + background-image: -webkit-linear-gradient(top, #ffffff, #f2f2f2); + background-image: -o-linear-gradient(top, #ffffff, #f2f2f2); + background-image: linear-gradient(to bottom, #ffffff, #f2f2f2); + background-repeat: repeat-x; + border: 1px solid #d4d4d4; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); + *zoom: 1; + -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.065); +} + +.navbar-inner:before, +.navbar-inner:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-inner:after { + clear: both; +} + +.navbar .container { + width: auto; +} + +.nav-collapse.collapse { + height: auto; + overflow: visible; +} + +.navbar .brand { + display: block; + float: left; + padding: 10px 20px 10px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + color: #777777; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .brand:hover, +.navbar .brand:focus { + text-decoration: none; +} + +.navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #777777; +} + +.navbar-link { + color: #777777; +} + +.navbar-link:hover, +.navbar-link:focus { + color: #333333; +} + +.navbar .divider-vertical { + height: 40px; + margin: 0 9px; + border-right: 1px solid #ffffff; + border-left: 1px solid #f2f2f2; +} + +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} + +.navbar .btn-group .btn, +.navbar .input-prepend .btn, +.navbar .input-append .btn, +.navbar .input-prepend .btn-group, +.navbar .input-append .btn-group { + margin-top: 0; +} + +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} + +.navbar-form:before, +.navbar-form:after { + display: table; + line-height: 0; + content: ""; +} + +.navbar-form:after { + clear: both; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} + +.navbar-form input, +.navbar-form select, +.navbar-form .btn { + display: inline-block; + margin-bottom: 0; +} + +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} + +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 5px; + white-space: nowrap; +} + +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} + +.navbar-search { + position: relative; + float: left; + margin-top: 5px; + margin-bottom: 0; +} + +.navbar-search .search-query { + padding: 4px 14px; + margin-bottom: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.navbar-static-top { + position: static; + margin-bottom: 0; +} + +.navbar-static-top .navbar-inner { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + border-width: 0 0 1px; +} + +.navbar-fixed-bottom .navbar-inner { + border-width: 1px 0 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-right: 0; + padding-left: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.navbar-static-top .container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} + +.navbar-fixed-top { + top: 0; +} + +.navbar-fixed-top .navbar-inner, +.navbar-static-top .navbar-inner { + -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar-fixed-bottom { + bottom: 0; +} + +.navbar-fixed-bottom .navbar-inner { + -webkit-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0 -1px 10px rgba(0, 0, 0, 0.1); +} + +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} + +.navbar .nav.pull-right { + float: right; + margin-right: 0; +} + +.navbar .nav > li { + float: left; +} + +.navbar .nav > li > a { + float: none; + padding: 10px 15px 10px; + color: #777777; + text-decoration: none; + text-shadow: 0 1px 0 #ffffff; +} + +.navbar .nav .dropdown-toggle .caret { + margin-top: 8px; +} + +.navbar .nav > li > a:focus, +.navbar .nav > li > a:hover { + color: #333333; + text-decoration: none; + background-color: transparent; +} + +.navbar .nav > .active > a, +.navbar .nav > .active > a:hover, +.navbar .nav > .active > a:focus { + color: #555555; + text-decoration: none; + background-color: #e5e5e5; + -webkit-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.125); +} + +.navbar .btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-right: 5px; + margin-left: 5px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #ededed; + *background-color: #e5e5e5; + background-image: -moz-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5)); + background-image: -webkit-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: -o-linear-gradient(top, #f2f2f2, #e5e5e5); + background-image: linear-gradient(to bottom, #f2f2f2, #e5e5e5); + background-repeat: repeat-x; + border-color: #e5e5e5 #e5e5e5 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} + +.navbar .btn-navbar:hover, +.navbar .btn-navbar:focus, +.navbar .btn-navbar:active, +.navbar .btn-navbar.active, +.navbar .btn-navbar.disabled, +.navbar .btn-navbar[disabled] { + color: #ffffff; + background-color: #e5e5e5; + *background-color: #d9d9d9; +} + +.navbar .btn-navbar:active, +.navbar .btn-navbar.active { + background-color: #cccccc \9; +} + +.navbar .btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} + +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} + +.navbar .nav > li > .dropdown-menu:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.navbar .nav > li > .dropdown-menu:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + border-left: 6px solid transparent; + content: ''; +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:before { + top: auto; + bottom: -7px; + border-top: 7px solid #ccc; + border-bottom: 0; + border-top-color: rgba(0, 0, 0, 0.2); +} + +.navbar-fixed-bottom .nav > li > .dropdown-menu:after { + top: auto; + bottom: -6px; + border-top: 6px solid #ffffff; + border-bottom: 0; +} + +.navbar .nav li.dropdown > a:hover .caret, +.navbar .nav li.dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle, +.navbar .nav li.dropdown.active > .dropdown-toggle, +.navbar .nav li.dropdown.open.active > .dropdown-toggle { + color: #555555; + background-color: #e5e5e5; +} + +.navbar .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +.navbar .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar .pull-right > li > .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:before, +.navbar .nav > li > .dropdown-menu.pull-right:before { + right: 12px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu:after, +.navbar .nav > li > .dropdown-menu.pull-right:after { + right: 13px; + left: auto; +} + +.navbar .pull-right > li > .dropdown-menu .dropdown-menu, +.navbar .nav > li > .dropdown-menu.pull-right .dropdown-menu { + right: 100%; + left: auto; + margin-right: -1px; + margin-left: 0; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + +.navbar-inverse .navbar-inner { + background-color: #1b1b1b; + background-image: -moz-linear-gradient(top, #222222, #111111); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); + background-image: -webkit-linear-gradient(top, #222222, #111111); + background-image: -o-linear-gradient(top, #222222, #111111); + background-image: linear-gradient(to bottom, #222222, #111111); + background-repeat: repeat-x; + border-color: #252525; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); +} + +.navbar-inverse .brand, +.navbar-inverse .nav > li > a { + color: #999999; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.navbar-inverse .brand:hover, +.navbar-inverse .nav > li > a:hover, +.navbar-inverse .brand:focus, +.navbar-inverse .nav > li > a:focus { + color: #ffffff; +} + +.navbar-inverse .brand { + color: #999999; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .nav > li > a:focus, +.navbar-inverse .nav > li > a:hover { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .nav .active > a, +.navbar-inverse .nav .active > a:hover, +.navbar-inverse .nav .active > a:focus { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover, +.navbar-inverse .navbar-link:focus { + color: #ffffff; +} + +.navbar-inverse .divider-vertical { + border-right-color: #222222; + border-left-color: #111111; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle { + color: #ffffff; + background-color: #111111; +} + +.navbar-inverse .nav li.dropdown > a:hover .caret, +.navbar-inverse .nav li.dropdown > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .nav li.dropdown > .dropdown-toggle .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .nav li.dropdown.open > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.active > .dropdown-toggle .caret, +.navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-search .search-query { + color: #ffffff; + background-color: #515151; + border-color: #111111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; +} + +.navbar-inverse .navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:-ms-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} + +.navbar-inverse .navbar-search .search-query:focus, +.navbar-inverse .navbar-search .search-query.focused { + padding: 5px 15px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + outline: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); +} + +.navbar-inverse .btn-navbar { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e0e0e; + *background-color: #040404; + background-image: -moz-linear-gradient(top, #151515, #040404); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404)); + background-image: -webkit-linear-gradient(top, #151515, #040404); + background-image: -o-linear-gradient(top, #151515, #040404); + background-image: linear-gradient(to bottom, #151515, #040404); + background-repeat: repeat-x; + border-color: #040404 #040404 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.navbar-inverse .btn-navbar:hover, +.navbar-inverse .btn-navbar:focus, +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active, +.navbar-inverse .btn-navbar.disabled, +.navbar-inverse .btn-navbar[disabled] { + color: #ffffff; + background-color: #040404; + *background-color: #000000; +} + +.navbar-inverse .btn-navbar:active, +.navbar-inverse .btn-navbar.active { + background-color: #000000 \9; +} + +.breadcrumb { + padding: 8px 15px; + margin: 0 0 20px; + list-style: none; + background-color: #f5f5f5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; + *display: inline; + text-shadow: 0 1px 0 #ffffff; + *zoom: 1; +} + +.breadcrumb > li > .divider { + padding: 0 5px; + color: #ccc; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + margin: 20px 0; +} + +.pagination ul { + display: inline-block; + *display: inline; + margin-bottom: 0; + margin-left: 0; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + *zoom: 1; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.pagination ul > li { + display: inline; +} + +.pagination ul > li > a, +.pagination ul > li > span { + float: left; + padding: 4px 12px; + line-height: 20px; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; + border-left-width: 0; +} + +.pagination ul > li > a:hover, +.pagination ul > li > a:focus, +.pagination ul > .active > a, +.pagination ul > .active > span { + background-color: #f5f5f5; +} + +.pagination ul > .active > a, +.pagination ul > .active > span { + color: #999999; + cursor: default; +} + +.pagination ul > .disabled > span, +.pagination ul > .disabled > a, +.pagination ul > .disabled > a:hover, +.pagination ul > .disabled > a:focus { + color: #999999; + cursor: default; + background-color: transparent; +} + +.pagination ul > li:first-child > a, +.pagination ul > li:first-child > span { + border-left-width: 1px; + -webkit-border-bottom-left-radius: 4px; + border-bottom-left-radius: 4px; + -webkit-border-top-left-radius: 4px; + border-top-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + -moz-border-radius-topleft: 4px; +} + +.pagination ul > li:last-child > a, +.pagination ul > li:last-child > span { + -webkit-border-top-right-radius: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + border-bottom-right-radius: 4px; + -moz-border-radius-topright: 4px; + -moz-border-radius-bottomright: 4px; +} + +.pagination-centered { + text-align: center; +} + +.pagination-right { + text-align: right; +} + +.pagination-large ul > li > a, +.pagination-large ul > li > span { + padding: 11px 19px; + font-size: 17.5px; +} + +.pagination-large ul > li:first-child > a, +.pagination-large ul > li:first-child > span { + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -webkit-border-top-left-radius: 6px; + border-top-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + -moz-border-radius-topleft: 6px; +} + +.pagination-large ul > li:last-child > a, +.pagination-large ul > li:last-child > span { + -webkit-border-top-right-radius: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-topright: 6px; + -moz-border-radius-bottomright: 6px; +} + +.pagination-mini ul > li:first-child > a, +.pagination-small ul > li:first-child > a, +.pagination-mini ul > li:first-child > span, +.pagination-small ul > li:first-child > span { + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -webkit-border-top-left-radius: 3px; + border-top-left-radius: 3px; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-topleft: 3px; +} + +.pagination-mini ul > li:last-child > a, +.pagination-small ul > li:last-child > a, +.pagination-mini ul > li:last-child > span, +.pagination-small ul > li:last-child > span { + -webkit-border-top-right-radius: 3px; + border-top-right-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-right-radius: 3px; + -moz-border-radius-topright: 3px; + -moz-border-radius-bottomright: 3px; +} + +.pagination-small ul > li > a, +.pagination-small ul > li > span { + padding: 2px 10px; + font-size: 11.9px; +} + +.pagination-mini ul > li > a, +.pagination-mini ul > li > span { + padding: 0 6px; + font-size: 10.5px; +} + +.pager { + margin: 20px 0; + text-align: center; + list-style: none; + *zoom: 1; +} + +.pager:before, +.pager:after { + display: table; + line-height: 0; + content: ""; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: default; + background-color: #fff; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; +} + +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.modal { + position: fixed; + top: 10%; + left: 50%; + z-index: 1050; + width: 560px; + margin-left: -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} + +.modal.fade { + top: -25%; + -webkit-transition: opacity 0.3s linear, top 0.3s ease-out; + -moz-transition: opacity 0.3s linear, top 0.3s ease-out; + -o-transition: opacity 0.3s linear, top 0.3s ease-out; + transition: opacity 0.3s linear, top 0.3s ease-out; +} + +.modal.fade.in { + top: 10%; +} + +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} + +.modal-header .close { + margin-top: 2px; +} + +.modal-header h3 { + margin: 0; + line-height: 30px; +} + +.modal-body { + position: relative; + max-height: 400px; + padding: 15px; + overflow-y: auto; +} + +.modal-form { + margin-bottom: 0; +} + +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + *zoom: 1; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + line-height: 0; + content: ""; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 11px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.popover-title:empty { + display: none; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; +} + +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} + +.thumbnails:before, +.thumbnails:after { + display: table; + line-height: 0; + content: ""; +} + +.thumbnails:after { + clear: both; +} + +.row-fluid .thumbnails { + margin-left: 0; +} + +.thumbnails > li { + float: left; + margin-bottom: 20px; + margin-left: 20px; +} + +.thumbnail { + display: block; + padding: 4px; + line-height: 20px; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.055); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} + +.thumbnail > img { + display: block; + max-width: 100%; + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #555555; +} + +.media, +.media-body { + overflow: hidden; + *overflow: visible; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + margin-left: 0; + list-style: none; +} + +.label, +.badge { + display: inline-block; + padding: 2px 4px; + font-size: 11.844px; + font-weight: bold; + line-height: 14px; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; +} + +.label { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.badge { + padding-right: 9px; + padding-left: 9px; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} + +.label:empty, +.badge:empty { + display: none; +} + +a.label:hover, +a.label:focus, +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label-important, +.badge-important { + background-color: #b94a48; +} + +.label-important[href], +.badge-important[href] { + background-color: #953b39; +} + +.label-warning, +.badge-warning { + background-color: #f89406; +} + +.label-warning[href], +.badge-warning[href] { + background-color: #c67605; +} + +.label-success, +.badge-success { + background-color: #468847; +} + +.label-success[href], +.badge-success[href] { + background-color: #356635; +} + +.label-info, +.badge-info { + background-color: #3a87ad; +} + +.label-info[href], +.badge-info[href] { + background-color: #2d6987; +} + +.label-inverse, +.badge-inverse { + background-color: #333333; +} + +.label-inverse[href], +.badge-inverse[href] { + background-color: #1a1a1a; +} + +.btn .label, +.btn .badge { + position: relative; + top: -1px; +} + +.btn-mini .label, +.btn-mini .badge { + top: 0; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-ms-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(to bottom, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress .bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(to bottom, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress .bar + .bar { + -webkit-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15); +} + +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-danger .bar, +.progress .bar-danger { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(to bottom, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0); +} + +.progress-danger.progress-striped .bar, +.progress-striped .bar-danger { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-success .bar, +.progress .bar-success { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(to bottom, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0); +} + +.progress-success.progress-striped .bar, +.progress-striped .bar-success { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-info .bar, +.progress .bar-info { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(to bottom, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0); +} + +.progress-info.progress-striped .bar, +.progress-striped .bar-info { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-warning .bar, +.progress .bar-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(to bottom, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); +} + +.progress-warning.progress-striped .bar, +.progress-striped .bar-warning { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.accordion { + margin-bottom: 20px; +} + +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.accordion-heading { + border-bottom: 0; +} + +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} + +.accordion-toggle { + cursor: pointer; +} + +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} + +.carousel { + position: relative; + margin-bottom: 20px; + line-height: 1; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.right { + right: 15px; + left: auto; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-indicators { + position: absolute; + top: 15px; + right: 15px; + z-index: 5; + margin: 0; + list-style: none; +} + +.carousel-indicators li { + display: block; + float: left; + width: 10px; + height: 10px; + margin-left: 5px; + text-indent: -999px; + background-color: #ccc; + background-color: rgba(255, 255, 255, 0.25); + border-radius: 5px; +} + +.carousel-indicators .active { + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 15px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} + +.carousel-caption h4, +.carousel-caption p { + line-height: 20px; + color: #ffffff; +} + +.carousel-caption h4 { + margin: 0 0 5px; +} + +.carousel-caption p { + margin-bottom: 0; +} + +.hero-unit { + padding: 60px; + margin-bottom: 30px; + font-size: 18px; + font-weight: 200; + line-height: 30px; + color: inherit; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; + color: inherit; +} + +.hero-unit li { + line-height: 30px; +} + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.invisible { + visibility: hidden; +} + +.affix { + position: fixed; +} diff --git a/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.min.css b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.min.css new file mode 100644 index 0000000..c10c7f4 --- /dev/null +++ b/docs/_build/html/_static/bootstrap-2.3.1/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings-white.png b/docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf6484a29d8da269f9bc874b25493a45fae3bae GIT binary patch literal 8777 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 B7G?kd literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings.png b/docs/_build/html/_static/bootstrap-2.3.1/img/glyphicons-halflings.png new file mode 100644 index 0000000000000000000000000000000000000000..a9969993201f9cee63cf9f49217646347297b643 GIT binary patch literal 12799 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 literal 0 HcmV?d00001 diff --git a/docs/_build/html/_static/bootstrap-2.3.1/js/bootstrap.js b/docs/_build/html/_static/bootstrap-2.3.1/js/bootstrap.js new file mode 100644 index 0000000..baad593 --- /dev/null +++ b/docs/_build/html/_static/bootstrap-2.3.1/js/bootstrap.js @@ -0,0 +1,2276 @@ +/* =================================================== + * bootstrap-transition.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $(function () { + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}($jqTheme || window.jQuery);/* ========================================================== + * bootstrap-alert.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}($jqTheme || window.jQuery);/* ============================================================ + * bootstrap-button.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}($jqTheme || window.jQuery);/* ========================================================== + * bootstrap-carousel.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + if (this.interval) clearInterval(this.interval); + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , getActiveIndex: function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + return this.$items.index(this.$active) + } + + , to: function (pos) { + var activeIndex = this.getActiveIndex() + , that = this + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activeIndex == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + , direction: direction + }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + , slideIndex + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('carousel').pause().to(slideIndex).cycle() + } + + e.preventDefault() + }) + +}($jqTheme || window.jQuery);/* ============================================================= + * bootstrap-collapse.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning || this.$element.hasClass('in')) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning || !this.$element.hasClass('in')) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}($jqTheme || window.jQuery);/* ============================================================ + * bootstrap-dropdown.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + $parent.toggleClass('open') + } + + $this.focus() + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + $(toggle).each(function () { + getParent($(this)).removeClass('open') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = selector && $(selector) + + if (!$parent || !$parent.length) $parent = $this.parent() + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* DROPDOWN NO CONFLICT + * ==================== */ + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(document) + .on('click.dropdown.data-api', clearMenus) + .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown-menu', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}($jqTheme || window.jQuery); +/* ========================================================= + * bootstrap-modal.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element.show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : + that.$element.focus().trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden') + }) + } + + , removeBackdrop: function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('