Twython 1.3, OAuth support is now finally included and working. Ships with an example Django application to get people started with OAuth, entire library is refactored to not be a royal clusterfsck. Enjoy.
This commit is contained in:
parent
7ccf8a2baf
commit
eb5541e433
22 changed files with 2400 additions and 4492 deletions
|
|
@ -1,52 +1,76 @@
|
||||||
Twython - Easy Twitter utilities in Python
|
Twython - Easy Twitter utilities in Python
|
||||||
=========================================================================================
|
=========================================================================================
|
||||||
I wrote Twython because I found that other Python Twitter libraries weren't that up to date. Certain
|
Ah, Twitter, your API used to be so awesome, before you went and implemented the crap known
|
||||||
things like the Search API, OAuth, etc, don't seem to be fully covered. This is my attempt at
|
as OAuth 1.0. However, since you decided to force your entire development community over a barrel
|
||||||
a library that offers more coverage.
|
about it, I suppose Twython has to support this. So, that said...
|
||||||
|
|
||||||
This is my first library I've ever written in Python, so there could be some stuff in here that'll
|
If you used this library and it all stopped working, it's because of the Authentication method change.
|
||||||
make a seasoned Python vet scratch his head, or possibly call me insane. It's open source, though,
|
=========================================================================================================
|
||||||
and I'm open to anything that'll improve the library as a whole.
|
Twitter recently disabled the use of "Basic Authentication", which is why, if you used Twython previously,
|
||||||
|
you probably started getting a ton of 401 errors. To fix this, we should note one thing...
|
||||||
|
|
||||||
OAuth and Streaming API support is in the works, but every other part of the Twitter API should be covered. Twython
|
You need to change how authentication works in your program/application. If you're using a command line
|
||||||
handles both Basic (HTTP) Authentication and OAuth (Older versions (pre 0.9) of Twython need Basic Auth specified -
|
application or something, you'll probably languish in hell for a bit, because OAuth wasn't really designed
|
||||||
to override this, specify 'authtype="Basic"' in your twython.setup() call).
|
for those types of use cases. Twython cannot help you with that or fix the annoying parts of OAuth.
|
||||||
|
|
||||||
Twython has Docstrings if you want function-by-function plays; otherwise, check the Twython Wiki or
|
If you need OAuth, though, Twython now supports it, and ships with a skeleton Django application to get you started.
|
||||||
Twitter's API Wiki (Twython calls mirror most of the methods listed there).
|
Enjoy!
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
-----------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------
|
||||||
Twython (for versions of Python before 2.6) requires a library called
|
Twython (for versions of Python before 2.6) requires a library called
|
||||||
"simplejson". You can grab it at the following link:
|
"simplejson". Depending on your flavor of package manager, you can do the following...
|
||||||
|
|
||||||
> http://pypi.python.org/pypi/simplejson
|
(pip install | easy_install) simplejson
|
||||||
|
|
||||||
|
Twython also requires the (most excellent) OAuth2 library for handling OAuth tokens/signing/etc. Again...
|
||||||
|
|
||||||
|
(pip install | easy_install) oauth2
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
-----------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------
|
||||||
Installing Twython is fairly easy. You can...
|
Installing Twython is fairly easy. You can...
|
||||||
|
|
||||||
> easy_install twython
|
(pip install | easy_install) twython
|
||||||
|
|
||||||
...or, you can clone the repo and install it the old fashioned way.
|
...or, you can clone the repo and install it the old fashioned way.
|
||||||
|
|
||||||
> git clone git://github.com/ryanmcgrath/twython.git
|
git clone git://github.com/ryanmcgrath/twython.git
|
||||||
> cd twython
|
cd twython
|
||||||
> sudo python setup.py install
|
sudo python setup.py install
|
||||||
|
|
||||||
Example Use
|
Example Use
|
||||||
-----------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------
|
||||||
> import twython
|
from twython import Twython
|
||||||
>
|
|
||||||
> twitter = twython.core.setup(username="example", password="example")
|
|
||||||
> twitter.updateStatus("See how easy this was?")
|
|
||||||
|
|
||||||
|
twitter = Twython()
|
||||||
|
results = twitter.searchTwitter(q="bert")
|
||||||
|
|
||||||
|
A note about the development of Twython (specifically, 1.3)
|
||||||
|
----------------------------------------------------------------------------------------------------
|
||||||
|
As of version 1.3, Twython has been extensively overhauled. Most API endpoint definitions are stored
|
||||||
|
in a separate Python file, and the class itself catches calls to methods that match up in said table.
|
||||||
|
|
||||||
|
Certain functions require a bit more legwork, and get to stay in the main file, but for the most part
|
||||||
|
it's all abstracted out.
|
||||||
|
|
||||||
|
As of Twython 1.3, the syntax has changed a bit as well. Instead of Twython.core, there's a main
|
||||||
|
Twython class to import and use. If you need to catch exceptions, import those from twython as well.
|
||||||
|
|
||||||
|
Arguments to functions are now exact keyword matches for the Twitter API documentation - that means that
|
||||||
|
whatever query parameter arguments you read on Twitter's documentation (http://dev.twitter.com/doc) gets mapped
|
||||||
|
as a named argument to any Twitter function.
|
||||||
|
|
||||||
|
For example: the search API looks for arguments under the name "q", so you pass q="query_here" to searchTwitter().
|
||||||
|
|
||||||
|
Doing this allows us to be incredibly flexible in querying the Twitter API, so changes to the API aren't held up
|
||||||
|
from you using them by this library.
|
||||||
|
|
||||||
Twython 3k
|
Twython 3k
|
||||||
-----------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------
|
||||||
There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed
|
There's an experimental version of Twython that's made for Python 3k. This is currently not guaranteed
|
||||||
to work, but it's provided so that others can grab it and hack on it. If you choose to try it out,
|
to work (especially with regards to OAuth), but it's provided so that others can grab it and hack on it.
|
||||||
be aware of this.
|
If you choose to try it out, be aware of this.
|
||||||
|
|
||||||
|
|
||||||
Questions, Comments, etc?
|
Questions, Comments, etc?
|
||||||
|
|
|
||||||
0
oauth_django_example/__init__.py
Normal file
0
oauth_django_example/__init__.py
Normal file
11
oauth_django_example/manage.py
Normal file
11
oauth_django_example/manage.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
from django.core.management import execute_manager
|
||||||
|
try:
|
||||||
|
import settings # Assumed to be in the same directory.
|
||||||
|
except ImportError:
|
||||||
|
import sys
|
||||||
|
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
execute_manager(settings)
|
||||||
94
oauth_django_example/settings.py
Normal file
94
oauth_django_example/settings.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
||||||
|
ADMINS = (
|
||||||
|
('Ryan McGrath', 'ryan@venodesigns.net'),
|
||||||
|
)
|
||||||
|
|
||||||
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||||
|
'NAME': os.path.join(os.path.dirname(__file__), 'test.db'), # Or path to database file if using sqlite3.
|
||||||
|
'USER': '', # Not used with sqlite3.
|
||||||
|
'PASSWORD': '', # Not used with sqlite3.
|
||||||
|
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||||
|
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
AUTH_PROFILE_MODULE = 'twitter.Profile'
|
||||||
|
|
||||||
|
# Local time zone for this installation. Choices can be found here:
|
||||||
|
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||||
|
# although not all choices may be available on all operating systems.
|
||||||
|
# On Unix systems, a value of None will cause Django to use the same
|
||||||
|
# timezone as the operating system.
|
||||||
|
# If running in a Windows environment this must be set to the same as your
|
||||||
|
# system time zone.
|
||||||
|
TIME_ZONE = 'America/New_York'
|
||||||
|
|
||||||
|
# Language code for this installation. All choices can be found here:
|
||||||
|
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# If you set this to False, Django will make some optimizations so as not
|
||||||
|
# to load the internationalization machinery.
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
# If you set this to False, Django will not format dates, numbers and
|
||||||
|
# calendars according to the current locale
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
# Absolute path to the directory that holds media.
|
||||||
|
# Example: "/home/media/media.lawrence.com/"
|
||||||
|
MEDIA_ROOT = ''
|
||||||
|
|
||||||
|
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||||
|
# trailing slash if there is a path component (optional in other cases).
|
||||||
|
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||||
|
MEDIA_URL = ''
|
||||||
|
|
||||||
|
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||||
|
# trailing slash.
|
||||||
|
# Examples: "http://foo.com/media/", "/media/".
|
||||||
|
ADMIN_MEDIA_PREFIX = '/media/'
|
||||||
|
|
||||||
|
# Make this unique, and don't share it with anybody.
|
||||||
|
SECRET_KEY = '&!_t1t^gmenaid9mkmkuw=4nthj7f)o+!@$#ipfp*s11380t*)'
|
||||||
|
|
||||||
|
# List of callables that know how to import templates from various sources.
|
||||||
|
TEMPLATE_LOADERS = (
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
# 'django.template.loaders.eggs.Loader',
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE_CLASSES = (
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
)
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'twython_testing.urls'
|
||||||
|
|
||||||
|
TEMPLATE_DIRS = (
|
||||||
|
os.path.join(os.path.dirname(__file__), 'templates'),
|
||||||
|
)
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'twitter',
|
||||||
|
)
|
||||||
3
oauth_django_example/templates/tweets.html
Normal file
3
oauth_django_example/templates/tweets.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% for tweet in tweets %}
|
||||||
|
{{ tweet.text }}
|
||||||
|
{% endfor %}
|
||||||
BIN
oauth_django_example/test.db
Normal file
BIN
oauth_django_example/test.db
Normal file
Binary file not shown.
0
oauth_django_example/twitter/__init__.py
Normal file
0
oauth_django_example/twitter/__init__.py
Normal file
7
oauth_django_example/twitter/models.py
Normal file
7
oauth_django_example/twitter/models.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Profile(models.Model):
|
||||||
|
user = models.ForeignKey(User)
|
||||||
|
oauth_token = models.CharField(max_length = 200)
|
||||||
|
oauth_secret = models.CharField(max_length = 200)
|
||||||
299
oauth_django_example/twitter/twitter_endpoints.py
Normal file
299
oauth_django_example/twitter/twitter_endpoints.py
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
"""
|
||||||
|
A huge map of every Twitter API endpoint to a function definition in Twython.
|
||||||
|
|
||||||
|
Parameters that need to be embedded in the URL are treated with mustaches, e.g:
|
||||||
|
|
||||||
|
{{version}}, etc
|
||||||
|
|
||||||
|
When creating new endpoint definitions, keep in mind that the name of the mustache
|
||||||
|
will be replaced with the keyword that gets passed in to the function at call time.
|
||||||
|
|
||||||
|
i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
|
||||||
|
with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base Twitter API url, no need to repeat this junk...
|
||||||
|
base_url = 'http://api.twitter.com/{{version}}'
|
||||||
|
|
||||||
|
api_table = {
|
||||||
|
'getRateLimitStatus': {
|
||||||
|
'url': '/account/rate_limit_status.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Timeline methods
|
||||||
|
'getPublicTimeline': {
|
||||||
|
'url': '/statuses/public_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getHomeTimeline': {
|
||||||
|
'url': '/statuses/home_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getUserTimeline': {
|
||||||
|
'url': '/statuses/user_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsTimeline': {
|
||||||
|
'url': '/statuses/friends_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Interfacing with friends/followers
|
||||||
|
'getUserMentions': {
|
||||||
|
'url': '/statuses/mentions.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsStatus': {
|
||||||
|
'url': '/statuses/friends.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersStatus': {
|
||||||
|
'url': '/statuses/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFriendship': {
|
||||||
|
'url': '/friendships/create.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFriendship': {
|
||||||
|
'url': '/friendships/destroy.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getFriendsIDs': {
|
||||||
|
'url': '/friends/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersIDs': {
|
||||||
|
'url': '/followers/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Retweets
|
||||||
|
'reTweet': {
|
||||||
|
'url': '/statuses/retweet/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getRetweets': {
|
||||||
|
'url': '/statuses/retweets/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedOfMe': {
|
||||||
|
'url': '/statuses/retweets_of_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedByMe': {
|
||||||
|
'url': '/statuses/retweeted_by_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedToMe': {
|
||||||
|
'url': '/statuses/retweeted_to_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# User methods
|
||||||
|
'showUser': {
|
||||||
|
'url': '/users/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'searchUsers': {
|
||||||
|
'url': '/users/search.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Status methods - showing, updating, destroying, etc.
|
||||||
|
'showStatus': {
|
||||||
|
'url': '/statuses/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'updateStatus': {
|
||||||
|
'url': '/statuses/update.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyStatus': {
|
||||||
|
'url': '/statuses/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Direct Messages - getting, sending, effing, etc.
|
||||||
|
'getDirectMessages': {
|
||||||
|
'url': '/direct_messages.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSentMessages': {
|
||||||
|
'url': '/direct_messages/sent.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'sendDirectMessage': {
|
||||||
|
'url': '/direct_messages/new.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyDirectMessage': {
|
||||||
|
'url': '/direct_messages/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Friendship methods
|
||||||
|
'checkIfFriendshipExists': {
|
||||||
|
'url': '/friendships/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showFriendship': {
|
||||||
|
'url': '/friendships/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Profile methods
|
||||||
|
'updateProfile': {
|
||||||
|
'url': '/account/update_profile.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateProfileColors': {
|
||||||
|
'url': '/account/update_profile_colors.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Favorites methods
|
||||||
|
'getFavorites': {
|
||||||
|
'url': '/favorites.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFavorite': {
|
||||||
|
'url': '/favorites/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFavorite': {
|
||||||
|
'url': '/favorites/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Blocking methods
|
||||||
|
'createBlock': {
|
||||||
|
'url': '/blocks/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyBlock': {
|
||||||
|
'url': '/blocks/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getBlocking': {
|
||||||
|
'url': '/blocks/blocking.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getBlockedIDs': {
|
||||||
|
'url': '/blocks/blocking/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'checkIfBlockExists': {
|
||||||
|
'url': '/blocks/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Trending methods
|
||||||
|
'getCurrentTrends': {
|
||||||
|
'url': '/trends/current.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getDailyTrends': {
|
||||||
|
'url': '/trends/daily.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getWeeklyTrends': {
|
||||||
|
'url': '/trends/weekly.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'availableTrends': {
|
||||||
|
'url': '/trends/available.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'trendsByLocation': {
|
||||||
|
'url': '/trends/{{woeid}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Saved Searches
|
||||||
|
'getSavedSearches': {
|
||||||
|
'url': '/saved_searches.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showSavedSearch': {
|
||||||
|
'url': '/saved_searches/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createSavedSearch': {
|
||||||
|
'url': '/saved_searches/create.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'destroySavedSearch': {
|
||||||
|
'url': '/saved_searches/destroy/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# List API methods/endpoints. Fairly exhaustive and annoying in general. ;P
|
||||||
|
'createList': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'showLists': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getListMemberships': {
|
||||||
|
'url': '/{{username}}/lists/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'getListTimeline': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSpecificList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'addListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getListMembers': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'subscribeToList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'unsubscribeFromList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
|
||||||
|
# The one-offs
|
||||||
|
'notificationFollow': {
|
||||||
|
'url': '/notifications/follow/follow.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'notificationLeave': {
|
||||||
|
'url': '/notifications/leave/leave.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateDeliveryService': {
|
||||||
|
'url': '/account/update_delivery_device.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'reportSpam': {
|
||||||
|
'url': '/report_spam.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
}
|
||||||
421
oauth_django_example/twitter/twython.py
Normal file
421
oauth_django_example/twitter/twython.py
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Twython is a library for Python that wraps the Twitter API.
|
||||||
|
It aims to abstract away all the API endpoints, so that additions to the library
|
||||||
|
and/or the Twitter API won't cause any overall problems.
|
||||||
|
|
||||||
|
Questions, comments? ryan@venodesigns.net
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Ryan McGrath <ryan@venodesigns.net>"
|
||||||
|
__version__ = "1.3"
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import urlparse
|
||||||
|
import httplib
|
||||||
|
import httplib2
|
||||||
|
import mimetypes
|
||||||
|
import mimetools
|
||||||
|
import re
|
||||||
|
|
||||||
|
import oauth2 as oauth
|
||||||
|
|
||||||
|
# Twython maps keyword based arguments to Twitter API endpoints. The endpoints
|
||||||
|
# table is a file with a dictionary of every API endpoint that Twython supports.
|
||||||
|
from twitter_endpoints import base_url, api_table
|
||||||
|
|
||||||
|
from urllib2 import HTTPError
|
||||||
|
|
||||||
|
# There are some special setups (like, oh, a Django application) where
|
||||||
|
# simplejson exists behind the scenes anyway. Past Python 2.6, this should
|
||||||
|
# never really cause any problems to begin with.
|
||||||
|
try:
|
||||||
|
# Python 2.6 and up
|
||||||
|
import json as simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with)
|
||||||
|
import simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there.
|
||||||
|
from django.utils import simplejson
|
||||||
|
except:
|
||||||
|
# Seriously wtf is wrong with you if you get this Exception.
|
||||||
|
raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/")
|
||||||
|
|
||||||
|
class TwythonError(Exception):
|
||||||
|
"""
|
||||||
|
Generic error class, catch-all for most Twython issues.
|
||||||
|
Special cases are handled by APILimit and AuthError.
|
||||||
|
|
||||||
|
Note: To use these, the syntax has changed as of Twython 1.3. To catch these,
|
||||||
|
you need to explicitly import them into your code, e.g:
|
||||||
|
|
||||||
|
from twython import TwythonError, APILimit, AuthError
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, error_code=None):
|
||||||
|
self.msg = msg
|
||||||
|
if error_code == 400:
|
||||||
|
raise APILimit(msg)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class APILimit(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you've hit an API limit. Try to avoid these, read the API
|
||||||
|
docs if you're running into issues here, Twython does not concern itself with
|
||||||
|
this matter beyond telling you that you've done goofed.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you try to access a protected resource and it fails due to some issue with
|
||||||
|
your authentication.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Twython(object):
|
||||||
|
def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None):
|
||||||
|
"""setup(self, oauth_token = None, headers = None)
|
||||||
|
|
||||||
|
Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
twitter_token - Given to you when you register your application with Twitter.
|
||||||
|
twitter_secret - Given to you when you register your application with Twitter.
|
||||||
|
oauth_token - If you've gone through the authentication process and have a token for this user,
|
||||||
|
pass it in and it'll be used for all requests going forward.
|
||||||
|
oauth_token_secret - see oauth_token; it's the other half.
|
||||||
|
headers - User agent header, dictionary style ala {'User-Agent': 'Bert'}
|
||||||
|
|
||||||
|
** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported.
|
||||||
|
"""
|
||||||
|
# Needed for hitting that there API.
|
||||||
|
self.request_token_url = 'http://twitter.com/oauth/request_token'
|
||||||
|
self.access_token_url = 'http://twitter.com/oauth/access_token'
|
||||||
|
self.authorize_url = 'http://twitter.com/oauth/authorize'
|
||||||
|
self.authenticate_url = 'http://twitter.com/oauth/authenticate'
|
||||||
|
self.twitter_token = twitter_token
|
||||||
|
self.twitter_secret = twitter_secret
|
||||||
|
self.oauth_token = oauth_token
|
||||||
|
self.oauth_secret = oauth_token_secret
|
||||||
|
|
||||||
|
# If there's headers, set them, otherwise be an embarassing parent for their own good.
|
||||||
|
self.headers = headers
|
||||||
|
if self.headers is None:
|
||||||
|
headers = {'User-agent': 'Twython Python Twitter Library v1.3'}
|
||||||
|
|
||||||
|
consumer = None
|
||||||
|
token = None
|
||||||
|
|
||||||
|
if self.twitter_token is not None and self.twitter_secret is not None:
|
||||||
|
consumer = oauth.Consumer(self.twitter_token, self.twitter_secret)
|
||||||
|
|
||||||
|
if self.oauth_token is not None and self.oauth_secret is not None:
|
||||||
|
token = oauth.Token(oauth_token, oauth_token_secret)
|
||||||
|
|
||||||
|
# Filter down through the possibilities here - if they have a token, if they're first stage, etc.
|
||||||
|
if consumer is not None and token is not None:
|
||||||
|
self.client = oauth.Client(consumer, token)
|
||||||
|
elif consumer is not None:
|
||||||
|
self.client = oauth.Client(consumer)
|
||||||
|
else:
|
||||||
|
# If they don't do authentication, but still want to request unprotected resources, we need an opener.
|
||||||
|
self.client = httplib2.Http()
|
||||||
|
|
||||||
|
def __getattr__(self, api_call):
|
||||||
|
"""
|
||||||
|
The most magically awesome block of code you'll see in 2010.
|
||||||
|
|
||||||
|
Rather than list out 9 million damn methods for this API, we just keep a table (see above) of
|
||||||
|
every API endpoint and their corresponding function id for this library. This pretty much gives
|
||||||
|
unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is
|
||||||
|
going to be your bottleneck... well, don't use Python. ;P
|
||||||
|
|
||||||
|
For those who don't get what's going on here, Python classes have this great feature known as __getattr__().
|
||||||
|
It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist,
|
||||||
|
we can take over and find the API method in our table. We then return a function that downloads and parses
|
||||||
|
what we're looking for, based on the keywords passed in.
|
||||||
|
|
||||||
|
I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing".
|
||||||
|
"""
|
||||||
|
def get(self, **kwargs):
|
||||||
|
# Go through and replace any mustaches that are in our API url.
|
||||||
|
fn = api_table[api_call]
|
||||||
|
base = re.sub(
|
||||||
|
'\{\{(?P<m>[a-zA-Z]+)\}\}',
|
||||||
|
lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious.
|
||||||
|
base_url + fn['url']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication
|
||||||
|
if fn['method'] == 'POST':
|
||||||
|
resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs))
|
||||||
|
else:
|
||||||
|
url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()])
|
||||||
|
resp, content = self.client.request(url, fn['method'])
|
||||||
|
|
||||||
|
return simplejson.loads(content)
|
||||||
|
|
||||||
|
if api_call in api_table:
|
||||||
|
return get.__get__(self)
|
||||||
|
else:
|
||||||
|
raise AttributeError, api_call
|
||||||
|
|
||||||
|
def get_authentication_tokens(self):
|
||||||
|
"""
|
||||||
|
get_auth_url(self)
|
||||||
|
|
||||||
|
Returns an authorization URL for a user to hit.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.request_token_url, "GET")
|
||||||
|
|
||||||
|
if resp['status'] != '200':
|
||||||
|
raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content))
|
||||||
|
|
||||||
|
request_tokens = dict(urlparse.parse_qsl(content))
|
||||||
|
request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token'])
|
||||||
|
return request_tokens
|
||||||
|
|
||||||
|
def get_authorized_tokens(self):
|
||||||
|
"""
|
||||||
|
get_authorized_tokens
|
||||||
|
|
||||||
|
Returns authorized tokens after they go through the auth_url phase.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.access_token_url, "GET")
|
||||||
|
return dict(urlparse.parse_qsl(content))
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"):
|
||||||
|
"""shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl")
|
||||||
|
|
||||||
|
Shortens url specified by url_to_shorten.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url_to_shorten - URL to shorten.
|
||||||
|
shortener - In case you want to use a url shortening service other than is.gd.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(
|
||||||
|
shortener + "?" + urllib.urlencode({query: self.unicode2utf8(url_to_shorten)}),
|
||||||
|
"GET"
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("shortenURL() failed with a %s error code." % `e.code`)
|
||||||
|
|
||||||
|
def bulkUserLookup(self, ids = None, screen_names = None, version = None):
|
||||||
|
""" bulkUserLookup(self, ids = None, screen_names = None, version = None)
|
||||||
|
|
||||||
|
A method to do bulk user lookups against the Twitter API. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that
|
||||||
|
contain their respective data sets.
|
||||||
|
|
||||||
|
Statuses for the users in question will be returned inline if they exist. Requires authentication!
|
||||||
|
"""
|
||||||
|
version = version or self.apiVersion
|
||||||
|
if self.authenticated is True:
|
||||||
|
apiURL = "http://api.twitter.com/%d/users/lookup.json?lol=1" % version
|
||||||
|
if ids is not None:
|
||||||
|
apiURL += "&user_id="
|
||||||
|
for id in ids:
|
||||||
|
apiURL += `id` + ","
|
||||||
|
if screen_names is not None:
|
||||||
|
apiURL += "&screen_name="
|
||||||
|
for name in screen_names:
|
||||||
|
apiURL += name + ","
|
||||||
|
try:
|
||||||
|
return simplejson.load(self.opener.open(apiURL))
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
else:
|
||||||
|
raise AuthError("bulkUserLookup() requires you to be authenticated.")
|
||||||
|
|
||||||
|
def searchTwitter(self, **kwargs):
|
||||||
|
"""searchTwitter(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
return simplejson.load(self.opener.open(searchURL))
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
|
||||||
|
def searchTwitterGen(self, **kwargs):
|
||||||
|
"""searchTwitterGen(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns a generator of tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
data = simplejson.load(self.opener.open(searchURL))
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
|
||||||
|
if not data['results']:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
for tweet in data['results']:
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
if 'page' not in kwargs:
|
||||||
|
kwargs['page'] = 2
|
||||||
|
else:
|
||||||
|
kwargs['page'] += 1
|
||||||
|
|
||||||
|
for tweet in self.searchTwitterGen(search_query, **kwargs):
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
def isListMember(self, list_id, id, username, version = None):
|
||||||
|
""" isListMember(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a member of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - User who owns the list you're checking against (username)
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
version = version or self.apiVersion
|
||||||
|
try:
|
||||||
|
return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`)))
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
def isListSubscriber(self, list_id, id, version = None):
|
||||||
|
""" isListSubscriber(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a subscriber of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - Required. The username of the owner of the list that you're seeing if someone is subscribed to.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
version = version or self.apiVersion
|
||||||
|
try:
|
||||||
|
return simplejson.load(self.opener.open("http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`)))
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
# The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set.
|
||||||
|
def updateProfileBackgroundImage(self, filename, tile="true", version = None):
|
||||||
|
""" updateProfileBackgroundImage(filename, tile="true")
|
||||||
|
|
||||||
|
Updates the authenticating user's profile background image.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down.
|
||||||
|
tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise.
|
||||||
|
** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false"
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
version = version or self.apiVersion
|
||||||
|
if self.authenticated is True:
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = self.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers)
|
||||||
|
return self.opener.open(r).read()
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
else:
|
||||||
|
raise AuthError("You realize you need to be authenticated to change a background image, right?")
|
||||||
|
|
||||||
|
def updateProfileImage(self, filename, version = None):
|
||||||
|
""" updateProfileImage(filename)
|
||||||
|
|
||||||
|
Updates the authenticating user's profile image (avatar).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
version = version or self.apiVersion
|
||||||
|
if self.authenticated is True:
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = Twython.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers)
|
||||||
|
return self.opener.open(r).read()
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
else:
|
||||||
|
raise AuthError("You realize you need to be authenticated to change a profile image, right?")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_multipart_formdata(fields, files):
|
||||||
|
BOUNDARY = mimetools.choose_boundary()
|
||||||
|
CRLF = '\r\n'
|
||||||
|
L = []
|
||||||
|
for (key, value) in fields:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
for (key, filename, value) in files:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||||
|
L.append('Content-Type: %s' % Twython.get_content_type(filename))
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
L.append('--' + BOUNDARY + '--')
|
||||||
|
L.append('')
|
||||||
|
body = CRLF.join(L)
|
||||||
|
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||||
|
return content_type, body
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_content_type(filename):
|
||||||
|
""" get_content_type(self, filename)
|
||||||
|
|
||||||
|
Exactly what you think it does. :D
|
||||||
|
"""
|
||||||
|
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unicode2utf8(text):
|
||||||
|
try:
|
||||||
|
if isinstance(text, unicode):
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
73
oauth_django_example/twitter/views.py
Normal file
73
oauth_django_example/twitter/views.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import render_to_response
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
from twython import Twython
|
||||||
|
from twitter.models import Profile
|
||||||
|
|
||||||
|
CONSUMER_KEY = "piKE9TwKoAhJoj7KEMlwGQ"
|
||||||
|
CONSUMER_SECRET = "RA9IzvvzoLAFGOOoOndm1Cvyh94pwPWLy4Grl4dt0o"
|
||||||
|
|
||||||
|
def twitter_logout(request):
|
||||||
|
logout(request)
|
||||||
|
return HttpResponseRedirect('/')
|
||||||
|
|
||||||
|
def twitter_begin_auth(request):
|
||||||
|
# Instantiate Twython with the first leg of our trip.
|
||||||
|
twitter = Twython(
|
||||||
|
twitter_token = CONSUMER_KEY,
|
||||||
|
twitter_secret = CONSUMER_SECRET
|
||||||
|
)
|
||||||
|
|
||||||
|
# Request an authorization url to send the user to...
|
||||||
|
auth_props = twitter.get_authentication_tokens()
|
||||||
|
|
||||||
|
# Then send them over there, durh.
|
||||||
|
request.session['request_token'] = auth_props
|
||||||
|
return HttpResponseRedirect(auth_props['auth_url'])
|
||||||
|
|
||||||
|
def twitter_thanks(request):
|
||||||
|
# Now that we've got the magic tokens back from Twitter, we need to exchange
|
||||||
|
# for permanent ones and store them...
|
||||||
|
twitter = Twython(
|
||||||
|
twitter_token = CONSUMER_KEY,
|
||||||
|
twitter_secret = CONSUMER_SECRET,
|
||||||
|
oauth_token = request.session['request_token']['oauth_token'],
|
||||||
|
oauth_token_secret = request.session['request_token']['oauth_token_secret']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve the tokens we want...
|
||||||
|
authorized_tokens = twitter.get_authorized_tokens()
|
||||||
|
|
||||||
|
# If they already exist, grab them, login and redirect to a page displaying stuff.
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username = authorized_tokens['screen_name'])
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# We mock a creation here; no email, password is just the token, etc.
|
||||||
|
user = User.objects.create_user(authorized_tokens['screen_name'], "fjdsfn@jfndjfn.com", authorized_tokens['oauth_token_secret'])
|
||||||
|
profile = Profile()
|
||||||
|
profile.user = user
|
||||||
|
profile.oauth_token = authorized_tokens['oauth_token']
|
||||||
|
profile.oauth_secret = authorized_tokens['oauth_token_secret']
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
user = authenticate(
|
||||||
|
username = authorized_tokens['screen_name'],
|
||||||
|
password = authorized_tokens['oauth_token_secret']
|
||||||
|
)
|
||||||
|
login(request, user)
|
||||||
|
return HttpResponseRedirect('/timeline')
|
||||||
|
|
||||||
|
def twitter_timeline(request):
|
||||||
|
user = request.user.get_profile()
|
||||||
|
twitter = Twython(
|
||||||
|
twitter_token = CONSUMER_KEY,
|
||||||
|
twitter_secret = CONSUMER_SECRET,
|
||||||
|
oauth_token = user.oauth_token,
|
||||||
|
oauth_token_secret = user.oauth_secret
|
||||||
|
)
|
||||||
|
my_tweets = twitter.getHomeTimeline()
|
||||||
|
print my_tweets
|
||||||
|
return render_to_response('tweets.html', {'tweets': my_tweets})
|
||||||
9
oauth_django_example/urls.py
Normal file
9
oauth_django_example/urls.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
from twitter.views import twitter_begin_auth, twitter_thanks, twitter_logout, twitter_timeline
|
||||||
|
|
||||||
|
urlpatterns = patterns('',
|
||||||
|
(r'^login/?$', twitter_begin_auth),
|
||||||
|
(r'^/logout?$', twitter_logout),
|
||||||
|
(r'^thanks/?$', twitter_thanks), # Where they're redirect to after authorizing
|
||||||
|
(r'^timeline/?$', twitter_timeline),
|
||||||
|
)
|
||||||
8
setup.py
8
setup.py
|
|
@ -4,13 +4,10 @@ import sys, os
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
from setuptools import find_packages
|
from setuptools import find_packages
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'Ryan McGrath <ryan@venodesigns.net>'
|
__author__ = 'Ryan McGrath <ryan@venodesigns.net>'
|
||||||
__version__ = '1.2.1'
|
__version__ = '1.3'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
||||||
# Basic package information.
|
# Basic package information.
|
||||||
name = 'twython',
|
name = 'twython',
|
||||||
version = __version__,
|
version = __version__,
|
||||||
|
|
@ -20,7 +17,7 @@ setup(
|
||||||
include_package_data = True,
|
include_package_data = True,
|
||||||
|
|
||||||
# Package dependencies.
|
# Package dependencies.
|
||||||
install_requires = ['simplejson'],
|
install_requires = ['simplejson', 'oauth2'],
|
||||||
|
|
||||||
# Metadata for PyPI.
|
# Metadata for PyPI.
|
||||||
author = 'Ryan McGrath',
|
author = 'Ryan McGrath',
|
||||||
|
|
@ -38,5 +35,4 @@ setup(
|
||||||
'Topic :: Communications :: Chat',
|
'Topic :: Communications :: Chat',
|
||||||
'Topic :: Internet'
|
'Topic :: Internet'
|
||||||
]
|
]
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
import core, twyauth, streaming
|
from twython import Twython
|
||||||
|
|
|
||||||
1956
twython/core.py
1956
twython/core.py
File diff suppressed because it is too large
Load diff
524
twython/oauth.py
524
twython/oauth.py
|
|
@ -1,524 +0,0 @@
|
||||||
import cgi
|
|
||||||
import urllib
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import urlparse
|
|
||||||
import hmac
|
|
||||||
import binascii
|
|
||||||
|
|
||||||
VERSION = '1.0' # Hi Blaine!
|
|
||||||
HTTP_METHOD = 'GET'
|
|
||||||
SIGNATURE_METHOD = 'PLAINTEXT'
|
|
||||||
|
|
||||||
# Generic exception class
|
|
||||||
class OAuthError(RuntimeError):
|
|
||||||
def __init__(self, message='OAuth error occured.'):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
# optional WWW-Authenticate header (401 error)
|
|
||||||
def build_authenticate_header(realm=''):
|
|
||||||
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
|
|
||||||
|
|
||||||
# url escape
|
|
||||||
def escape(s):
|
|
||||||
# escape '/' too
|
|
||||||
return urllib.quote(s, safe='~')
|
|
||||||
|
|
||||||
# util function: current timestamp
|
|
||||||
# seconds since epoch (UTC)
|
|
||||||
def generate_timestamp():
|
|
||||||
return int(time.time())
|
|
||||||
|
|
||||||
# util function: nonce
|
|
||||||
# pseudorandom number
|
|
||||||
def generate_nonce(length=8):
|
|
||||||
return ''.join([str(random.randint(0, 9)) for i in range(length)])
|
|
||||||
|
|
||||||
# OAuthConsumer is a data type that represents the identity of the Consumer
|
|
||||||
# via its shared secret with the Service Provider.
|
|
||||||
class OAuthConsumer(object):
|
|
||||||
key = None
|
|
||||||
secret = None
|
|
||||||
|
|
||||||
def __init__(self, key, secret):
|
|
||||||
self.key = key
|
|
||||||
self.secret = secret
|
|
||||||
|
|
||||||
# OAuthToken is a data type that represents an End User via either an access
|
|
||||||
# or request token.
|
|
||||||
class OAuthToken(object):
|
|
||||||
# access tokens and request tokens
|
|
||||||
key = None
|
|
||||||
secret = None
|
|
||||||
|
|
||||||
'''
|
|
||||||
key = the token
|
|
||||||
secret = the token secret
|
|
||||||
'''
|
|
||||||
def __init__(self, key, secret):
|
|
||||||
self.key = key
|
|
||||||
self.secret = secret
|
|
||||||
|
|
||||||
def to_string(self):
|
|
||||||
return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
|
|
||||||
|
|
||||||
# return a token from something like:
|
|
||||||
# oauth_token_secret=digg&oauth_token=digg
|
|
||||||
def from_string(s):
|
|
||||||
params = cgi.parse_qs(s, keep_blank_values=False)
|
|
||||||
key = params['oauth_token'][0]
|
|
||||||
secret = params['oauth_token_secret'][0]
|
|
||||||
return OAuthToken(key, secret)
|
|
||||||
from_string = staticmethod(from_string)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.to_string()
|
|
||||||
|
|
||||||
# OAuthRequest represents the request and can be serialized
|
|
||||||
class OAuthRequest(object):
|
|
||||||
'''
|
|
||||||
OAuth parameters:
|
|
||||||
- oauth_consumer_key
|
|
||||||
- oauth_token
|
|
||||||
- oauth_signature_method
|
|
||||||
- oauth_signature
|
|
||||||
- oauth_timestamp
|
|
||||||
- oauth_nonce
|
|
||||||
- oauth_version
|
|
||||||
... any additional parameters, as defined by the Service Provider.
|
|
||||||
'''
|
|
||||||
parameters = None # oauth parameters
|
|
||||||
http_method = HTTP_METHOD
|
|
||||||
http_url = None
|
|
||||||
version = VERSION
|
|
||||||
|
|
||||||
def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
|
|
||||||
self.http_method = http_method
|
|
||||||
self.http_url = http_url
|
|
||||||
self.parameters = parameters or {}
|
|
||||||
|
|
||||||
def set_parameter(self, parameter, value):
|
|
||||||
self.parameters[parameter] = value
|
|
||||||
|
|
||||||
def get_parameter(self, parameter):
|
|
||||||
try:
|
|
||||||
return self.parameters[parameter]
|
|
||||||
except:
|
|
||||||
raise OAuthError('Parameter not found: %s' % parameter)
|
|
||||||
|
|
||||||
def _get_timestamp_nonce(self):
|
|
||||||
return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
|
|
||||||
|
|
||||||
# get any non-oauth parameters
|
|
||||||
def get_nonoauth_parameters(self):
|
|
||||||
parameters = {}
|
|
||||||
for k, v in self.parameters.iteritems():
|
|
||||||
# ignore oauth parameters
|
|
||||||
if k.find('oauth_') < 0:
|
|
||||||
parameters[k] = v
|
|
||||||
return parameters
|
|
||||||
|
|
||||||
# serialize as a header for an HTTPAuth request
|
|
||||||
def to_header(self, realm=''):
|
|
||||||
auth_header = 'OAuth realm="%s"' % realm
|
|
||||||
# add the oauth parameters
|
|
||||||
if self.parameters:
|
|
||||||
for k, v in self.parameters.iteritems():
|
|
||||||
if k[:6] == 'oauth_':
|
|
||||||
auth_header += ', %s="%s"' % (k, escape(str(v)))
|
|
||||||
return {'Authorization': auth_header}
|
|
||||||
|
|
||||||
# serialize as post data for a POST request
|
|
||||||
def to_postdata(self):
|
|
||||||
return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()])
|
|
||||||
|
|
||||||
# serialize as a url for a GET request
|
|
||||||
def to_url(self):
|
|
||||||
return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
|
|
||||||
|
|
||||||
# return a string that consists of all the parameters that need to be signed
|
|
||||||
def get_normalized_parameters(self):
|
|
||||||
params = self.parameters
|
|
||||||
try:
|
|
||||||
# exclude the signature if it exists
|
|
||||||
del params['oauth_signature']
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
key_values = params.items()
|
|
||||||
# sort lexicographically, first after key, then after value
|
|
||||||
key_values.sort()
|
|
||||||
# combine key value pairs in string and escape
|
|
||||||
return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values])
|
|
||||||
|
|
||||||
# just uppercases the http method
|
|
||||||
def get_normalized_http_method(self):
|
|
||||||
return self.http_method.upper()
|
|
||||||
|
|
||||||
# parses the url and rebuilds it to be scheme://host/path
|
|
||||||
def get_normalized_http_url(self):
|
|
||||||
parts = urlparse.urlparse(self.http_url)
|
|
||||||
url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
|
|
||||||
return url_string
|
|
||||||
|
|
||||||
# set the signature parameter to the result of build_signature
|
|
||||||
def sign_request(self, signature_method, consumer, token):
|
|
||||||
# set the signature method
|
|
||||||
self.set_parameter('oauth_signature_method', signature_method.get_name())
|
|
||||||
# set the signature
|
|
||||||
self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
|
|
||||||
|
|
||||||
def build_signature(self, signature_method, consumer, token):
|
|
||||||
# call the build signature method within the signature method
|
|
||||||
return signature_method.build_signature(self, consumer, token)
|
|
||||||
|
|
||||||
def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
|
|
||||||
# combine multiple parameter sources
|
|
||||||
if parameters is None:
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
# headers
|
|
||||||
if headers and 'Authorization' in headers:
|
|
||||||
auth_header = headers['Authorization']
|
|
||||||
# check that the authorization header is OAuth
|
|
||||||
if auth_header.index('OAuth') > -1:
|
|
||||||
try:
|
|
||||||
# get the parameters from the header
|
|
||||||
header_params = OAuthRequest._split_header(auth_header)
|
|
||||||
parameters.update(header_params)
|
|
||||||
except:
|
|
||||||
raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
|
|
||||||
|
|
||||||
# GET or POST query string
|
|
||||||
if query_string:
|
|
||||||
query_params = OAuthRequest._split_url_string(query_string)
|
|
||||||
parameters.update(query_params)
|
|
||||||
|
|
||||||
# URL parameters
|
|
||||||
param_str = urlparse.urlparse(http_url)[4] # query
|
|
||||||
url_params = OAuthRequest._split_url_string(param_str)
|
|
||||||
parameters.update(url_params)
|
|
||||||
|
|
||||||
if parameters:
|
|
||||||
return OAuthRequest(http_method, http_url, parameters)
|
|
||||||
|
|
||||||
return None
|
|
||||||
from_request = staticmethod(from_request)
|
|
||||||
|
|
||||||
def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
|
|
||||||
if not parameters:
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
'oauth_consumer_key': oauth_consumer.key,
|
|
||||||
'oauth_timestamp': generate_timestamp(),
|
|
||||||
'oauth_nonce': generate_nonce(),
|
|
||||||
'oauth_version': OAuthRequest.version,
|
|
||||||
}
|
|
||||||
|
|
||||||
defaults.update(parameters)
|
|
||||||
parameters = defaults
|
|
||||||
|
|
||||||
if token:
|
|
||||||
parameters['oauth_token'] = token.key
|
|
||||||
|
|
||||||
return OAuthRequest(http_method, http_url, parameters)
|
|
||||||
from_consumer_and_token = staticmethod(from_consumer_and_token)
|
|
||||||
|
|
||||||
def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
|
|
||||||
if not parameters:
|
|
||||||
parameters = {}
|
|
||||||
|
|
||||||
parameters['oauth_token'] = token.key
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
parameters['oauth_callback'] = callback
|
|
||||||
|
|
||||||
return OAuthRequest(http_method, http_url, parameters)
|
|
||||||
from_token_and_callback = staticmethod(from_token_and_callback)
|
|
||||||
|
|
||||||
# util function: turn Authorization: header into parameters, has to do some unescaping
|
|
||||||
def _split_header(header):
|
|
||||||
params = {}
|
|
||||||
parts = header.split(',')
|
|
||||||
for param in parts:
|
|
||||||
# ignore realm parameter
|
|
||||||
if param.find('OAuth realm') > -1:
|
|
||||||
continue
|
|
||||||
# remove whitespace
|
|
||||||
param = param.strip()
|
|
||||||
# split key-value
|
|
||||||
param_parts = param.split('=', 1)
|
|
||||||
# remove quotes and unescape the value
|
|
||||||
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
|
|
||||||
return params
|
|
||||||
_split_header = staticmethod(_split_header)
|
|
||||||
|
|
||||||
# util function: turn url string into parameters, has to do some unescaping
|
|
||||||
def _split_url_string(param_str):
|
|
||||||
parameters = cgi.parse_qs(param_str, keep_blank_values=False)
|
|
||||||
for k, v in parameters.iteritems():
|
|
||||||
parameters[k] = urllib.unquote(v[0])
|
|
||||||
return parameters
|
|
||||||
_split_url_string = staticmethod(_split_url_string)
|
|
||||||
|
|
||||||
# OAuthServer is a worker to check a requests validity against a data store
|
|
||||||
class OAuthServer(object):
|
|
||||||
timestamp_threshold = 300 # in seconds, five minutes
|
|
||||||
version = VERSION
|
|
||||||
signature_methods = None
|
|
||||||
data_store = None
|
|
||||||
|
|
||||||
def __init__(self, data_store=None, signature_methods=None):
|
|
||||||
self.data_store = data_store
|
|
||||||
self.signature_methods = signature_methods or {}
|
|
||||||
|
|
||||||
def set_data_store(self, oauth_data_store):
|
|
||||||
self.data_store = data_store
|
|
||||||
|
|
||||||
def get_data_store(self):
|
|
||||||
return self.data_store
|
|
||||||
|
|
||||||
def add_signature_method(self, signature_method):
|
|
||||||
self.signature_methods[signature_method.get_name()] = signature_method
|
|
||||||
return self.signature_methods
|
|
||||||
|
|
||||||
# process a request_token request
|
|
||||||
# returns the request token on success
|
|
||||||
def fetch_request_token(self, oauth_request):
|
|
||||||
try:
|
|
||||||
# get the request token for authorization
|
|
||||||
token = self._get_token(oauth_request, 'request')
|
|
||||||
except OAuthError:
|
|
||||||
# no token required for the initial token request
|
|
||||||
version = self._get_version(oauth_request)
|
|
||||||
consumer = self._get_consumer(oauth_request)
|
|
||||||
self._check_signature(oauth_request, consumer, None)
|
|
||||||
# fetch a new token
|
|
||||||
token = self.data_store.fetch_request_token(consumer)
|
|
||||||
return token
|
|
||||||
|
|
||||||
# process an access_token request
|
|
||||||
# returns the access token on success
|
|
||||||
def fetch_access_token(self, oauth_request):
|
|
||||||
version = self._get_version(oauth_request)
|
|
||||||
consumer = self._get_consumer(oauth_request)
|
|
||||||
# get the request token
|
|
||||||
token = self._get_token(oauth_request, 'request')
|
|
||||||
self._check_signature(oauth_request, consumer, token)
|
|
||||||
new_token = self.data_store.fetch_access_token(consumer, token)
|
|
||||||
return new_token
|
|
||||||
|
|
||||||
# verify an api call, checks all the parameters
|
|
||||||
def verify_request(self, oauth_request):
|
|
||||||
# -> consumer and token
|
|
||||||
version = self._get_version(oauth_request)
|
|
||||||
consumer = self._get_consumer(oauth_request)
|
|
||||||
# get the access token
|
|
||||||
token = self._get_token(oauth_request, 'access')
|
|
||||||
self._check_signature(oauth_request, consumer, token)
|
|
||||||
parameters = oauth_request.get_nonoauth_parameters()
|
|
||||||
return consumer, token, parameters
|
|
||||||
|
|
||||||
# authorize a request token
|
|
||||||
def authorize_token(self, token, user):
|
|
||||||
return self.data_store.authorize_request_token(token, user)
|
|
||||||
|
|
||||||
# get the callback url
|
|
||||||
def get_callback(self, oauth_request):
|
|
||||||
return oauth_request.get_parameter('oauth_callback')
|
|
||||||
|
|
||||||
# optional support for the authenticate header
|
|
||||||
def build_authenticate_header(self, realm=''):
|
|
||||||
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
|
|
||||||
|
|
||||||
# verify the correct version request for this server
|
|
||||||
def _get_version(self, oauth_request):
|
|
||||||
try:
|
|
||||||
version = oauth_request.get_parameter('oauth_version')
|
|
||||||
except:
|
|
||||||
version = VERSION
|
|
||||||
if version and version != self.version:
|
|
||||||
raise OAuthError('OAuth version %s not supported.' % str(version))
|
|
||||||
return version
|
|
||||||
|
|
||||||
# figure out the signature with some defaults
|
|
||||||
def _get_signature_method(self, oauth_request):
|
|
||||||
try:
|
|
||||||
signature_method = oauth_request.get_parameter('oauth_signature_method')
|
|
||||||
except:
|
|
||||||
signature_method = SIGNATURE_METHOD
|
|
||||||
try:
|
|
||||||
# get the signature method object
|
|
||||||
signature_method = self.signature_methods[signature_method]
|
|
||||||
except:
|
|
||||||
signature_method_names = ', '.join(self.signature_methods.keys())
|
|
||||||
raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
|
|
||||||
|
|
||||||
return signature_method
|
|
||||||
|
|
||||||
def _get_consumer(self, oauth_request):
|
|
||||||
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
|
|
||||||
if not consumer_key:
|
|
||||||
raise OAuthError('Invalid consumer key.')
|
|
||||||
consumer = self.data_store.lookup_consumer(consumer_key)
|
|
||||||
if not consumer:
|
|
||||||
raise OAuthError('Invalid consumer.')
|
|
||||||
return consumer
|
|
||||||
|
|
||||||
# try to find the token for the provided request token key
|
|
||||||
def _get_token(self, oauth_request, token_type='access'):
|
|
||||||
token_field = oauth_request.get_parameter('oauth_token')
|
|
||||||
token = self.data_store.lookup_token(token_type, token_field)
|
|
||||||
if not token:
|
|
||||||
raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
|
|
||||||
return token
|
|
||||||
|
|
||||||
def _check_signature(self, oauth_request, consumer, token):
|
|
||||||
timestamp, nonce = oauth_request._get_timestamp_nonce()
|
|
||||||
self._check_timestamp(timestamp)
|
|
||||||
self._check_nonce(consumer, token, nonce)
|
|
||||||
signature_method = self._get_signature_method(oauth_request)
|
|
||||||
try:
|
|
||||||
signature = oauth_request.get_parameter('oauth_signature')
|
|
||||||
except:
|
|
||||||
raise OAuthError('Missing signature.')
|
|
||||||
# validate the signature
|
|
||||||
valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
|
|
||||||
if not valid_sig:
|
|
||||||
key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
|
|
||||||
raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
|
|
||||||
built = signature_method.build_signature(oauth_request, consumer, token)
|
|
||||||
|
|
||||||
def _check_timestamp(self, timestamp):
|
|
||||||
# verify that timestamp is recentish
|
|
||||||
timestamp = int(timestamp)
|
|
||||||
now = int(time.time())
|
|
||||||
lapsed = now - timestamp
|
|
||||||
if lapsed > self.timestamp_threshold:
|
|
||||||
raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
|
|
||||||
|
|
||||||
def _check_nonce(self, consumer, token, nonce):
|
|
||||||
# verify that the nonce is uniqueish
|
|
||||||
nonce = self.data_store.lookup_nonce(consumer, token, nonce)
|
|
||||||
if nonce:
|
|
||||||
raise OAuthError('Nonce already used: %s' % str(nonce))
|
|
||||||
|
|
||||||
# OAuthClient is a worker to attempt to execute a request
|
|
||||||
class OAuthClient(object):
|
|
||||||
consumer = None
|
|
||||||
token = None
|
|
||||||
|
|
||||||
def __init__(self, oauth_consumer, oauth_token):
|
|
||||||
self.consumer = oauth_consumer
|
|
||||||
self.token = oauth_token
|
|
||||||
|
|
||||||
def get_consumer(self):
|
|
||||||
return self.consumer
|
|
||||||
|
|
||||||
def get_token(self):
|
|
||||||
return self.token
|
|
||||||
|
|
||||||
def fetch_request_token(self, oauth_request):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def fetch_access_token(self, oauth_request):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def access_resource(self, oauth_request):
|
|
||||||
# -> some protected resource
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
# OAuthDataStore is a database abstraction used to lookup consumers and tokens
|
|
||||||
class OAuthDataStore(object):
|
|
||||||
|
|
||||||
def lookup_consumer(self, key):
|
|
||||||
# -> OAuthConsumer
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def lookup_token(self, oauth_consumer, token_type, token_token):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def fetch_request_token(self, oauth_consumer):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def fetch_access_token(self, oauth_consumer, oauth_token):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def authorize_request_token(self, oauth_token, user):
|
|
||||||
# -> OAuthToken
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
# OAuthSignatureMethod is a strategy class that implements a signature method
|
|
||||||
class OAuthSignatureMethod(object):
|
|
||||||
def get_name(self):
|
|
||||||
# -> str
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
|
|
||||||
# -> str key, str raw
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def build_signature(self, oauth_request, oauth_consumer, oauth_token):
|
|
||||||
# -> str
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def check_signature(self, oauth_request, consumer, token, signature):
|
|
||||||
built = self.build_signature(oauth_request, consumer, token)
|
|
||||||
return built == signature
|
|
||||||
|
|
||||||
class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return 'HMAC-SHA1'
|
|
||||||
|
|
||||||
def build_signature_base_string(self, oauth_request, consumer, token):
|
|
||||||
sig = (
|
|
||||||
escape(oauth_request.get_normalized_http_method()),
|
|
||||||
escape(oauth_request.get_normalized_http_url()),
|
|
||||||
escape(oauth_request.get_normalized_parameters()),
|
|
||||||
)
|
|
||||||
|
|
||||||
key = '%s&' % escape(consumer.secret)
|
|
||||||
if token:
|
|
||||||
key += escape(token.secret)
|
|
||||||
raw = '&'.join(sig)
|
|
||||||
return key, raw
|
|
||||||
|
|
||||||
def build_signature(self, oauth_request, consumer, token):
|
|
||||||
# build the base signature string
|
|
||||||
key, raw = self.build_signature_base_string(oauth_request, consumer, token)
|
|
||||||
|
|
||||||
# hmac object
|
|
||||||
try:
|
|
||||||
import hashlib # 2.5
|
|
||||||
hashed = hmac.new(key, raw, hashlib.sha1)
|
|
||||||
except:
|
|
||||||
import sha # deprecated
|
|
||||||
hashed = hmac.new(key, raw, sha)
|
|
||||||
|
|
||||||
# calculate the digest base 64
|
|
||||||
return binascii.b2a_base64(hashed.digest())[:-1]
|
|
||||||
|
|
||||||
class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
return 'PLAINTEXT'
|
|
||||||
|
|
||||||
def build_signature_base_string(self, oauth_request, consumer, token):
|
|
||||||
# concatenate the consumer key and secret
|
|
||||||
sig = escape(consumer.secret) + '&'
|
|
||||||
if token:
|
|
||||||
sig = sig + escape(token.secret)
|
|
||||||
return sig
|
|
||||||
|
|
||||||
def build_signature(self, oauth_request, consumer, token):
|
|
||||||
return self.build_signature_base_string(oauth_request, consumer, token)
|
|
||||||
299
twython/twitter_endpoints.py
Normal file
299
twython/twitter_endpoints.py
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
"""
|
||||||
|
A huge map of every Twitter API endpoint to a function definition in Twython.
|
||||||
|
|
||||||
|
Parameters that need to be embedded in the URL are treated with mustaches, e.g:
|
||||||
|
|
||||||
|
{{version}}, etc
|
||||||
|
|
||||||
|
When creating new endpoint definitions, keep in mind that the name of the mustache
|
||||||
|
will be replaced with the keyword that gets passed in to the function at call time.
|
||||||
|
|
||||||
|
i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
|
||||||
|
with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base Twitter API url, no need to repeat this junk...
|
||||||
|
base_url = 'http://api.twitter.com/{{version}}'
|
||||||
|
|
||||||
|
api_table = {
|
||||||
|
'getRateLimitStatus': {
|
||||||
|
'url': '/account/rate_limit_status.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Timeline methods
|
||||||
|
'getPublicTimeline': {
|
||||||
|
'url': '/statuses/public_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getHomeTimeline': {
|
||||||
|
'url': '/statuses/home_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getUserTimeline': {
|
||||||
|
'url': '/statuses/user_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsTimeline': {
|
||||||
|
'url': '/statuses/friends_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Interfacing with friends/followers
|
||||||
|
'getUserMentions': {
|
||||||
|
'url': '/statuses/mentions.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsStatus': {
|
||||||
|
'url': '/statuses/friends.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersStatus': {
|
||||||
|
'url': '/statuses/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFriendship': {
|
||||||
|
'url': '/friendships/create.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFriendship': {
|
||||||
|
'url': '/friendships/destroy.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getFriendsIDs': {
|
||||||
|
'url': '/friends/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersIDs': {
|
||||||
|
'url': '/followers/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Retweets
|
||||||
|
'reTweet': {
|
||||||
|
'url': '/statuses/retweet/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getRetweets': {
|
||||||
|
'url': '/statuses/retweets/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedOfMe': {
|
||||||
|
'url': '/statuses/retweets_of_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedByMe': {
|
||||||
|
'url': '/statuses/retweeted_by_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedToMe': {
|
||||||
|
'url': '/statuses/retweeted_to_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# User methods
|
||||||
|
'showUser': {
|
||||||
|
'url': '/users/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'searchUsers': {
|
||||||
|
'url': '/users/search.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Status methods - showing, updating, destroying, etc.
|
||||||
|
'showStatus': {
|
||||||
|
'url': '/statuses/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'updateStatus': {
|
||||||
|
'url': '/statuses/update.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyStatus': {
|
||||||
|
'url': '/statuses/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Direct Messages - getting, sending, effing, etc.
|
||||||
|
'getDirectMessages': {
|
||||||
|
'url': '/direct_messages.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSentMessages': {
|
||||||
|
'url': '/direct_messages/sent.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'sendDirectMessage': {
|
||||||
|
'url': '/direct_messages/new.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyDirectMessage': {
|
||||||
|
'url': '/direct_messages/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Friendship methods
|
||||||
|
'checkIfFriendshipExists': {
|
||||||
|
'url': '/friendships/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showFriendship': {
|
||||||
|
'url': '/friendships/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Profile methods
|
||||||
|
'updateProfile': {
|
||||||
|
'url': '/account/update_profile.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateProfileColors': {
|
||||||
|
'url': '/account/update_profile_colors.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Favorites methods
|
||||||
|
'getFavorites': {
|
||||||
|
'url': '/favorites.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFavorite': {
|
||||||
|
'url': '/favorites/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFavorite': {
|
||||||
|
'url': '/favorites/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Blocking methods
|
||||||
|
'createBlock': {
|
||||||
|
'url': '/blocks/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyBlock': {
|
||||||
|
'url': '/blocks/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getBlocking': {
|
||||||
|
'url': '/blocks/blocking.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getBlockedIDs': {
|
||||||
|
'url': '/blocks/blocking/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'checkIfBlockExists': {
|
||||||
|
'url': '/blocks/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Trending methods
|
||||||
|
'getCurrentTrends': {
|
||||||
|
'url': '/trends/current.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getDailyTrends': {
|
||||||
|
'url': '/trends/daily.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getWeeklyTrends': {
|
||||||
|
'url': '/trends/weekly.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'availableTrends': {
|
||||||
|
'url': '/trends/available.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'trendsByLocation': {
|
||||||
|
'url': '/trends/{{woeid}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Saved Searches
|
||||||
|
'getSavedSearches': {
|
||||||
|
'url': '/saved_searches.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showSavedSearch': {
|
||||||
|
'url': '/saved_searches/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createSavedSearch': {
|
||||||
|
'url': '/saved_searches/create.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'destroySavedSearch': {
|
||||||
|
'url': '/saved_searches/destroy/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# List API methods/endpoints. Fairly exhaustive and annoying in general. ;P
|
||||||
|
'createList': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'showLists': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getListMemberships': {
|
||||||
|
'url': '/{{username}}/lists/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'getListTimeline': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSpecificList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'addListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getListMembers': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'subscribeToList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'unsubscribeFromList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
|
||||||
|
# The one-offs
|
||||||
|
'notificationFollow': {
|
||||||
|
'url': '/notifications/follow/follow.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'notificationLeave': {
|
||||||
|
'url': '/notifications/leave/leave.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateDeliveryService': {
|
||||||
|
'url': '/account/update_delivery_device.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'reportSpam': {
|
||||||
|
'url': '/report_spam.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
#!/usr/bin/python
|
|
||||||
|
|
||||||
"""
|
|
||||||
Twython-oauth (twyauth) is a separate library to handle OAuth routines with Twython. This currently doesn't work, as I never get the time to finish it.
|
|
||||||
Feel free to help out.
|
|
||||||
|
|
||||||
Questions, comments? ryan@venodesigns.net
|
|
||||||
"""
|
|
||||||
|
|
||||||
import httplib, urllib, urllib2, mimetypes, mimetools
|
|
||||||
|
|
||||||
from urlparse import urlparse
|
|
||||||
from urllib2 import HTTPError
|
|
||||||
|
|
||||||
try:
|
|
||||||
import oauth
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class oauth:
|
|
||||||
def __init__(self, username, consumer_key, consumer_secret, signature_method = None, headers = None, version = 1):
|
|
||||||
"""oauth(username = None, consumer_secret = None, consumer_key = None, headers = None)
|
|
||||||
|
|
||||||
Instantiates an instance of Twython with OAuth. Takes optional parameters for authentication and such (see below).
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
username - Your Twitter username, if you want Basic (HTTP) Authentication.
|
|
||||||
consumer_secret - Consumer secret, given to you when you register your App with Twitter.
|
|
||||||
consumer_key - Consumer key (see situation with consumer_secret).
|
|
||||||
signature_method - Method for signing OAuth requests; defaults to oauth.OAuthSignatureMethod_HMAC_SHA1()
|
|
||||||
headers - User agent header.
|
|
||||||
version (number) - Twitter supports a "versioned" API as of Oct. 16th, 2009 - this defaults to 1, but can be overridden on a class and function-based basis.
|
|
||||||
"""
|
|
||||||
# OAuth specific variables below
|
|
||||||
self.request_token_url = 'https://api.twitter.com/%s/oauth/request_token' % version
|
|
||||||
self.access_token_url = 'https://api.twitter.com/%s/oauth/access_token' % version
|
|
||||||
self.authorization_url = 'http://api.twitter.com/%s/oauth/authorize' % version
|
|
||||||
self.signin_url = 'http://api.twitter.com/%s/oauth/authenticate' % version
|
|
||||||
self.consumer_key = consumer_key
|
|
||||||
self.consumer_secret = consumer_secret
|
|
||||||
self.request_token = None
|
|
||||||
self.access_token = None
|
|
||||||
self.consumer = None
|
|
||||||
self.connection = None
|
|
||||||
self.signature_method = None
|
|
||||||
self.consumer = oauth.OAuthConsumer(self.consumer_key, self.consumer_secret)
|
|
||||||
self.connection = httplib.HTTPSConnection("http://api.twitter.com")
|
|
||||||
|
|
||||||
def getOAuthResource(self, url, access_token, params, http_method="GET"):
|
|
||||||
"""getOAuthResource(self, url, access_token, params, http_method="GET")
|
|
||||||
|
|
||||||
Returns a signed OAuth object for use in requests.
|
|
||||||
"""
|
|
||||||
newRequest = oauth.OAuthRequest.from_consumer_and_token(consumer, token=self.access_token, http_method=http_method, http_url=url, parameters=parameters)
|
|
||||||
oauth_request.sign_request(self.signature_method, consumer, access_token)
|
|
||||||
return oauth_request
|
|
||||||
|
|
||||||
def getResponse(self, oauth_request, connection):
|
|
||||||
"""getResponse(self, oauth_request, connection)
|
|
||||||
|
|
||||||
Returns a JSON-ified list of results.
|
|
||||||
"""
|
|
||||||
url = oauth_request.to_url()
|
|
||||||
connection.request(oauth_request.http_method, url)
|
|
||||||
response = connection.getresponse()
|
|
||||||
return simplejson.load(response.read())
|
|
||||||
|
|
||||||
def getUnauthorisedRequestToken(self, consumer, connection, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()):
|
|
||||||
oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, consumer, http_url=self.request_token_url)
|
|
||||||
oauth_request.sign_request(signature_method, consumer, None)
|
|
||||||
resp = fetch_response(oauth_request, connection)
|
|
||||||
return oauth.OAuthToken.from_string(resp)
|
|
||||||
|
|
||||||
def getAuthorizationURL(self, consumer, token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()):
|
|
||||||
oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_url=self.authorization_url)
|
|
||||||
oauth_request.sign_request(signature_method, consumer, token)
|
|
||||||
return oauth_request.to_url()
|
|
||||||
|
|
||||||
def exchangeRequestTokenForAccessToken(self, consumer, connection, request_token, signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()):
|
|
||||||
# May not be needed...
|
|
||||||
self.request_token = request_token
|
|
||||||
oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token = request_token, http_url=self.access_token_url)
|
|
||||||
oauth_request.sign_request(signature_method, consumer, request_token)
|
|
||||||
resp = fetch_response(oauth_request, connection)
|
|
||||||
return oauth.OAuthToken.from_string(resp)
|
|
||||||
417
twython/twython.py
Normal file
417
twython/twython.py
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Twython is a library for Python that wraps the Twitter API.
|
||||||
|
It aims to abstract away all the API endpoints, so that additions to the library
|
||||||
|
and/or the Twitter API won't cause any overall problems.
|
||||||
|
|
||||||
|
Questions, comments? ryan@venodesigns.net
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Ryan McGrath <ryan@venodesigns.net>"
|
||||||
|
__version__ = "1.3"
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import urlparse
|
||||||
|
import httplib
|
||||||
|
import httplib2
|
||||||
|
import mimetypes
|
||||||
|
import mimetools
|
||||||
|
import re
|
||||||
|
|
||||||
|
import oauth2 as oauth
|
||||||
|
|
||||||
|
# Twython maps keyword based arguments to Twitter API endpoints. The endpoints
|
||||||
|
# table is a file with a dictionary of every API endpoint that Twython supports.
|
||||||
|
from twitter_endpoints import base_url, api_table
|
||||||
|
|
||||||
|
from urllib2 import HTTPError
|
||||||
|
|
||||||
|
# There are some special setups (like, oh, a Django application) where
|
||||||
|
# simplejson exists behind the scenes anyway. Past Python 2.6, this should
|
||||||
|
# never really cause any problems to begin with.
|
||||||
|
try:
|
||||||
|
# Python 2.6 and up
|
||||||
|
import json as simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with)
|
||||||
|
import simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there.
|
||||||
|
from django.utils import simplejson
|
||||||
|
except:
|
||||||
|
# Seriously wtf is wrong with you if you get this Exception.
|
||||||
|
raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/")
|
||||||
|
|
||||||
|
class TwythonError(Exception):
|
||||||
|
"""
|
||||||
|
Generic error class, catch-all for most Twython issues.
|
||||||
|
Special cases are handled by APILimit and AuthError.
|
||||||
|
|
||||||
|
Note: To use these, the syntax has changed as of Twython 1.3. To catch these,
|
||||||
|
you need to explicitly import them into your code, e.g:
|
||||||
|
|
||||||
|
from twython import TwythonError, APILimit, AuthError
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, error_code=None):
|
||||||
|
self.msg = msg
|
||||||
|
if error_code == 400:
|
||||||
|
raise APILimit(msg)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class APILimit(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you've hit an API limit. Try to avoid these, read the API
|
||||||
|
docs if you're running into issues here, Twython does not concern itself with
|
||||||
|
this matter beyond telling you that you've done goofed.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you try to access a protected resource and it fails due to some issue with
|
||||||
|
your authentication.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Twython(object):
|
||||||
|
def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None):
|
||||||
|
"""setup(self, oauth_token = None, headers = None)
|
||||||
|
|
||||||
|
Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
twitter_token - Given to you when you register your application with Twitter.
|
||||||
|
twitter_secret - Given to you when you register your application with Twitter.
|
||||||
|
oauth_token - If you've gone through the authentication process and have a token for this user,
|
||||||
|
pass it in and it'll be used for all requests going forward.
|
||||||
|
oauth_token_secret - see oauth_token; it's the other half.
|
||||||
|
headers - User agent header, dictionary style ala {'User-Agent': 'Bert'}
|
||||||
|
|
||||||
|
** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported.
|
||||||
|
"""
|
||||||
|
# Needed for hitting that there API.
|
||||||
|
self.request_token_url = 'http://twitter.com/oauth/request_token'
|
||||||
|
self.access_token_url = 'http://twitter.com/oauth/access_token'
|
||||||
|
self.authorize_url = 'http://twitter.com/oauth/authorize'
|
||||||
|
self.authenticate_url = 'http://twitter.com/oauth/authenticate'
|
||||||
|
self.twitter_token = twitter_token
|
||||||
|
self.twitter_secret = twitter_secret
|
||||||
|
self.oauth_token = oauth_token
|
||||||
|
self.oauth_secret = oauth_token_secret
|
||||||
|
|
||||||
|
# If there's headers, set them, otherwise be an embarassing parent for their own good.
|
||||||
|
self.headers = headers
|
||||||
|
if self.headers is None:
|
||||||
|
headers = {'User-agent': 'Twython Python Twitter Library v1.3'}
|
||||||
|
|
||||||
|
consumer = None
|
||||||
|
token = None
|
||||||
|
|
||||||
|
if self.twitter_token is not None and self.twitter_secret is not None:
|
||||||
|
consumer = oauth.Consumer(self.twitter_token, self.twitter_secret)
|
||||||
|
|
||||||
|
if self.oauth_token is not None and self.oauth_secret is not None:
|
||||||
|
token = oauth.Token(oauth_token, oauth_token_secret)
|
||||||
|
|
||||||
|
# Filter down through the possibilities here - if they have a token, if they're first stage, etc.
|
||||||
|
if consumer is not None and token is not None:
|
||||||
|
self.client = oauth.Client(consumer, token)
|
||||||
|
elif consumer is not None:
|
||||||
|
self.client = oauth.Client(consumer)
|
||||||
|
else:
|
||||||
|
# If they don't do authentication, but still want to request unprotected resources, we need an opener.
|
||||||
|
self.client = httplib2.Http()
|
||||||
|
|
||||||
|
def __getattr__(self, api_call):
|
||||||
|
"""
|
||||||
|
The most magically awesome block of code you'll see in 2010.
|
||||||
|
|
||||||
|
Rather than list out 9 million damn methods for this API, we just keep a table (see above) of
|
||||||
|
every API endpoint and their corresponding function id for this library. This pretty much gives
|
||||||
|
unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is
|
||||||
|
going to be your bottleneck... well, don't use Python. ;P
|
||||||
|
|
||||||
|
For those who don't get what's going on here, Python classes have this great feature known as __getattr__().
|
||||||
|
It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist,
|
||||||
|
we can take over and find the API method in our table. We then return a function that downloads and parses
|
||||||
|
what we're looking for, based on the keywords passed in.
|
||||||
|
|
||||||
|
I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing".
|
||||||
|
"""
|
||||||
|
def get(self, **kwargs):
|
||||||
|
# Go through and replace any mustaches that are in our API url.
|
||||||
|
fn = api_table[api_call]
|
||||||
|
base = re.sub(
|
||||||
|
'\{\{(?P<m>[a-zA-Z]+)\}\}',
|
||||||
|
lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious.
|
||||||
|
base_url + fn['url']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication
|
||||||
|
if fn['method'] == 'POST':
|
||||||
|
resp, content = self.client.request(base, fn['method'], urllib.urlencode(kwargs))
|
||||||
|
else:
|
||||||
|
url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()])
|
||||||
|
resp, content = self.client.request(url, fn['method'])
|
||||||
|
|
||||||
|
return simplejson.loads(content)
|
||||||
|
|
||||||
|
if api_call in api_table:
|
||||||
|
return get.__get__(self)
|
||||||
|
else:
|
||||||
|
raise AttributeError, api_call
|
||||||
|
|
||||||
|
def get_authentication_tokens(self):
|
||||||
|
"""
|
||||||
|
get_auth_url(self)
|
||||||
|
|
||||||
|
Returns an authorization URL for a user to hit.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.request_token_url, "GET")
|
||||||
|
|
||||||
|
if resp['status'] != '200':
|
||||||
|
raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content))
|
||||||
|
|
||||||
|
request_tokens = dict(urlparse.parse_qsl(content))
|
||||||
|
request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token'])
|
||||||
|
return request_tokens
|
||||||
|
|
||||||
|
def get_authorized_tokens(self):
|
||||||
|
"""
|
||||||
|
get_authorized_tokens
|
||||||
|
|
||||||
|
Returns authorized tokens after they go through the auth_url phase.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.access_token_url, "GET")
|
||||||
|
return dict(urlparse.parse_qsl(content))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------------------------------
|
||||||
|
# The following methods are all different in some manner or require special attention with regards to the Twitter API.
|
||||||
|
# Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able,
|
||||||
|
# but it's not high on the priority list at the moment.
|
||||||
|
# ------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"):
|
||||||
|
"""shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl")
|
||||||
|
|
||||||
|
Shortens url specified by url_to_shorten.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url_to_shorten - URL to shorten.
|
||||||
|
shortener - In case you want to use a url shortening service other than is.gd.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(
|
||||||
|
shortener + "?" + urllib.urlencode({query: Twython.unicode2utf8(url_to_shorten)}),
|
||||||
|
"GET"
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("shortenURL() failed with a %s error code." % `e.code`)
|
||||||
|
|
||||||
|
def bulkUserLookup(self, ids = None, screen_names = None, version = None):
|
||||||
|
""" bulkUserLookup(self, ids = None, screen_names = None, version = None)
|
||||||
|
|
||||||
|
A method to do bulk user lookups against the Twitter API. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that
|
||||||
|
contain their respective data sets.
|
||||||
|
|
||||||
|
Statuses for the users in question will be returned inline if they exist. Requires authentication!
|
||||||
|
"""
|
||||||
|
apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1"
|
||||||
|
if ids is not None:
|
||||||
|
apiURL += "&user_id="
|
||||||
|
for id in ids:
|
||||||
|
apiURL += `id` + ","
|
||||||
|
if screen_names is not None:
|
||||||
|
apiURL += "&screen_name="
|
||||||
|
for name in screen_names:
|
||||||
|
apiURL += name + ","
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(apiURL, "GET")
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("bulkUserLookup() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
|
||||||
|
def searchTwitter(self, **kwargs):
|
||||||
|
"""searchTwitter(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(searchURL, "GET")
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("getSearchTimeline() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
|
||||||
|
def searchTwitterGen(self, **kwargs):
|
||||||
|
"""searchTwitterGen(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns a generator of tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(searchURL, "GET")
|
||||||
|
data = simplejson.loads(content)
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("searchTwitterGen() failed with a %s error code." % `e.code`, e.code)
|
||||||
|
|
||||||
|
if not data['results']:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
for tweet in data['results']:
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
if 'page' not in kwargs:
|
||||||
|
kwargs['page'] = 2
|
||||||
|
else:
|
||||||
|
kwargs['page'] += 1
|
||||||
|
|
||||||
|
for tweet in self.searchTwitterGen(search_query, **kwargs):
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
def isListMember(self, list_id, id, username, version = 1):
|
||||||
|
""" isListMember(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a member of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - User who owns the list you're checking against (username)
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, `id`))
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
def isListSubscriber(self, list_id, id, version = 1):
|
||||||
|
""" isListSubscriber(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a subscriber of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - Required. The username of the owner of the list that you're seeing if someone is subscribed to.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, `id`)
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
# The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set.
|
||||||
|
def updateProfileBackgroundImage(self, filename, tile="true", version = 1):
|
||||||
|
""" updateProfileBackgroundImage(filename, tile="true")
|
||||||
|
|
||||||
|
Updates the authenticating user's profile background image.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down.
|
||||||
|
tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise.
|
||||||
|
** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false"
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = Twython.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers)
|
||||||
|
return urllib2.urlopen(r).read()
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
def updateProfileImage(self, filename, version = 1):
|
||||||
|
""" updateProfileImage(filename)
|
||||||
|
|
||||||
|
Updates the authenticating user's profile image (avatar).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = Twython.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers)
|
||||||
|
return urllib2.urlopen(r).read()
|
||||||
|
except HTTPError, e:
|
||||||
|
raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_multipart_formdata(fields, files):
|
||||||
|
BOUNDARY = mimetools.choose_boundary()
|
||||||
|
CRLF = '\r\n'
|
||||||
|
L = []
|
||||||
|
for (key, value) in fields:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
for (key, filename, value) in files:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||||
|
L.append('Content-Type: %s' % Twython.get_content_type(filename))
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
L.append('--' + BOUNDARY + '--')
|
||||||
|
L.append('')
|
||||||
|
body = CRLF.join(L)
|
||||||
|
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||||
|
return content_type, body
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_content_type(filename):
|
||||||
|
""" get_content_type(self, filename)
|
||||||
|
|
||||||
|
Exactly what you think it does. :D
|
||||||
|
"""
|
||||||
|
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unicode2utf8(text):
|
||||||
|
try:
|
||||||
|
if isinstance(text, unicode):
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
1896
twython3k/core.py
1896
twython3k/core.py
File diff suppressed because it is too large
Load diff
299
twython3k/twitter_endpoints.py
Normal file
299
twython3k/twitter_endpoints.py
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
"""
|
||||||
|
A huge map of every Twitter API endpoint to a function definition in Twython.
|
||||||
|
|
||||||
|
Parameters that need to be embedded in the URL are treated with mustaches, e.g:
|
||||||
|
|
||||||
|
{{version}}, etc
|
||||||
|
|
||||||
|
When creating new endpoint definitions, keep in mind that the name of the mustache
|
||||||
|
will be replaced with the keyword that gets passed in to the function at call time.
|
||||||
|
|
||||||
|
i.e, in this case, if I pass version = 47 to any function, {{version}} will be replaced
|
||||||
|
with 47, instead of defaulting to 1 (said defaulting takes place at conversion time).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Base Twitter API url, no need to repeat this junk...
|
||||||
|
base_url = 'http://api.twitter.com/{{version}}'
|
||||||
|
|
||||||
|
api_table = {
|
||||||
|
'getRateLimitStatus': {
|
||||||
|
'url': '/account/rate_limit_status.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Timeline methods
|
||||||
|
'getPublicTimeline': {
|
||||||
|
'url': '/statuses/public_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getHomeTimeline': {
|
||||||
|
'url': '/statuses/home_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getUserTimeline': {
|
||||||
|
'url': '/statuses/user_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsTimeline': {
|
||||||
|
'url': '/statuses/friends_timeline.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Interfacing with friends/followers
|
||||||
|
'getUserMentions': {
|
||||||
|
'url': '/statuses/mentions.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFriendsStatus': {
|
||||||
|
'url': '/statuses/friends.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersStatus': {
|
||||||
|
'url': '/statuses/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFriendship': {
|
||||||
|
'url': '/friendships/create.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFriendship': {
|
||||||
|
'url': '/friendships/destroy.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getFriendsIDs': {
|
||||||
|
'url': '/friends/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getFollowersIDs': {
|
||||||
|
'url': '/followers/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Retweets
|
||||||
|
'reTweet': {
|
||||||
|
'url': '/statuses/retweet/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getRetweets': {
|
||||||
|
'url': '/statuses/retweets/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedOfMe': {
|
||||||
|
'url': '/statuses/retweets_of_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedByMe': {
|
||||||
|
'url': '/statuses/retweeted_by_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'retweetedToMe': {
|
||||||
|
'url': '/statuses/retweeted_to_me.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# User methods
|
||||||
|
'showUser': {
|
||||||
|
'url': '/users/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'searchUsers': {
|
||||||
|
'url': '/users/search.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Status methods - showing, updating, destroying, etc.
|
||||||
|
'showStatus': {
|
||||||
|
'url': '/statuses/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'updateStatus': {
|
||||||
|
'url': '/statuses/update.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyStatus': {
|
||||||
|
'url': '/statuses/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Direct Messages - getting, sending, effing, etc.
|
||||||
|
'getDirectMessages': {
|
||||||
|
'url': '/direct_messages.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSentMessages': {
|
||||||
|
'url': '/direct_messages/sent.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'sendDirectMessage': {
|
||||||
|
'url': '/direct_messages/new.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyDirectMessage': {
|
||||||
|
'url': '/direct_messages/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Friendship methods
|
||||||
|
'checkIfFriendshipExists': {
|
||||||
|
'url': '/friendships/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showFriendship': {
|
||||||
|
'url': '/friendships/show.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Profile methods
|
||||||
|
'updateProfile': {
|
||||||
|
'url': '/account/update_profile.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateProfileColors': {
|
||||||
|
'url': '/account/update_profile_colors.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Favorites methods
|
||||||
|
'getFavorites': {
|
||||||
|
'url': '/favorites.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createFavorite': {
|
||||||
|
'url': '/favorites/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyFavorite': {
|
||||||
|
'url': '/favorites/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Blocking methods
|
||||||
|
'createBlock': {
|
||||||
|
'url': '/blocks/create/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'destroyBlock': {
|
||||||
|
'url': '/blocks/destroy/{{id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getBlocking': {
|
||||||
|
'url': '/blocks/blocking.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getBlockedIDs': {
|
||||||
|
'url': '/blocks/blocking/ids.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'checkIfBlockExists': {
|
||||||
|
'url': '/blocks/exists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Trending methods
|
||||||
|
'getCurrentTrends': {
|
||||||
|
'url': '/trends/current.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getDailyTrends': {
|
||||||
|
'url': '/trends/daily.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getWeeklyTrends': {
|
||||||
|
'url': '/trends/weekly.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'availableTrends': {
|
||||||
|
'url': '/trends/available.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'trendsByLocation': {
|
||||||
|
'url': '/trends/{{woeid}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# Saved Searches
|
||||||
|
'getSavedSearches': {
|
||||||
|
'url': '/saved_searches.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'showSavedSearch': {
|
||||||
|
'url': '/saved_searches/show/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'createSavedSearch': {
|
||||||
|
'url': '/saved_searches/create.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'destroySavedSearch': {
|
||||||
|
'url': '/saved_searches/destroy/{{id}}.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
|
||||||
|
# List API methods/endpoints. Fairly exhaustive and annoying in general. ;P
|
||||||
|
'createList': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'showLists': {
|
||||||
|
'url': '/{{username}}/lists.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getListMemberships': {
|
||||||
|
'url': '/{{username}}/lists/followers.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'getListTimeline': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'getSpecificList': {
|
||||||
|
'url': '/{{username}}/lists/{{list_id}}/statuses.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'addListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'getListMembers': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'GET',
|
||||||
|
},
|
||||||
|
'deleteListMember': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/members.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
'subscribeToList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'unsubscribeFromList': {
|
||||||
|
'url': '/{{username}}/{{list_id}}/following.json',
|
||||||
|
'method': 'DELETE',
|
||||||
|
},
|
||||||
|
|
||||||
|
# The one-offs
|
||||||
|
'notificationFollow': {
|
||||||
|
'url': '/notifications/follow/follow.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'notificationLeave': {
|
||||||
|
'url': '/notifications/leave/leave.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'updateDeliveryService': {
|
||||||
|
'url': '/account/update_delivery_device.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
'reportSpam': {
|
||||||
|
'url': '/report_spam.json',
|
||||||
|
'method': 'POST',
|
||||||
|
},
|
||||||
|
}
|
||||||
417
twython3k/twython.py
Normal file
417
twython3k/twython.py
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
"""
|
||||||
|
Twython is a library for Python that wraps the Twitter API.
|
||||||
|
It aims to abstract away all the API endpoints, so that additions to the library
|
||||||
|
and/or the Twitter API won't cause any overall problems.
|
||||||
|
|
||||||
|
Questions, comments? ryan@venodesigns.net
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "Ryan McGrath <ryan@venodesigns.net>"
|
||||||
|
__version__ = "1.3"
|
||||||
|
|
||||||
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
import urllib.request, urllib.error, urllib.parse
|
||||||
|
import urllib.parse
|
||||||
|
import http.client
|
||||||
|
import httplib2
|
||||||
|
import mimetypes
|
||||||
|
import mimetools
|
||||||
|
import re
|
||||||
|
|
||||||
|
import oauth2 as oauth
|
||||||
|
|
||||||
|
# Twython maps keyword based arguments to Twitter API endpoints. The endpoints
|
||||||
|
# table is a file with a dictionary of every API endpoint that Twython supports.
|
||||||
|
from .twitter_endpoints import base_url, api_table
|
||||||
|
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
# There are some special setups (like, oh, a Django application) where
|
||||||
|
# simplejson exists behind the scenes anyway. Past Python 2.6, this should
|
||||||
|
# never really cause any problems to begin with.
|
||||||
|
try:
|
||||||
|
# Python 2.6 and up
|
||||||
|
import json as simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# Python 2.6 and below (2.4/2.5, 2.3 is not guranteed to work with this library to begin with)
|
||||||
|
import simplejson
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# This case gets rarer by the day, but if we need to, we can pull it from Django provided it's there.
|
||||||
|
from django.utils import simplejson
|
||||||
|
except:
|
||||||
|
# Seriously wtf is wrong with you if you get this Exception.
|
||||||
|
raise Exception("Twython requires the simplejson library (or Python 2.6) to work. http://www.undefined.org/python/")
|
||||||
|
|
||||||
|
class TwythonError(Exception):
|
||||||
|
"""
|
||||||
|
Generic error class, catch-all for most Twython issues.
|
||||||
|
Special cases are handled by APILimit and AuthError.
|
||||||
|
|
||||||
|
Note: To use these, the syntax has changed as of Twython 1.3. To catch these,
|
||||||
|
you need to explicitly import them into your code, e.g:
|
||||||
|
|
||||||
|
from twython import TwythonError, APILimit, AuthError
|
||||||
|
"""
|
||||||
|
def __init__(self, msg, error_code=None):
|
||||||
|
self.msg = msg
|
||||||
|
if error_code == 400:
|
||||||
|
raise APILimit(msg)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class APILimit(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you've hit an API limit. Try to avoid these, read the API
|
||||||
|
docs if you're running into issues here, Twython does not concern itself with
|
||||||
|
this matter beyond telling you that you've done goofed.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(TwythonError):
|
||||||
|
"""
|
||||||
|
Raised when you try to access a protected resource and it fails due to some issue with
|
||||||
|
your authentication.
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Twython(object):
|
||||||
|
def __init__(self, twitter_token = None, twitter_secret = None, oauth_token = None, oauth_token_secret = None, headers=None):
|
||||||
|
"""setup(self, oauth_token = None, headers = None)
|
||||||
|
|
||||||
|
Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
twitter_token - Given to you when you register your application with Twitter.
|
||||||
|
twitter_secret - Given to you when you register your application with Twitter.
|
||||||
|
oauth_token - If you've gone through the authentication process and have a token for this user,
|
||||||
|
pass it in and it'll be used for all requests going forward.
|
||||||
|
oauth_token_secret - see oauth_token; it's the other half.
|
||||||
|
headers - User agent header, dictionary style ala {'User-Agent': 'Bert'}
|
||||||
|
|
||||||
|
** Note: versioning is not currently used by search.twitter functions; when Twitter moves their junk, it'll be supported.
|
||||||
|
"""
|
||||||
|
# Needed for hitting that there API.
|
||||||
|
self.request_token_url = 'http://twitter.com/oauth/request_token'
|
||||||
|
self.access_token_url = 'http://twitter.com/oauth/access_token'
|
||||||
|
self.authorize_url = 'http://twitter.com/oauth/authorize'
|
||||||
|
self.authenticate_url = 'http://twitter.com/oauth/authenticate'
|
||||||
|
self.twitter_token = twitter_token
|
||||||
|
self.twitter_secret = twitter_secret
|
||||||
|
self.oauth_token = oauth_token
|
||||||
|
self.oauth_secret = oauth_token_secret
|
||||||
|
|
||||||
|
# If there's headers, set them, otherwise be an embarassing parent for their own good.
|
||||||
|
self.headers = headers
|
||||||
|
if self.headers is None:
|
||||||
|
headers = {'User-agent': 'Twython Python Twitter Library v1.3'}
|
||||||
|
|
||||||
|
consumer = None
|
||||||
|
token = None
|
||||||
|
|
||||||
|
if self.twitter_token is not None and self.twitter_secret is not None:
|
||||||
|
consumer = oauth.Consumer(self.twitter_token, self.twitter_secret)
|
||||||
|
|
||||||
|
if self.oauth_token is not None and self.oauth_secret is not None:
|
||||||
|
token = oauth.Token(oauth_token, oauth_token_secret)
|
||||||
|
|
||||||
|
# Filter down through the possibilities here - if they have a token, if they're first stage, etc.
|
||||||
|
if consumer is not None and token is not None:
|
||||||
|
self.client = oauth.Client(consumer, token)
|
||||||
|
elif consumer is not None:
|
||||||
|
self.client = oauth.Client(consumer)
|
||||||
|
else:
|
||||||
|
# If they don't do authentication, but still want to request unprotected resources, we need an opener.
|
||||||
|
self.client = httplib2.Http()
|
||||||
|
|
||||||
|
def __getattr__(self, api_call):
|
||||||
|
"""
|
||||||
|
The most magically awesome block of code you'll see in 2010.
|
||||||
|
|
||||||
|
Rather than list out 9 million damn methods for this API, we just keep a table (see above) of
|
||||||
|
every API endpoint and their corresponding function id for this library. This pretty much gives
|
||||||
|
unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is
|
||||||
|
going to be your bottleneck... well, don't use Python. ;P
|
||||||
|
|
||||||
|
For those who don't get what's going on here, Python classes have this great feature known as __getattr__().
|
||||||
|
It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist,
|
||||||
|
we can take over and find the API method in our table. We then return a function that downloads and parses
|
||||||
|
what we're looking for, based on the keywords passed in.
|
||||||
|
|
||||||
|
I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing".
|
||||||
|
"""
|
||||||
|
def get(self, **kwargs):
|
||||||
|
# Go through and replace any mustaches that are in our API url.
|
||||||
|
fn = api_table[api_call]
|
||||||
|
base = re.sub(
|
||||||
|
'\{\{(?P<m>[a-zA-Z]+)\}\}',
|
||||||
|
lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious.
|
||||||
|
base_url + fn['url']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication
|
||||||
|
if fn['method'] == 'POST':
|
||||||
|
resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(kwargs))
|
||||||
|
else:
|
||||||
|
url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.items()])
|
||||||
|
resp, content = self.client.request(url, fn['method'])
|
||||||
|
|
||||||
|
return simplejson.loads(content)
|
||||||
|
|
||||||
|
if api_call in api_table:
|
||||||
|
return get.__get__(self)
|
||||||
|
else:
|
||||||
|
raise AttributeError(api_call)
|
||||||
|
|
||||||
|
def get_authentication_tokens(self):
|
||||||
|
"""
|
||||||
|
get_auth_url(self)
|
||||||
|
|
||||||
|
Returns an authorization URL for a user to hit.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.request_token_url, "GET")
|
||||||
|
|
||||||
|
if resp['status'] != '200':
|
||||||
|
raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content))
|
||||||
|
|
||||||
|
request_tokens = dict(urllib.parse.parse_qsl(content))
|
||||||
|
request_tokens['auth_url'] = "%s?oauth_token=%s" % (self.authenticate_url, request_tokens['oauth_token'])
|
||||||
|
return request_tokens
|
||||||
|
|
||||||
|
def get_authorized_tokens(self):
|
||||||
|
"""
|
||||||
|
get_authorized_tokens
|
||||||
|
|
||||||
|
Returns authorized tokens after they go through the auth_url phase.
|
||||||
|
"""
|
||||||
|
resp, content = self.client.request(self.access_token_url, "GET")
|
||||||
|
return dict(urllib.parse.parse_qsl(content))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------------------------------
|
||||||
|
# The following methods are all different in some manner or require special attention with regards to the Twitter API.
|
||||||
|
# Because of this, we keep them separate from all the other endpoint definitions - ideally this should be change-able,
|
||||||
|
# but it's not high on the priority list at the moment.
|
||||||
|
# ------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl"):
|
||||||
|
"""shortenURL(url_to_shorten, shortener = "http://is.gd/api.php", query = "longurl")
|
||||||
|
|
||||||
|
Shortens url specified by url_to_shorten.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
url_to_shorten - URL to shorten.
|
||||||
|
shortener - In case you want to use a url shortening service other than is.gd.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(
|
||||||
|
shortener + "?" + urllib.parse.urlencode({query: Twython.unicode2utf8(url_to_shorten)}),
|
||||||
|
"GET"
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("shortenURL() failed with a %s error code." % repr(e.code))
|
||||||
|
|
||||||
|
def bulkUserLookup(self, ids = None, screen_names = None, version = None):
|
||||||
|
""" bulkUserLookup(self, ids = None, screen_names = None, version = None)
|
||||||
|
|
||||||
|
A method to do bulk user lookups against the Twitter API. Arguments (ids (numbers) / screen_names (strings)) should be flat Arrays that
|
||||||
|
contain their respective data sets.
|
||||||
|
|
||||||
|
Statuses for the users in question will be returned inline if they exist. Requires authentication!
|
||||||
|
"""
|
||||||
|
apiURL = "http://api.twitter.com/1/users/lookup.json?lol=1"
|
||||||
|
if ids is not None:
|
||||||
|
apiURL += "&user_id="
|
||||||
|
for id in ids:
|
||||||
|
apiURL += repr(id) + ","
|
||||||
|
if screen_names is not None:
|
||||||
|
apiURL += "&screen_name="
|
||||||
|
for name in screen_names:
|
||||||
|
apiURL += name + ","
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(apiURL, "GET")
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("bulkUserLookup() failed with a %s error code." % repr(e.code), e.code)
|
||||||
|
|
||||||
|
def searchTwitter(self, **kwargs):
|
||||||
|
"""searchTwitter(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(searchURL, "GET")
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("getSearchTimeline() failed with a %s error code." % repr(e.code), e.code)
|
||||||
|
|
||||||
|
def searchTwitterGen(self, **kwargs):
|
||||||
|
"""searchTwitterGen(search_query, **kwargs)
|
||||||
|
|
||||||
|
Returns a generator of tweets that match a specified query.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
See the documentation at http://dev.twitter.com/doc/get/search. Pass in the API supported arguments as named parameters.
|
||||||
|
|
||||||
|
e.g x.searchTwitter(q="jjndf", page="2")
|
||||||
|
"""
|
||||||
|
searchURL = self.constructApiURL("http://search.twitter.com/search.json", kwargs) + "&" + urllib.parse.urlencode({"q": self.unicode2utf8(search_query)})
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request(searchURL, "GET")
|
||||||
|
data = simplejson.loads(content)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("searchTwitterGen() failed with a %s error code." % repr(e.code), e.code)
|
||||||
|
|
||||||
|
if not data['results']:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
for tweet in data['results']:
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
if 'page' not in kwargs:
|
||||||
|
kwargs['page'] = 2
|
||||||
|
else:
|
||||||
|
kwargs['page'] += 1
|
||||||
|
|
||||||
|
for tweet in self.searchTwitterGen(search_query, **kwargs):
|
||||||
|
yield tweet
|
||||||
|
|
||||||
|
def isListMember(self, list_id, id, username, version = 1):
|
||||||
|
""" isListMember(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a member of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - User who owns the list you're checking against (username)
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = self.client.request("http://api.twitter.com/%d/%s/%s/members/%s.json" % (version, username, list_id, repr(id)))
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
def isListSubscriber(self, list_id, id, version = 1):
|
||||||
|
""" isListSubscriber(self, list_id, id, version)
|
||||||
|
|
||||||
|
Check if a specified user (id) is a subscriber of the list in question (list_id).
|
||||||
|
|
||||||
|
**Note: This method may not work for private/protected lists, unless you're authenticated and have access to those lists.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
list_id - Required. The slug of the list to check against.
|
||||||
|
id - Required. The ID of the user being checked in the list.
|
||||||
|
username - Required. The username of the owner of the list that you're seeing if someone is subscribed to.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
resp, content = "http://api.twitter.com/%d/%s/%s/following/%s.json" % (version, username, list_id, repr(id))
|
||||||
|
return simplejson.loads(content)
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
# The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set.
|
||||||
|
def updateProfileBackgroundImage(self, filename, tile="true", version = 1):
|
||||||
|
""" updateProfileBackgroundImage(filename, tile="true")
|
||||||
|
|
||||||
|
Updates the authenticating user's profile background image.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down.
|
||||||
|
tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise.
|
||||||
|
** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false"
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = Twython.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers)
|
||||||
|
return urllib.request.urlopen(r).read()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
def updateProfileImage(self, filename, version = 1):
|
||||||
|
""" updateProfileImage(filename)
|
||||||
|
|
||||||
|
Updates the authenticating user's profile image (avatar).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down.
|
||||||
|
version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
files = [("image", filename, open(filename, 'rb').read())]
|
||||||
|
fields = []
|
||||||
|
content_type, body = Twython.encode_multipart_formdata(fields, files)
|
||||||
|
headers = {'Content-Type': content_type, 'Content-Length': str(len(body))}
|
||||||
|
r = urllib.request.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers)
|
||||||
|
return urllib.request.urlopen(r).read()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def encode_multipart_formdata(fields, files):
|
||||||
|
BOUNDARY = mimetools.choose_boundary()
|
||||||
|
CRLF = '\r\n'
|
||||||
|
L = []
|
||||||
|
for (key, value) in fields:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"' % key)
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
for (key, filename, value) in files:
|
||||||
|
L.append('--' + BOUNDARY)
|
||||||
|
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
|
||||||
|
L.append('Content-Type: %s' % Twython.get_content_type(filename))
|
||||||
|
L.append('')
|
||||||
|
L.append(value)
|
||||||
|
L.append('--' + BOUNDARY + '--')
|
||||||
|
L.append('')
|
||||||
|
body = CRLF.join(L)
|
||||||
|
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
|
||||||
|
return content_type, body
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_content_type(filename):
|
||||||
|
""" get_content_type(self, filename)
|
||||||
|
|
||||||
|
Exactly what you think it does. :D
|
||||||
|
"""
|
||||||
|
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unicode2utf8(text):
|
||||||
|
try:
|
||||||
|
if isinstance(text, str):
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return text
|
||||||
Loading…
Add table
Add a link
Reference in a new issue