Making twython work (again?) in Python 3

- Added a ``HISTORY.rst`` to start tracking history of changes
- Updated ``twitter_endpoints.py`` to ``endpoints.py`` for cleanliness
- Removed twython3k directory, no longer needed
- Added ``compat.py`` for compatability with Python 2.6 and greater
- Added some ascii art, moved description of Twython and ``__author__`` to ``__init__.py``
- Added ``version.py`` to store the current Twython version, instead of repeating it twice -- it also had to go into it's own file because of dependencies of ``requests`` and ``requests-oauthlib``, install would fail because those libraries weren't installed yet (on fresh install of Twython)
- Removed ``find_packages()`` from ``setup.py``, only one package -- we can
just define it
- added quick publish method for Ryan and I: ``python setup.py publish`` is faster to type and easier to remember than ``python setup.py sdist upload``
- Removed ``base_url`` from ``endpoints.py`` because we're just repeating it in
``Twython.__init__``
- ``Twython.get_authentication_tokens()`` now takes ``callback_url`` argument rather than passing the ``callback_url`` through ``Twython.__init__``, ``callback_url`` is only used in the ``get_authentication_tokens`` method and nowhere else (kept in init though for backwards compatability)
- Updated README to better reflect current Twython codebase
- Added ``warnings.simplefilter('default')`` line in ``twython.py`` for Python 2.7 and greater to display Deprecation Warnings in console
- Added Deprecation Warnings for usage of ``twitter_token``, ``twitter_secret`` and ``callback_url`` in ``Twython.__init__``
- Headers now always include the User-Agent as Twython vXX unless User-Agent is overwritten
- Removed senseless TwythonError thrown if method is not GET or POST, who cares -- if the user passes something other than GET or POST just let Twitter return the error that they messed up
- Removed conversion to unicode of (int, bool) params passed to a requests. ``requests`` isn't greedy about variables that can't be converted to unicode anymore
This commit is contained in:
Mike Helmick 2013-04-17 18:59:11 -04:00
parent 8ecc55b5ad
commit bb019d3a57
16 changed files with 306 additions and 1000 deletions

View file

@ -1,2 +1,23 @@
from twython import Twython
# ______ __ __
# /_ __/_ __ __ __ / /_ / /_ ____ ____
# / / | | /| / // / / // __// __ \ / __ \ / __ \
# / / | |/ |/ // /_/ // /_ / / / // /_/ // / / /
# /_/ |__/|__/ \__, / \__//_/ /_/ \____//_/ /_/
# /____/
"""
Twython
-------
Twython is a library for Python that wraps the Twitter API.
It aims to abstract away all the API endpoints, so that additions to the library
and/or the Twitter API won't cause any overall problems.
Questions, comments? ryan@venodesigns.net
"""
__author__ = 'Ryan McGrath <ryan@venodesigns.net>'
from .twython import Twython
from .exceptions import TwythonError, TwythonRateLimitError, TwythonAuthError

38
twython/compat.py Normal file
View file

@ -0,0 +1,38 @@
import sys
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
try:
import simplejson as json
except ImportError:
import json
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
if is_py2:
from urllib import urlencode, quote_plus
builtin_str = str
bytes = str
str = unicode
basestring = basestring
numeric_types = (int, long, float)
elif is_py3:
from urllib.parse import urlencode, quote_plus
builtin_str = str
str = str
bytes = bytes
basestring = (str, bytes)
numeric_types = (int, float)

View file

@ -15,9 +15,6 @@
https://dev.twitter.com/docs/api/1.1
"""
# Base Twitter API url, no need to repeat this junk...
base_url = 'http://api.twitter.com/{{version}}'
api_table = {
# Timelines
'getMentionsTimeline': {

View file

@ -1,4 +1,4 @@
from twitter_endpoints import twitter_http_status_codes
from .endpoints import twitter_http_status_codes
class TwythonError(Exception):

View file

@ -1,40 +1,19 @@
"""
Twython is a library for Python that wraps the Twitter API.
It aims to abstract away all the API endpoints, so that additions to the library
and/or the Twitter API won't cause any overall problems.
Questions, comments? ryan@venodesigns.net
"""
__author__ = "Ryan McGrath <ryan@venodesigns.net>"
__version__ = "2.7.3"
import urllib
import re
import warnings
warnings.simplefilter('default') # For Python 2.7 >
import requests
from requests_oauthlib import OAuth1
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
# Twython maps keyword based arguments to Twitter API endpoints. The endpoints
# table is a file with a dictionary of every API endpoint that Twython supports.
from twitter_endpoints import base_url, api_table
from .compat import json, urlencode, parse_qsl, quote_plus
from .endpoints import api_table
from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError
try:
import simplejson as json
except ImportError:
import json
from .version import __version__
class Twython(object):
def __init__(self, app_key=None, app_secret=None, oauth_token=None, oauth_token_secret=None,
headers=None, callback_url=None, twitter_token=None, twitter_secret=None, proxies=None, version='1.1'):
headers=None, proxies=None, version='1.1', callback_url=None, twitter_token=None, twitter_secret=None):
"""Instantiates an instance of Twython. Takes optional parameters for authentication and such (see below).
:param app_key: (optional) Your applications key
@ -46,46 +25,55 @@ class Twython(object):
:param proxies: (optional) A dictionary of proxies, for example {"http":"proxy.example.org:8080", "https":"proxy.example.org:8081"}.
"""
# Needed for hitting that there API.
# API urls, OAuth urls and API version; needed for hitting that there API.
self.api_version = version
self.api_url = 'https://api.twitter.com/%s'
self.request_token_url = self.api_url % 'oauth/request_token'
self.access_token_url = self.api_url % 'oauth/access_token'
self.authenticate_url = self.api_url % 'oauth/authenticate'
# Enforce unicode on keys and secrets
self.app_key = app_key and unicode(app_key) or twitter_token and unicode(twitter_token)
self.app_secret = app_key and unicode(app_secret) or twitter_secret and unicode(twitter_secret)
self.oauth_token = oauth_token and u'%s' % oauth_token
self.oauth_token_secret = oauth_token_secret and u'%s' % oauth_token_secret
self.app_key = app_key or twitter_token
self.app_secret = app_secret or twitter_secret
self.oauth_token = oauth_token
self.oauth_token_secret = oauth_token_secret
self.callback_url = callback_url
# If there's headers, set them, otherwise be an embarassing parent for their own good.
self.headers = headers or {'User-Agent': 'Twython v' + __version__}
if twitter_token or twitter_secret:
warnings.warn(
'Instead of twitter_token or twitter_secret, please use app_key or app_secret (respectively).',
DeprecationWarning,
stacklevel=2
)
# Allow for unauthenticated requests
self.client = requests.Session()
self.client.proxies = proxies
if callback_url:
warnings.warn(
'Please pass callback_url to the get_authentication_tokens method rather than Twython.__init__',
DeprecationWarning,
stacklevel=2
)
self.headers = {'User-Agent': 'Twython v' + __version__}
if headers:
self.headers.update(headers)
# Generate OAuth authentication object for the request
# If no keys/tokens are passed to __init__, self.auth=None allows for
# unauthenticated requests, although I think all v1.1 requests need auth
self.auth = None
if self.app_key is not None and self.app_secret is not None and \
self.oauth_token is None and self.oauth_token_secret is None:
self.auth = OAuth1(self.app_key, self.app_secret)
if self.app_key is not None and self.app_secret is not None and \
self.oauth_token is None and self.oauth_token_secret is None:
self.oauth_token is not None and self.oauth_token_secret is not None:
self.auth = OAuth1(self.app_key, self.app_secret,
signature_type='auth_header')
self.oauth_token, self.oauth_token_secret)
if self.app_key is not None and self.app_secret is not None and \
self.oauth_token is not None and self.oauth_token_secret is not None:
self.auth = OAuth1(self.app_key, self.app_secret,
self.oauth_token, self.oauth_token_secret,
signature_type='auth_header')
if self.auth is not None:
self.client = requests.Session()
self.client.headers = self.headers
self.client.auth = self.auth
self.client.proxies = proxies
self.client = requests.Session()
self.client.headers = self.headers
self.client.proxies = proxies
self.client.auth = self.auth
# register available funcs to allow listing name when debugging.
def setFunc(key):
@ -101,8 +89,8 @@ class Twython(object):
fn = api_table[api_call]
url = re.sub(
'\{\{(?P<m>[a-zA-Z_]+)\}\}',
lambda m: "%s" % kwargs.get(m.group(1), self.api_version),
base_url + fn['url']
lambda m: "%s" % kwargs.get(m.group(1)),
self.api_url % self.api_version + fn['url']
)
content = self._request(url, method=fn['method'], params=kwargs)
@ -114,15 +102,7 @@ class Twython(object):
code twice, right? ;)
'''
method = method.lower()
if not method in ('get', 'post'):
raise TwythonError('Method must be of GET or POST')
params = params or {}
# requests doesn't like items that can't be converted to unicode,
# so let's be nice and do that for the user
for k, v in params.items():
if isinstance(v, (int, bool)):
params[k] = u'%s' % v
func = getattr(self.client, method)
if method == 'get':
@ -176,7 +156,7 @@ class Twython(object):
error_code=response.status_code,
retry_after=response.headers.get('retry-after'))
# if we have a json error here, then it's not an official TwitterAPI error
# if we have a json error here, then it's not an official Twitter API error
if json_error and not response.status_code in (200, 201, 202):
raise TwythonError('Response was not valid JSON, unable to decode.')
@ -228,23 +208,23 @@ class Twython(object):
return self._last_call['headers'][header]
return self._last_call
def get_authentication_tokens(self, force_login=False, screen_name=''):
"""Returns an authorization URL for a user to hit.
def get_authentication_tokens(self, callback_url=None, force_login=False, screen_name=''):
"""Returns a dict including an authorization URL (auth_url) to direct a user to
:param callback_url: (optional.. for now) Url the user is returned to after they authorize your app
:param force_login: (optional) Forces the user to enter their credentials to ensure the correct users account is authorized.
:param app_secret: (optional) If forced_login is set OR user is not currently logged in, Prefills the username input box of the OAuth login screen with the given value
"""
request_args = {}
if self.callback_url:
request_args['oauth_callback'] = self.callback_url
callback_url = callback_url or self.callback_url
request_args = {'oauth_callback': callback_url}
response = self.client.get(self.request_token_url, params=request_args)
if response.status_code == 401:
raise TwythonAuthError(response.content, error_code=response.status_code)
elif response.status_code != 200:
raise TwythonError(response.content, error_code=response.status_code)
request_tokens = dict(parse_qsl(response.content))
request_tokens = dict(parse_qsl(response.content.decode('utf-8')))
if not request_tokens:
raise TwythonError('Unable to decode request tokens.')
@ -261,18 +241,20 @@ class Twython(object):
})
# Use old-style callback argument if server didn't accept new-style
if self.callback_url and not oauth_callback_confirmed:
if callback_url and not oauth_callback_confirmed:
auth_url_params['oauth_callback'] = self.callback_url
request_tokens['auth_url'] = self.authenticate_url + '?' + urllib.urlencode(auth_url_params)
request_tokens['auth_url'] = self.authenticate_url + '?' + urlencode(auth_url_params)
return request_tokens
def get_authorized_tokens(self, oauth_verifier):
"""Returns authorized tokens after they go through the auth_url phase.
:param oauth_verifier: (required) The oauth_verifier retrieved from the callback url querystring
"""
response = self.client.get(self.access_token_url, params={'oauth_verifier': oauth_verifier})
authorized_tokens = dict(parse_qsl(response.content))
authorized_tokens = dict(parse_qsl(response.content.decode('utf-8')))
if not authorized_tokens:
raise TwythonError('Unable to decode authorized tokens.')
@ -308,7 +290,7 @@ class Twython(object):
@staticmethod
def constructApiURL(base_url, params):
return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), urllib.quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()])
return base_url + "?" + "&".join(["%s=%s" % (Twython.unicode2utf8(key), quote_plus(Twython.unicode2utf8(value))) for (key, value) in params.iteritems()])
def searchGen(self, search_query, **kwargs):
""" Returns a generator of tweets that match a specified query.
@ -331,7 +313,7 @@ class Twython(object):
yield tweet
if 'page' not in kwargs:
kwargs['page'] = '2'
kwargs['page'] = 2
else:
try:
kwargs['page'] = int(kwargs['page'])
@ -416,7 +398,7 @@ class Twython(object):
only API version for Twitter that supports this call
**params - You may pass items that are taken in this doc
(https://dev.twitter.com/docs/api/1/post/account/update_profile_banner)
(https://dev.twitter.com/docs/api/1.1/post/account/update_profile_banner)
"""
url = 'https://api.twitter.com/%s/account/update_profile_banner.json' % version
return self._media_update(url,

1
twython/version.py Normal file
View file

@ -0,0 +1 @@
__version__ = '2.7.3'