From 7468bda94f3ddbaff88ea53b35c578576a61fccf Mon Sep 17 00:00:00 2001 From: Dick Brouwer Date: Mon, 5 Mar 2012 13:16:38 -0800 Subject: [PATCH 001/432] Fixed converting request token to an access token step --- twython/twython.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index 6d38ac3..174ebab 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -244,13 +244,13 @@ class Twython(object): return request_tokens - def get_authorized_tokens(self): + def get_authorized_tokens(self, oauth_verifier): """ get_authorized_tokens - Returns authorized tokens after they go through the auth_url phase. + Returns authorized tokens and basic user info after they go through the auth_url phase. """ - resp, content = self.client.request(self.access_token_url, "GET") + resp, content = self.client.request(self.access_token_url, "POST", body="oauth_verifier=%s" % oauth_verifier) return dict(parse_qsl(content)) # ------------------------------------------------------------------------------------------------------------------------ -- 2.39.5 From e3d9ed656b008b147656a074c08f06d4d3ae0334 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 6 Mar 2012 16:58:27 -0500 Subject: [PATCH 002/432] 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', + }, } -- 2.39.5 From 8630dc3f03af5390eed1e0598f25f7d0e50146d0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 8 Mar 2012 12:20:04 -0500 Subject: [PATCH 003/432] 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): -- 2.39.5 From 158bf77231f8ad09221205db53aa18ce21df95e3 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 8 Mar 2012 12:24:03 -0500 Subject: [PATCH 004/432] 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) -- 2.39.5 From 9e8bc0912152fdaf2736e2666729977c8c55e833 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 11:41:27 -0400 Subject: [PATCH 005/432] 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)) -- 2.39.5 From 9deced8f8b5b1d7b3e02018421574b8eee87732f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:21:34 +0100 Subject: [PATCH 006/432] 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 -- 2.39.5 From e0c76501bacb5420099a769f8f0f16a5ef3fd13f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:32:25 +0100 Subject: [PATCH 007/432] 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 -- 2.39.5 From 6d72b8aa33912a869f7dc2396e5b834a875ba18b Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:34:32 +0100 Subject: [PATCH 008/432] 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 -- 2.39.5 From 16a70d0240fd2ad938f386b01eca84cf3256e1ad Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:35:35 +0100 Subject: [PATCH 009/432] 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) -- 2.39.5 From 87c1f1e71c325a7eb9b20e67524f4c780823f062 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:36:33 +0100 Subject: [PATCH 010/432] 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. -- 2.39.5 From 8e26e568a6a01b40f68784bdc8c0c527d90c6486 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 21 Mar 2012 19:37:37 +0100 Subject: [PATCH 011/432] 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) ---------------------------------------------------------------------------------------------------- -- 2.39.5 From 5eb7f29bffe8cd5a6c52d24e2ce7b6f3061f7609 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:19:27 -0400 Subject: [PATCH 012/432] 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) -- 2.39.5 From f917b6bfea338e30d95e8b5d54edb5a8ff1d3fb9 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:25:25 -0400 Subject: [PATCH 013/432] 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)): -- 2.39.5 From 59b5733a8698cb76f2c33f16713b052467aa27c0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 21 Mar 2012 15:27:13 -0400 Subject: [PATCH 014/432] 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 -- 2.39.5 From 23e529e1673ef8fd6654ac645481fa060173ce17 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 23 Mar 2012 15:46:19 -0400 Subject: [PATCH 015/432] 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')) -- 2.39.5 From 03f3a22480fcf275c8f85a785257e62c624bd75a Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 18:12:07 -0400 Subject: [PATCH 016/432] 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) -- 2.39.5 From cf38c7c3de42b5bd7b2a3e3eb348dc8dae2fa234 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 20:16:30 -0400 Subject: [PATCH 017/432] 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 -- 2.39.5 From 0fcd4202c8f4aecfddc7c6f9eff3f0ede083bde0 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sat, 31 Mar 2012 20:18:12 -0400 Subject: [PATCH 018/432] 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 -- 2.39.5 From e17b3ed87782d5ff2c5b197a3ffdf326da703172 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:02:11 +0200 Subject: [PATCH 019/432] 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) -- 2.39.5 From f4c00ff996374ea4306ca0c352aef8f00015676a Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:08:12 +0200 Subject: [PATCH 020/432] 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) -- 2.39.5 From ffb768d24deaeb1a26c0e3d3324df88ec9c81914 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 11:09:43 +0200 Subject: [PATCH 021/432] 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) -- 2.39.5 From 703012ef2989e5c2d390e503a837d448b0812480 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 11:44:30 -0400 Subject: [PATCH 022/432] 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 -- 2.39.5 From e353125ef123a51a61518789b4e38697995daad5 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 6 Apr 2012 18:48:26 +0200 Subject: [PATCH 023/432] 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 -- 2.39.5 From 7205aa402a96e4248aaef0d09af6b8b5d4f76cd2 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Fri, 6 Apr 2012 14:19:46 -0400 Subject: [PATCH 024/432] 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) -- 2.39.5 From 59b038656499f0ea277534b59c3bc6b9f9aebd46 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Sun, 8 Apr 2012 18:37:47 -0400 Subject: [PATCH 025/432] 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): """ -- 2.39.5 From 813626a9add1b166a760c73ecbf3ef3bb44e5f65 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 9 Apr 2012 10:59:13 -0400 Subject: [PATCH 026/432] 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) -- 2.39.5 From 9b798f7ac0fecf310e9a1a34fd9ffef0038c1121 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Tue, 10 Apr 2012 10:21:00 -0400 Subject: [PATCH 027/432] 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): -- 2.39.5 From 3f26325ddb0a6cc13a9c7b253207836f8c44a814 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 10 Apr 2012 14:24:50 -0400 Subject: [PATCH 028/432] 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) + + ''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' -- 2.39.5 From a42570b68546973c4186c5eb898f635f04936f52 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Apr 2012 11:44:27 -0400 Subject: [PATCH 029/432] 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): -- 2.39.5 From 343dcb87ffe33938a8bb5f61bb66cb298736e22f Mon Sep 17 00:00:00 2001 From: Mohammed ALDOUB Date: Sat, 14 Apr 2012 05:34:22 +0300 Subject: [PATCH 030/432] 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')) -- 2.39.5 From 01a7284a8f3928739c410eebd969715a9cccc791 Mon Sep 17 00:00:00 2001 From: Mohammed ALDOUB Date: Sat, 14 Apr 2012 05:53:51 +0300 Subject: [PATCH 031/432] 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: -- 2.39.5 From aabd29a01ef649e76116d72d6f6504550e2e07d0 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Thu, 19 Apr 2012 18:38:10 -0400 Subject: [PATCH 032/432] 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) -- 2.39.5 From ac18837ed66e5baeb862e4e9e877fe18686de10b Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 19 Apr 2012 19:31:07 -0400 Subject: [PATCH 033/432] 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) -- 2.39.5 From 0ee5e5877e6ac7560601647234e92351be7dd810 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 8 May 2012 12:14:45 -0400 Subject: [PATCH 034/432] 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') -- 2.39.5 From 9fa9b525a1492570ba72d9864a59ce45cc9bf507 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 8 May 2012 12:29:57 -0400 Subject: [PATCH 035/432] 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) ---------------------------------------------------------------------------------------------------- -- 2.39.5 From 19293b54a9981a6cc316dc8394eb7e09715a6b94 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Sun, 13 May 2012 12:38:30 -0400 Subject: [PATCH 036/432] 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) - - ''' !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ''' -- 2.39.5 From 2f80933cb82ed7e43c4562b310003f892cc8735f Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 14 May 2012 11:12:23 -0400 Subject: [PATCH 037/432] 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 !!!'}) -- 2.39.5 From a4e3af1ad46bce70f4d887b97e781bc5e142a3e2 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Mon, 14 May 2012 15:16:50 -0400 Subject: [PATCH 038/432] 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 !!!'}) -- 2.39.5 From f2cd0d5284f9b032817fe67e7826fe7846ed9c5a Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 11:59:47 -0400 Subject: [PATCH 039/432] 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', -- 2.39.5 From 5e817195acd21aa13f7420de0c4e7a41e521a5e5 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 12:09:26 -0400 Subject: [PATCH 040/432] 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): -- 2.39.5 From 32a83a6b79498790c0e401c6f2de988bba3d4c44 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Wed, 16 May 2012 18:39:31 -0400 Subject: [PATCH 041/432] 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. -- 2.39.5 From d93b48cded90fa28454e6ec068a3eb56658b2de5 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 17 May 2012 12:22:37 -0400 Subject: [PATCH 042/432] 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, -- 2.39.5 From fc9e21435ef77e1d555704832edca17efb25ea02 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Tue, 22 May 2012 10:40:38 -0400 Subject: [PATCH 043/432] 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) -- 2.39.5 From 0d00ae97fb53347b2a720b988f2d4f356f307f4d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 23 May 2012 18:49:08 +0900 Subject: [PATCH 044/432] 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 -- 2.39.5 From 0b3f36f9b6a73ce12cde372266fe91ed5348dd20 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:03 -0300 Subject: [PATCH 045/432] 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', -- 2.39.5 From 072a257a1ffb1d9ee987f0a7c7b93127a22f6ec9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:30 -0300 Subject: [PATCH 046/432] 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. -- 2.39.5 From f232b873cb7a4caf5cf0061423bedf3b003226fa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 30 May 2012 11:35:51 -0300 Subject: [PATCH 047/432] 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 -- 2.39.5 From 92f9c941466dc9fa53aa43a3208b8e5115d8bc89 Mon Sep 17 00:00:00 2001 From: Leandro Ferreira Date: Thu, 31 May 2012 10:59:43 -0300 Subject: [PATCH 048/432] 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) -- 2.39.5 From 7caa68814631203cb63231918e42e54eee4d2273 Mon Sep 17 00:00:00 2001 From: fumieval Date: Sun, 3 Jun 2012 18:25:25 +0900 Subject: [PATCH 049/432] 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): -- 2.39.5 From 3f4e374911cddabe18acfb54c9fcf015733dd44b Mon Sep 17 00:00:00 2001 From: fumieval Date: Sun, 3 Jun 2012 18:43:09 +0900 Subject: [PATCH 050/432] 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. -- 2.39.5 From 1261b7b3045b75ed49fe8f6230f4aee726b5dede Mon Sep 17 00:00:00 2001 From: terrycojones Date: Mon, 11 Jun 2012 15:48:24 -0400 Subject: [PATCH 051/432] 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): -- 2.39.5 From 8ea61af4fc30e83e4d239ed3911a600cbb438fa4 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 25 Jun 2012 05:08:16 +0900 Subject: [PATCH 052/432] 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. """ -- 2.39.5 From 2155ae0c2344a70d06c8082ff6aef8fd4082d439 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 25 Jun 2012 11:50:44 -0400 Subject: [PATCH 053/432] 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 -- 2.39.5 From dfdbbec5a8a4c288bf3f7e118280c7a85ca829bc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 25 Jun 2012 11:54:33 -0400 Subject: [PATCH 054/432] 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.* -- 2.39.5 From b552913e53bd6710009a022d17f4aabcebe31e41 Mon Sep 17 00:00:00 2001 From: Mike Grouchy Date: Wed, 27 Jun 2012 09:53:35 -0400 Subject: [PATCH 055/432] 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): -- 2.39.5 From a9b7b836c985a963ddbdf89522d5222cd74f0db1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 28 Jun 2012 11:08:59 -0400 Subject: [PATCH 056/432] 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) -- 2.39.5 From f4b2ebc40a9c1e8b91ce84c25378091e51bb5828 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 28 Jun 2012 11:13:49 -0400 Subject: [PATCH 057/432] 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']: -- 2.39.5 From 73a1910066893fa7b0dc3951d8afde593520c6c5 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 30 Jun 2012 00:52:40 +0900 Subject: [PATCH 058/432] 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 -- 2.39.5 From d86437681679462a6d84e069a20fdad3bb1b76f3 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 29 Jun 2012 12:19:37 -0400 Subject: [PATCH 059/432] 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.') -- 2.39.5 From 7986da859f5ca42c4f1388c10e38097b8cdfe276 Mon Sep 17 00:00:00 2001 From: Mohmmadhd Date: Tue, 10 Jul 2012 15:22:31 +0300 Subject: [PATCH 060/432] 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 -- 2.39.5 From c5468ee1b5c6a0c554cecd9eb309b5b081e8fa4f Mon Sep 17 00:00:00 2001 From: lucadex Date: Tue, 24 Jul 2012 12:45:32 +0300 Subject: [PATCH 061/432] 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 -- 2.39.5 From ee940288548e3d1c5a6a0882be9b3b07bf2ab43a Mon Sep 17 00:00:00 2001 From: lucadex Date: Tue, 24 Jul 2012 12:46:16 +0300 Subject: [PATCH 062/432] 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']) -- 2.39.5 From e1c4035a637f737e602c6c05ba1d30d998e0264f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Wed, 25 Jul 2012 03:56:12 +0900 Subject: [PATCH 063/432] 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 -- 2.39.5 From 9e5a96655db1313feda400993a45f3b46d07ba29 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 27 Jul 2012 12:10:36 -0400 Subject: [PATCH 064/432] 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: -- 2.39.5 From 9d21865409c47fdf7c1109445ae89b0cd4f65815 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Wed, 1 Aug 2012 13:32:06 -0400 Subject: [PATCH 065/432] 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 ''' -- 2.39.5 From 341fdd8f49242d6dd4ac273135243f5e27be6289 Mon Sep 17 00:00:00 2001 From: jonathan vanasco Date: Wed, 1 Aug 2012 13:53:44 -0400 Subject: [PATCH 066/432] 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, -- 2.39.5 From 156368cf6e00d7cf15bd5c4fe3d3b53f8954df5f Mon Sep 17 00:00:00 2001 From: Denis Veselov Date: Tue, 28 Aug 2012 00:48:32 +0400 Subject: [PATCH 067/432] 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': { -- 2.39.5 From a2e95d40efabc53d292a256e2fe81d54b98a7734 Mon Sep 17 00:00:00 2001 From: Denis Veselov Date: Tue, 28 Aug 2012 00:49:09 +0400 Subject: [PATCH 068/432] 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': { -- 2.39.5 From 8e72adead2c7fead5b07063f20f0536ecdb523cd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 31 Aug 2012 14:22:48 -0400 Subject: [PATCH 069/432] 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 -- 2.39.5 From 0c2565192180e2e93d56d4f10b582a5c08dbb77f Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 13 Sep 2012 22:38:05 -0500 Subject: [PATCH 070/432] 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. -- 2.39.5 From 448b4f27b61d4d6336b3875bdddeb5d387eacaa5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 14:08:46 -0400 Subject: [PATCH 071/432] 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 -- 2.39.5 From cc31322102e0c2942713b8a98c1977644116d41d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 14:10:56 -0400 Subject: [PATCH 072/432] 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"] -- 2.39.5 From a3967390e10f8a5422cda6cee1d605701c3e24b5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 16:53:28 -0400 Subject: [PATCH 073/432] 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') -- 2.39.5 From b314a5606e2d464ee187a2c7306c4f63aa5d71ca Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Oct 2012 16:56:20 -0400 Subject: [PATCH 074/432] 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') -- 2.39.5 From 5a516c2bfbbea3a1474bde98dc466d152eca8143 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 10 Oct 2012 12:55:35 -0400 Subject: [PATCH 075/432] 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 -- 2.39.5 From 4e86da4aec63acb581d02eb3046ae1a098485356 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Oct 2012 12:36:45 -0400 Subject: [PATCH 076/432] 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', -- 2.39.5 From 98e213df9c8f6c46ca4dda6093a98bc958086caa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Oct 2012 12:37:16 -0400 Subject: [PATCH 077/432] 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. -- 2.39.5 From 909f1919b14c19b5255aa5c159ad32dbd9750699 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 9 Nov 2012 04:57:56 -0500 Subject: [PATCH 078/432] 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 -- 2.39.5 From b8084905d3fd681087062ee9576efc9094591ed5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 10:30:37 -0500 Subject: [PATCH 079/432] 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', -- 2.39.5 From d52ce03de43c6bd1e96952b2da4df7a32c7de8c8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 10:50:32 -0500 Subject: [PATCH 080/432] 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 -- 2.39.5 From be494d4c77c9fa305673a926b765f91d44b17c21 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 9 Nov 2012 11:14:36 -0500 Subject: [PATCH 081/432] 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 -- 2.39.5 From a125ad6048ee4938e64d6915c2f12b825b79121e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 10 Nov 2012 20:16:34 -0500 Subject: [PATCH 082/432] 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 -- 2.39.5 From 80282d9aa72f307a36c76e7a1e48ddab9ce348fe Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 14 Nov 2012 15:13:16 -0500 Subject: [PATCH 083/432] 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) -- 2.39.5 From 7d8220661440054a87bfe82f39ebc87eb8082833 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 29 Nov 2012 15:32:07 -0500 Subject: [PATCH 084/432] 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', -- 2.39.5 From 34e9474b91af875e14553b781f253b98619ea765 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 1 Dec 2012 01:34:38 -0500 Subject: [PATCH 085/432] 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 -- 2.39.5 From d83cc32b3dd5a9bbb9fff546a73e6626e7f1b2bd Mon Sep 17 00:00:00 2001 From: Christopher Brown Date: Thu, 6 Dec 2012 16:51:34 -0500 Subject: [PATCH 086/432] 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 -- 2.39.5 From 2d3d5b5b680905b73f6bdf717268defce056044d Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Fri, 14 Dec 2012 07:13:28 -0500 Subject: [PATCH 087/432] 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 -- 2.39.5 From fe3fcbdb04e2096cc2b4146383ad8f55dfd437c2 Mon Sep 17 00:00:00 2001 From: Ajay Nadathur Date: Sun, 30 Dec 2012 23:06:36 +0000 Subject: [PATCH 088/432] 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'] ) -- 2.39.5 From 87a3b44a306d4c3c55ee745ba0c7fe866567cecd Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 1 Jan 2013 20:47:02 -0500 Subject: [PATCH 089/432] 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 -- 2.39.5 From 6baa4fdd1482e7abf2dd28e8357137a80cd23b96 Mon Sep 17 00:00:00 2001 From: Ryan Merl Date: Thu, 10 Jan 2013 03:41:22 -0500 Subject: [PATCH 090/432] 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', }, -- 2.39.5 From 7f0751c27eb97fd4e888c6e5b4447af2b12bfbc5 Mon Sep 17 00:00:00 2001 From: Guru Devanla Date: Tue, 15 Jan 2013 16:40:57 -0600 Subject: [PATCH 091/432] 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, -- 2.39.5 From a28febb0b4283be0d3d024bd9bd3709898d4a14c Mon Sep 17 00:00:00 2001 From: Guru Devanla Date: Tue, 15 Jan 2013 16:43:50 -0600 Subject: [PATCH 092/432] 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, -- 2.39.5 From f0db93c59ea29b2d6e9ef626ed535c4ab1c0e2d9 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Wed, 30 Jan 2013 01:00:32 +1100 Subject: [PATCH 093/432] 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, -- 2.39.5 From dbf2a461b84e62b9736bcb0fbeed3ad4edcc532c Mon Sep 17 00:00:00 2001 From: Mahdi Yusuf Date: Tue, 5 Feb 2013 16:18:56 -0500 Subject: [PATCH 094/432] 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. -- 2.39.5 From b0f5af37d5a813c2e1740160a9a4643e3b146531 Mon Sep 17 00:00:00 2001 From: Mahdi Yusuf Date: Tue, 5 Feb 2013 16:53:47 -0500 Subject: [PATCH 095/432] 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 -- 2.39.5 From 9bda75b5200790bb2c68e256207d8fc5d45a76c6 Mon Sep 17 00:00:00 2001 From: Sam Mellor Date: Thu, 7 Feb 2013 22:20:35 +1100 Subject: [PATCH 096/432] 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', -- 2.39.5 From a172136f3edf9c6b085d8a350788767da0dd098b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 19 Mar 2013 15:40:40 -0400 Subject: [PATCH 097/432] 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 -- 2.39.5 From 454a41fe94b08ca93ae04b9b580cf2ff51e2fa75 Mon Sep 17 00:00:00 2001 From: Virendra Rajput Date: Sun, 31 Mar 2013 21:23:16 +0530 Subject: [PATCH 098/432] 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': { -- 2.39.5 From a6afb2cf5ce1a2f6c5750f0b4dd43ea260acf8c7 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sun, 31 Mar 2013 12:38:07 -0400 Subject: [PATCH 099/432] 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 -- 2.39.5 From 7d1ffefc45a7f29877034c0fe2a95ab00a26d5ac Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:44:05 -0400 Subject: [PATCH 100/432] 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): -- 2.39.5 From fffedd4588a73c85032c09227af6c6e2ca5857c7 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:52:32 -0400 Subject: [PATCH 101/432] 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 -- 2.39.5 From e65790d7170c97c120dd6037102358c276907f45 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 4 Apr 2013 14:52:54 -0400 Subject: [PATCH 102/432] 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 -- 2.39.5 From 99a6dccbce2fea77689475b18b411f49cc97d076 Mon Sep 17 00:00:00 2001 From: Paul Date: Fri, 5 Apr 2013 00:12:54 +0200 Subject: [PATCH 103/432] 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.') -- 2.39.5 From 26b3a232d0be46ce17920bca287260ec8eca43bc Mon Sep 17 00:00:00 2001 From: hansenrum Date: Fri, 5 Apr 2013 00:25:23 +0200 Subject: [PATCH 104/432] 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. -- 2.39.5 From 1eb1bd080d88e35b13de355926e64a14ebe767a7 Mon Sep 17 00:00:00 2001 From: hansenrum Date: Fri, 5 Apr 2013 18:36:58 +0200 Subject: [PATCH 105/432] 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.') -- 2.39.5 From abaa3e558a75d225a31d5ad1df93a459361334f9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 11:49:12 -0400 Subject: [PATCH 106/432] 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.') -- 2.39.5 From 4a181d3ac14ef41abff46cec9298044787791d8b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 11:51:34 -0400 Subject: [PATCH 107/432] 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 -- 2.39.5 From 6a3539882caf500e1287c0f71d107a4db1a45862 Mon Sep 17 00:00:00 2001 From: Virendra Rajput Date: Mon, 8 Apr 2013 22:59:27 +0530 Subject: [PATCH 108/432] 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 = {} -- 2.39.5 From 10dbe11b565aaa824d3ebc5d0c787072e6b9cbc6 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 8 Apr 2013 16:40:59 -0400 Subject: [PATCH 109/432] 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 -- 2.39.5 From 8dfb076f11ef9806c9dfa34b97c1a90a2b75751e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 10 Apr 2013 23:08:43 -0400 Subject: [PATCH 110/432] 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 -- 2.39.5 From 12eb1610c895357da49b759bb4bea2f1f44097ce Mon Sep 17 00:00:00 2001 From: Greg Nofi Date: Thu, 11 Apr 2013 19:42:16 -0400 Subject: [PATCH 111/432] 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): -- 2.39.5 From 7469f8bc739fa28fde7644bd2743bb8bc5511841 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:17:40 -0400 Subject: [PATCH 112/432] 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.') -- 2.39.5 From 969c0f5e72344e814f52959935f75e24f0ddcd87 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:28:58 -0400 Subject: [PATCH 113/432] 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 -- 2.39.5 From d228e04bc098866fa680e84eab4cf8c72c40f5b3 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 12 Apr 2013 11:40:05 -0400 Subject: [PATCH 114/432] 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 -- 2.39.5 From 023b29b202f8aa9b2027a6baa62378d38024b4c8 Mon Sep 17 00:00:00 2001 From: Adrien Tronche Date: Sun, 14 Apr 2013 00:16:28 -0300 Subject: [PATCH 115/432] 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.') -- 2.39.5 From 6d1c439a8920e808dd7a3099c81ff9ef2eef1257 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 15 Apr 2013 16:12:02 -0400 Subject: [PATCH 116/432] 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'): -- 2.39.5 From 80c74880b1a5370d6fde4299f9fb547ed89e44be Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 15 Apr 2013 16:15:52 -0400 Subject: [PATCH 117/432] 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 -- 2.39.5 From bb019d3a577dda00d59b6873f0d61191d30c35d9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 17 Apr 2013 18:59:11 -0400 Subject: [PATCH 118/432] 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) -- 2.39.5 From 4ac6436901d16394e264cd36234e55d24e4dba28 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 18 Apr 2013 22:21:32 -0400 Subject: [PATCH 119/432] 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 -- 2.39.5 From d4c19fc3a9357f642ba27cc1c08d88df80704871 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 19 Apr 2013 02:14:22 -0400 Subject: [PATCH 120/432] 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' -- 2.39.5 From a451db43c16e57f29e9f2d892e9f615dcac339e9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 22 Apr 2013 21:29:07 -0400 Subject: [PATCH 121/432] 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) -- 2.39.5 From 776e02b0715e9f059a1152593755ee6c0e834699 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 23 Apr 2013 19:43:05 -0400 Subject: [PATCH 122/432] 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) ########################################################################### -- 2.39.5 From 32432bcac963bf76618e9a53da773cfd583850cd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 11:42:34 -0400 Subject: [PATCH 123/432] 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 -- 2.39.5 From b6e820d792759a3f67f352e84f60618b0a4b34ba Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 12:04:22 -0400 Subject: [PATCH 124/432] 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' -- 2.39.5 From e18bff97d3299b66f4a4208a123c89aa021ceba2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 29 Apr 2013 14:30:30 -0300 Subject: [PATCH 125/432] 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 -- 2.39.5 From c3e84bc8eee44f61e4b255e1642592bbcd0faf49 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 2 May 2013 15:12:36 -0400 Subject: [PATCH 126/432] 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: -- 2.39.5 From 97a33ce8dd5cb32334d454889d44ed1d828622bd Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 3 May 2013 16:57:59 -0400 Subject: [PATCH 127/432] 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 -- 2.39.5 From cea0852a42200553a8dc5d04db7c7ab919d49475 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 3 May 2013 17:33:17 -0400 Subject: [PATCH 128/432] 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) -- 2.39.5 From b7d68c61365f17d794ac5c770bc4b54e20b771fb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 15:12:21 -0400 Subject: [PATCH 129/432] 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', -- 2.39.5 From 0e258fe1a19dd40ce5150d1e4fc6fb49580ed9cb Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 15:13:29 -0400 Subject: [PATCH 130/432] 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) ++++++++++++++++++ -- 2.39.5 From 84e4c5fe138a002c819a0a048b02fad2a0120046 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:28:47 -0400 Subject: [PATCH 131/432] 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 -- 2.39.5 From 828d355ad280d643e159f4b7072a3fb551edd632 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:29:55 -0400 Subject: [PATCH 132/432] 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 -- 2.39.5 From 498bd9e557040ba9c1e935fb0d7e815077726ff4 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:35:21 -0400 Subject: [PATCH 133/432] 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- 2.39.5 From 600f36c6bab6607bff81f9891bb3588d6512e581 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 19:46:23 -0400 Subject: [PATCH 134/432] 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 -- 2.39.5 From 60b2e14befd777530f51927c7d346def02a65981 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 4 May 2013 20:15:02 -0400 Subject: [PATCH 135/432] 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': -- 2.39.5 From bd0bd2748c44d79a109fe92d62b2bee5d23c203f Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 20:33:11 -0400 Subject: [PATCH 136/432] 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? ------------------------- -- 2.39.5 From f0f0d12a60d5334846741ac4730c1afc32ca9348 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Sat, 4 May 2013 20:35:13 -0400 Subject: [PATCH 137/432] 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 -- 2.39.5 From 5534ea2480f16e4a335b173e88b8ba85c73c7c35 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 10 May 2013 21:28:57 -0400 Subject: [PATCH 138/432] 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 -- 2.39.5 From 6841e7ef287abb1d4260e7ac3c9860a168edac59 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 21:34:22 -0400 Subject: [PATCH 139/432] 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() -- 2.39.5 From d3e17dcd4bf31166602e1f54cb0d349012c83156 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 21:52:25 -0400 Subject: [PATCH 140/432] 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): -- 2.39.5 From 0d3ae38c390ffe4b597efe502d161a8674041718 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 14 May 2013 22:07:05 -0400 Subject: [PATCH 141/432] 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 -- 2.39.5 From bb5e0dece15e1715099f58b3fc77a1803d5d7bd2 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 16:36:53 -0400 Subject: [PATCH 142/432] 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') -- 2.39.5 From a76b93e49107d987bb7633a7374909be2a27ccf5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 16:55:55 -0400 Subject: [PATCH 143/432] 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: -- 2.39.5 From 077406a8b344e335af037e31e0839a84c86bfa0b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 17:38:28 -0400 Subject: [PATCH 144/432] 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 -- 2.39.5 From 008c53048a3eaef180bc99f97ab678e22663eab5 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 15 May 2013 21:23:50 -0400 Subject: [PATCH 145/432] 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') -- 2.39.5 From ea0b646fd103f48fa40cebbd2e51ae37088ebf09 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 12:40:25 -0400 Subject: [PATCH 146/432] 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: -- 2.39.5 From 6238912b9669718a6f2cfb874c88a3bfc6664a29 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 13:36:46 -0400 Subject: [PATCH 147/432] 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) -- 2.39.5 From 27c51b8ba618f2a3dbb33f8a7b6cc94aae510fbc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:15:11 -0400 Subject: [PATCH 148/432] 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'))) -- 2.39.5 From c42a987f38634752b23061bda28fed8427ddb04d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:32:58 -0400 Subject: [PATCH 149/432] 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 = {} -- 2.39.5 From 35d84021736f5509dc37f12ca92a05693cff5d47 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 16 May 2013 14:45:38 -0400 Subject: [PATCH 150/432] 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 -- 2.39.5 From 48fc5d4c36d5e562948fcc6459e31dfcb17fa69b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 17 May 2013 13:13:55 -0400 Subject: [PATCH 151/432] 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 -- 2.39.5 From c00647a5a02d9efd9e90f126ca86c654efac935f Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 17 May 2013 21:32:44 -0400 Subject: [PATCH 152/432] 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() -- 2.39.5 From f7f19dbdc3fa834a2bec216200db8bbeb5176263 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Sat, 18 May 2013 10:45:25 -0400 Subject: [PATCH 153/432] 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 -- 2.39.5 From 3dbef22cee9ea83c7e80756037209334da237d4c Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Mon, 20 May 2013 10:31:32 -0400 Subject: [PATCH 154/432] 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) -- 2.39.5 From c7bce9189fa6291430b4cbf30ac9497dd11fb3db Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 11:41:43 -0400 Subject: [PATCH 155/432] 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) -- 2.39.5 From 126305d93db96029b14f234fdc27b42e220e1043 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 12:52:17 -0400 Subject: [PATCH 156/432] 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) -- 2.39.5 From 050835e660e43580f0fade8c6d8e0d0c19856d01 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 15:20:17 -0400 Subject: [PATCH 157/432] 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( -- 2.39.5 From bf7b6727ddd76770a6c1bca6a4ecd0e2cb724d1a Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 18:22:30 -0400 Subject: [PATCH 158/432] 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', -- 2.39.5 From 48e7ccd39c65d0308f006adcb931d236d30d2f87 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 18:30:37 -0400 Subject: [PATCH 159/432] 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/ -- 2.39.5 From 78c1a95dc30fa47b9665f8311d35ef7ce4cde9fa Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 19:14:20 -0400 Subject: [PATCH 160/432] 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 -- 2.39.5 From 52609334bdd25eb89dbc67b75921029c37babd63 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 19:29:15 -0400 Subject: [PATCH 161/432] 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', -- 2.39.5 From b99db3c90c4338c84f1d27d15d83c2784216fda1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 21 May 2013 22:48:34 -0400 Subject: [PATCH 162/432] 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, -- 2.39.5 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 163/432] 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' -- 2.39.5 From 11acb49295cffb8586378abc97c93313530645e9 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 18:33:37 -0500 Subject: [PATCH 164/432] 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 -- 2.39.5 From a246743698ba085074e584ebf0829ffaec5233fa Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 20:03:20 -0500 Subject: [PATCH 165/432] 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 -- 2.39.5 From b0d801b7bb6a71ffb31d6c18092c8f1eb6b42506 Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 20:05:04 -0500 Subject: [PATCH 166/432] 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) -- 2.39.5 From 13d4725fcad056e1f78ea3c1636720700eb3c17a Mon Sep 17 00:00:00 2001 From: devdave Date: Tue, 28 May 2013 21:01:05 -0500 Subject: [PATCH 167/432] 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 -- 2.39.5 From 894e94a4cd666bb32e3b0144a4935b0624e8e020 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 23 May 2013 23:32:32 -0400 Subject: [PATCH 168/432] 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: -- 2.39.5 From 815393cc336a87d759b67deda47fabb0a51e43db Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 23 May 2013 23:36:05 -0400 Subject: [PATCH 169/432] 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') -- 2.39.5 From c8b12028808f04f294b9452da6647877c81911e0 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 24 May 2013 15:19:19 -0400 Subject: [PATCH 170/432] 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 -- 2.39.5 From 64b134999371cbb6a07e12597213d331ab2b16a9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Fri, 24 May 2013 16:17:50 -0400 Subject: [PATCH 171/432] 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.') -- 2.39.5 From f879094ea196c655c7916359b45331ab81f603e9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 28 May 2013 17:42:41 -0400 Subject: [PATCH 172/432] 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 -- 2.39.5 From 71ea58cf6fb01c0030fd0c562e9b9ad5318578dc Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Tue, 28 May 2013 17:47:03 -0400 Subject: [PATCH 173/432] 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']) -- 2.39.5 From 81a6802c639e8209557a48b99ec905e5712dfaca Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 11:40:14 -0400 Subject: [PATCH 174/432] 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) ++++++++++++++++++ -- 2.39.5 From 14b268e560a77c6eeb5b7351ffffa8be67d58a75 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 11:50:23 -0400 Subject: [PATCH 175/432] 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 -- 2.39.5 From 8d2dd80ae83b7b20cc8665d337276082834d717e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 13:37:42 -0400 Subject: [PATCH 176/432] 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`` -- 2.39.5 From 6eaa58cd0b67411646dd9bb90f212faf93f6ceb1 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:02:44 -0400 Subject: [PATCH 177/432] 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! -- 2.39.5 From 57f8e6b22fe081e5227860b4fdd1d6bcca4ac782 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:04:42 -0400 Subject: [PATCH 178/432] 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 -- 2.39.5 From b0c7b74e3b575fd289304f6ad3889d12b1c84f26 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Wed, 29 May 2013 14:05:33 -0400 Subject: [PATCH 179/432] 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 -- 2.39.5 From 4327ff30dffb1b17945b08655daf2b16998a239b Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 17:48:47 -0400 Subject: [PATCH 180/432] 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', -- 2.39.5 From 47e1b7c158d8c933ab2c3fc42efa965d5e393da9 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 18:16:39 -0400 Subject: [PATCH 181/432] 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: -- 2.39.5 From 9c6fe0d6b8f7addd95a5b9efe9d71df2aefa073d Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 30 May 2013 18:21:26 -0400 Subject: [PATCH 182/432] 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') -- 2.39.5 From ff7e3fab9429efcb801acf57c6dc326fa309d7e8 Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:40:39 -0400 Subject: [PATCH 183/432] 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 """ -- 2.39.5 From ec2bd7d6860a9f3d7673876c33133a5ac7bc764a Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:41:44 -0400 Subject: [PATCH 184/432] 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 -- 2.39.5 From 55641a1966be67d359ac50f63c99729894fda53e Mon Sep 17 00:00:00 2001 From: Mike Helmick Date: Thu, 6 Jun 2013 13:41:56 -0400 Subject: [PATCH 185/432] 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 = $('