From 650a69ec17918bf37ec6d60168fc7debfa52e8ff Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Thu, 24 Nov 2011 13:15:23 +0000 Subject: [PATCH 1/5] Allow for easier debugging/development, by registering endpoints directly into Twython. --- twython/twython.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython/twython.py b/twython/twython.py index d8db763..bb67474 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -168,6 +168,9 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) + # register available funcs to allow listing name when debugging. + for key in api_table.keys(): + self.__dict__[key] = self.__getattr__(key) def __getattr__(self, api_call): """ -- 2.39.5 From 4710c49b2865b0d1fd8a253b4bdaa980c66a08fa Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Wed, 30 Nov 2011 14:29:51 +0000 Subject: [PATCH 2/5] Allow for easier debugging/development, by registering endpoints directly into Twython3k. --- twython3k/twython.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/twython3k/twython.py b/twython3k/twython.py index f30cfcd..53cab7c 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -156,6 +156,9 @@ class Twython(object): else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) + # register available funcs to allow listing name when debugging. + for key in api_table.keys(): + self.__dict__[key] = self.__getattr__(key) def __getattr__(self, api_call): """ -- 2.39.5 From 262b7441d41510010dad0cbe590e3aeac110f616 Mon Sep 17 00:00:00 2001 From: Mesar Hameed Date: Mon, 12 Dec 2011 07:32:40 +0000 Subject: [PATCH 3/5] Get rid of __getattr__ since the endpoints are directly registered into Twython by the constructor. --- twython/twython.py | 58 ++++++++++++++++---------------------------- twython3k/twython.py | 53 ++++++++++++---------------------------- 2 files changed, 37 insertions(+), 74 deletions(-) diff --git a/twython/twython.py b/twython/twython.py index bb67474..4f8c3a4 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -170,46 +170,30 @@ class Twython(object): self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. for key in api_table.keys(): - self.__dict__[key] = self.__getattr__(key) + self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def _constructFunc(self, api_call, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + # The '1' here catches the API version. Slightly + # hilarious. + lambda m: "%s" % kwargs.get(m.group(1), '1'), + base_url + fn['url'] + ) - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P - - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. - - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) - - return simplejson.loads(content.decode('utf-8')) - - if api_call in api_table: - return get.__get__(self) + # Then open and load that shiiit, yo. TODO: check HTTP method + # and junk, handle errors/authentication + if fn['method'] == 'POST': + myargs = urllib.urlencode(dict([k, Twython.encode(v)] for k, v in kwargs.items())) + resp, content = self.client.request(base, fn['method'], myargs, headers = self.headers) else: - raise TwythonError, api_call + myargs = ["%s=%s" %(key, value) for (key, value) in kwargs.iteritems()] + url = "%s?%s" %(base, "&".join(myargs)) + resp, content = self.client.request(url, fn['method'], headers=self.headers) + + return simplejson.loads(content.decode('utf-8')) def get_authentication_tokens(self): """ diff --git a/twython3k/twython.py b/twython3k/twython.py index 53cab7c..fe7bcb8 100644 --- a/twython3k/twython.py +++ b/twython3k/twython.py @@ -158,46 +158,25 @@ class Twython(object): self.client = httplib2.Http(**client_args) # register available funcs to allow listing name when debugging. for key in api_table.keys(): - self.__dict__[key] = self.__getattr__(key) + self.__dict__[key] = lambda **kwargs: self._constructFunc(key, **kwargs) - def __getattr__(self, api_call): - """ - The most magically awesome block of code you'll see in 2010. + def _constructFunc(self, api_call, **kwargs): + # Go through and replace any mustaches that are in our API url. + fn = api_table[api_call] + base = re.sub( + '\{\{(?P[a-zA-Z_]+)\}\}', + lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. + base_url + fn['url'] + ) - Rather than list out 9 million damn methods for this API, we just keep a table (see above) of - every API endpoint and their corresponding function id for this library. This pretty much gives - unlimited flexibility in API support - there's a slight chance of a performance hit here, but if this is - going to be your bottleneck... well, don't use Python. ;P - - For those who don't get what's going on here, Python classes have this great feature known as __getattr__(). - It's called when an attribute that was called on an object doesn't seem to exist - since it doesn't exist, - we can take over and find the API method in our table. We then return a function that downloads and parses - what we're looking for, based on the keywords passed in. - - I'll hate myself for saying this, but this is heavily inspired by Ruby's "method_missing". - """ - def get(self, **kwargs): - # Go through and replace any mustaches that are in our API url. - fn = api_table[api_call] - base = re.sub( - '\{\{(?P[a-zA-Z_]+)\}\}', - lambda m: "%s" % kwargs.get(m.group(1), '1'), # The '1' here catches the API version. Slightly hilarious. - base_url + fn['url'] - ) - - # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication - if fn['method'] == 'POST': - resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) - else: - url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) - resp, content = self.client.request(url, fn['method'], headers = self.headers) - - return simplejson.loads(content.decode('utf-8')) - - if api_call in api_table: - return get.__get__(self) + # Then open and load that shiiit, yo. TODO: check HTTP method and junk, handle errors/authentication + if fn['method'] == 'POST': + resp, content = self.client.request(base, fn['method'], urllib.parse.urlencode(dict([k, Twython.encode(v)] for k, v in list(kwargs.items()))), headers = self.headers) else: - raise TwythonError(api_call) + url = base + "?" + "&".join(["%s=%s" %(key, value) for (key, value) in list(kwargs.items())]) + resp, content = self.client.request(url, fn['method'], headers = self.headers) + + return simplejson.loads(content.decode('utf-8')) def get_authentication_tokens(self): """ -- 2.39.5 From e54183df9cb2654a228fb30edcacd17de8949e85 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Jan 2012 22:37:50 -0500 Subject: [PATCH 4/5] Uploading profile image and profile background image, update setup required packages, removed some funcs. * You can now update user profile image or user profile background image thanks to the Python requests library. * Updated setup to include 'requests' as a required package * Changed to beginning hashbang to use the users environment python version * try/except for parse_qsl, removed try/excepts where it used cgi.parse_qsl/urlparse.parse_sql * Lines 161/162 (using self.consumer/token) <- this addition ended up not being needed, but it doesn't hurt. * updateProfileBackgroundImage() - param 'tile' is now True/False rather than a string "true" or string "false" * removed encode_multipart_formdata func, not needed any longer --- setup.py | 2 +- twython/twython.py | 126 ++++++++++++++++++++++----------------------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/setup.py b/setup.py index 90f94a1..a75f04b 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( include_package_data = True, # Package dependencies. - install_requires = ['simplejson', 'oauth2', 'httplib2'], + install_requires = ['simplejson', 'oauth2', 'httplib2', 'requests'], # Metadata for PyPI. author = 'Ryan McGrath', diff --git a/twython/twython.py b/twython/twython.py index d8db763..abcf0de 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env """ Twython is a library for Python that wraps the Twitter API. @@ -21,9 +21,16 @@ import mimetypes import mimetools import re import inspect +import time +import requests import oauth2 as oauth +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + # Twython maps keyword based arguments to Twitter API endpoints. The endpoints # table is a file with a dictionary of every API endpoint that Twython supports. from twitter_endpoints import base_url, api_table @@ -151,20 +158,20 @@ class Twython(object): if self.headers is None: self.headers = {'User-agent': 'Twython Python Twitter Library v1.3'} - consumer = None - token = None + self.consumer = None + self.token = None if self.twitter_token is not None and self.twitter_secret is not None: - consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) + self.consumer = oauth.Consumer(self.twitter_token, self.twitter_secret) if self.oauth_token is not None and self.oauth_secret is not None: - token = oauth.Token(oauth_token, oauth_token_secret) + self.token = oauth.Token(oauth_token, oauth_token_secret) # Filter down through the possibilities here - if they have a token, if they're first stage, etc. - if consumer is not None and token is not None: - self.client = oauth.Client(consumer, token, **client_args) - elif consumer is not None: - self.client = oauth.Client(consumer, **client_args) + if self.consumer is not None and self.token is not None: + self.client = oauth.Client(self.consumer, self.token, **client_args) + elif self.consumer is not None: + self.client = oauth.Client(self.consumer, **client_args) else: # If they don't do authentication, but still want to request unprotected resources, we need an opener. self.client = httplib2.Http(**client_args) @@ -225,10 +232,7 @@ class Twython(object): if resp['status'] != '200': raise AuthError("Seems something couldn't be verified with your OAuth junk. Error: %s, Message: %s" % (resp['status'], content)) - try: - request_tokens = dict(urlparse.parse_qsl(content)) - except: - request_tokens = dict(cgi.parse_qsl(content)) + request_tokens = dict(parse_qsl(content)) oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed')=='true' @@ -256,10 +260,7 @@ class Twython(object): Returns authorized tokens after they go through the auth_url phase. """ resp, content = self.client.request(self.access_token_url, "GET") - try: - return dict(urlparse.parse_qsl(content)) - except: - return dict(cgi.parse_qsl(content)) + return dict(parse_qsl(content)) # ------------------------------------------------------------------------------------------------------------------------ # The following methods are all different in some manner or require special attention with regards to the Twitter API. @@ -421,26 +422,36 @@ class Twython(object): raise TwythonError("isListMember() failed with a %d error code." % e.code, e.code) # The following methods are apart from the other Account methods, because they rely on a whole multipart-data posting function set. - def updateProfileBackgroundImage(self, filename, tile="true", version = 1): - """ updateProfileBackgroundImage(filename, tile="true") + def updateProfileBackgroundImage(self, filename, tile=True, version = 1): + """ updateProfileBackgroundImage(filename, tile=True) Updates the authenticating user's profile background image. Parameters: image - Required. Must be a valid GIF, JPG, or PNG image of less than 800 kilobytes in size. Images with width larger than 2048 pixels will be forceably scaled down. - tile - Optional (defaults to true). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. - ** Note: It's sad, but when using this method, pass the tile value as a string, e.g tile="false" + tile - Optional (defaults to True). If set to true the background image will be displayed tiled. The image will not be tiled otherwise. version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_background_image.json?tile=%s" % (version, tile), body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileBackgroundImage() failed with a %d error code." % e.code, e.code) + upload_url = 'http://api.twitter.com/%d/account/update_profile_background_image.json' % version + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(length=41), + 'oauth_timestamp': int(time.time()), + } + + #create a fake request with your upload url and parameters + faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) + + #sign the fake request. + signature_method = oauth.SignatureMethod_HMAC_SHA1() + faux_req.sign_request(signature_method, self.consumer, self.token) + + #create a dict out of the fake request signed params + params = dict(parse_qsl(faux_req.to_postdata())) + self.headers.update(faux_req.to_header()) + + req = requests.post(upload_url, data={'tile':tile}, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) + return req.content def updateProfileImage(self, filename, version = 1): """ updateProfileImage(filename) @@ -451,15 +462,26 @@ class Twython(object): image - Required. Must be a valid GIF, JPG, or PNG image of less than 700 kilobytes in size. Images with width larger than 500 pixels will be scaled down. version (number) - Optional. API version to request. Entire Twython class defaults to 1, but you can override on a function-by-function or class basis - (version=2), etc. """ - try: - files = [("image", filename, open(filename, 'rb').read())] - fields = [] - content_type, body = Twython.encode_multipart_formdata(fields, files) - headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} - r = urllib2.Request("http://api.twitter.com/%d/account/update_profile_image.json" % version, body, headers) - return urllib2.urlopen(r).read() - except HTTPError, e: - raise TwythonError("updateProfileImage() failed with a %d error code." % e.code, e.code) + upload_url = 'http://api.twitter.com/%d/account/update_profile_image.json' % version + params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(length=41), + 'oauth_timestamp': int(time.time()), + } + + #create a fake request with your upload url and parameters + faux_req = oauth.Request(method='POST', url=upload_url, parameters=params) + + #sign the fake request. + signature_method = oauth.SignatureMethod_HMAC_SHA1() + faux_req.sign_request(signature_method, self.consumer, self.token) + + #create a dict out of the fake request signed params + params = dict(parse_qsl(faux_req.to_postdata())) + self.headers.update(faux_req.to_header()) + + req = requests.post(upload_url, files={'image':(filename, open(filename, 'rb'))}, headers=self.headers) + return req.content def getProfileImageUrl(self, username, size=None, version=1): """ getProfileImageUrl(username) @@ -485,29 +507,7 @@ class Twython(object): return simplejson.loads(content.decode('utf-8')) raise TwythonError("getProfileImageUrl() failed with a %d error code." % resp.status, resp.status) - - @staticmethod - def encode_multipart_formdata(fields, files): - BOUNDARY = mimetools.choose_boundary() - CRLF = '\r\n' - L = [] - for (key, value) in fields: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - L.append(value) - for (key, filename, value) in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) - L.append('Content-Type: %s' % mimetypes.guess_type(filename)[0] or 'application/octet-stream') - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - + @staticmethod def unicode2utf8(text): try: @@ -521,4 +521,4 @@ class Twython(object): def encode(text): if isinstance(text, (str,unicode)): return Twython.unicode2utf8(text) - return str(text) + return str(text) \ No newline at end of file -- 2.39.5 From f9d87b6fd38bb966d3c2561dc8205899eb81d744 Mon Sep 17 00:00:00 2001 From: Michael Helmick Date: Thu, 12 Jan 2012 23:16:36 -0500 Subject: [PATCH 5/5] Left out 'python' in hashbang, update setup to use env, too. Left out 'python' in hashbang, update setup to use env, too. --- setup.py | 2 +- twython/twython.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a75f04b..c8c7c98 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import sys, os from setuptools import setup diff --git a/twython/twython.py b/twython/twython.py index abcf0de..8a400cf 100644 --- a/twython/twython.py +++ b/twython/twython.py @@ -1,4 +1,4 @@ -#!/usr/bin/env +#!/usr/bin/env python """ Twython is a library for Python that wraps the Twitter API. -- 2.39.5