├── .gitignore ├── AUTHORS ├── README.md ├── script └── twitpic ├── setup.py └── twitpic ├── __init__.py └── twitpic2.py /.gitignore: -------------------------------------------------------------------------------- 1 | ._* 2 | .DS_Store 3 | build/* 4 | dist/* 5 | *.egg-info/* 6 | *.pyc 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Chris McMichael 2 | Ki-baek Lee -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **ABOUT:** 2 | 3 | *NOTE:* The TwitPicOAuthClient does NOT support fetching an access_token. Use your 4 | favorite Twitter API Client to retrieve this. An access_token is required in most API calls. 5 | 6 | GET based calls do not require any keys or tokens but for the rest of the API calls, 7 | they are required. 8 | 9 | There are 4 main methods that can be called. read, create, update, and remove which are your 10 | GET, POST, PUT, and DELETE methods. 11 | 12 | **INSTALLATION:** 13 | 14 | sudo pip install python-twitpic 15 | 16 | *OR:* 17 | 18 | git clone https://github.com/macmichael01/python-twitpic; 19 | cd python-twitpic; 20 | sudo python setup.py install 21 | 22 | **USAGE:** 23 | 24 | import twitpic 25 | twitpic = twitpic.TwitPicOAuthClient( 26 | consumer_key = CONSUMER_KEY, 27 | consumer_secret = CONSUMER_SECRET, 28 | access_token = "oauth_token=%s&oauth_token_secret=%s" % (USER_TOKEN, USER_TOKEN_SECRET), 29 | service_key = SERVICE_KEY 30 | ) 31 | # methods - read, create, update, remove 32 | response = twitpic.create(METHOD_CALL, PARAMS) 33 | print response 34 | 35 | *NOTE*: importing python-twitpic can now be done as follows: 36 | 37 | import twitpic 38 | 39 | *OR:* 40 | 41 | from twitpic import twitpic2 42 | 43 | 44 | **COMMAND-LINE USAGE:** 45 | 46 | *NOTE*: Bash auto complete script FTW! Command also requires python-2.7 to use. 47 | 48 | usage: twitpic [-h] [-m MESSAGE] 49 | consumer_key consumer_secret access_token service_key file 50 | 51 | Python-TwitPic commandline utility. 52 | 53 | positional arguments: 54 | consumer_key Twitter Consumer API Key 55 | consumer_secret Twitter Consumer API Kecret 56 | access_token Twitter Access Token 57 | service_key Twitpic API Key 58 | file Path to Image File. 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | -m MESSAGE, --message MESSAGE 63 | The tweet that belongs to the image. 64 | 65 | 66 | **CHANGE LOG:** 67 | 68 | - 2-24-2012: TwitPic Client API V1.0 is now officially deprecated. 69 | -------------------------------------------------------------------------------- /script/twitpic: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import argparse 3 | 4 | from twitpic import TwitPicOAuthClient 5 | 6 | if __name__ == '__main__': 7 | description = "Python-TwitPic commandline utility." 8 | parser = argparse.ArgumentParser(description=description) 9 | parser.add_argument('consumer_key', help='Twitter Consumer API Key') 10 | parser.add_argument('consumer_secret',help='Twitter Consumer API Kecret') 11 | parser.add_argument('access_token', help='Twitter Access Token') 12 | parser.add_argument('service_key', help='Twitpic API Key') 13 | parser.add_argument('file', help='Path to Image File.') 14 | parser.add_argument('-m', '--message', default="", 15 | help='The tweet that belongs to the image.') 16 | 17 | opts = parser.parse_args() 18 | 19 | twitpic = TwitPicOAuthClient( 20 | consumer_key = opts.consumer_key, 21 | consumer_secret = opts.consumer_secret, 22 | access_token = opts.access_token, 23 | service_key = opts.service_key, 24 | ) 25 | 26 | response = twitpic.create('upload', {'media': opts.file, 27 | 'message': opts.message }) 28 | print response 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from distutils.core import setup 3 | from setuptools import find_packages 4 | 5 | 6 | setup(name='python-twitpic', 7 | install_requires=('oauth',), 8 | description='Python TwitPic Client API', 9 | author='Chris McMichael', 10 | author_email='macmichael01@gmail.com', 11 | url="https://github.com/macmichael01/python-twitpic", 12 | version='2.1', 13 | packages = find_packages(), 14 | scripts=['script/twitpic'], 15 | long_description="Python TwitPic Client API", 16 | keywords="twitpic photos tweets", 17 | license="BSD" 18 | ) -------------------------------------------------------------------------------- /twitpic/__init__.py: -------------------------------------------------------------------------------- 1 | from twitpic2 import * -------------------------------------------------------------------------------- /twitpic/twitpic2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | # python-twitpic - Dead-simple Twitpic image uploader. 5 | 6 | # Copyright (c) 2009, Chris McMichael 7 | # http://chrismcmichael.com/ 8 | # http://code.google.com/p/python-twitpic/ 9 | 10 | # Redistribution and use in source and binary forms, with or without 11 | # modification, are permitted provided that the following conditions are met: 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above copyright 15 | # notice, this list of conditions and the following disclaimer in the 16 | # documentation and/or other materials provided with the distribution. 17 | # * Neither the name of the author nor the names of its contributors may 18 | # be used to endorse or promote products derived from this software 19 | # without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS``AS IS'' AND ANY 22 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY 25 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 28 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | """ 33 | import mimetypes 34 | import os 35 | import urllib 36 | import urllib2 37 | import re 38 | 39 | from oauth import oauth 40 | from xml.dom import minidom as xml 41 | from xml.parsers.expat import ExpatError 42 | 43 | try: 44 | import cStringIO as StringIO 45 | except ImportError: 46 | import StringIO 47 | 48 | try: 49 | import json 50 | except ImportError: 51 | import simplejson as json 52 | 53 | 54 | class TwitPicError(Exception): 55 | """TwitPic Exception""" 56 | 57 | def __init__(self, reason, response=None): 58 | self.reason = unicode(reason) 59 | self.response = response 60 | 61 | def __str__(self): 62 | return self.reason 63 | 64 | 65 | # Handles Twitter OAuth authentication 66 | class TwitPicOAuthClient(oauth.OAuthClient): 67 | """TwitPic OAuth Client API""" 68 | 69 | SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' 70 | STATUS_UPDATE_URL = 'https://api.twitter.com/1.1/statuses/update.json' 71 | USER_INFO_URL = 'https://api.twitter.com/1.1/account/verify_credentials.json' 72 | 73 | FORMAT = 'json' 74 | SERVER = 'http://api.twitpic.com' 75 | 76 | GET_URIS = { 77 | 'media_show': ('/2/media/show', ('id',)), 78 | 'faces_show': ('/2/faces/show', ('user')), 79 | 'user_show': ('/2/users/show', ('username',)), # additional optional params 80 | 'comments_show': ('/2/comments/show', ('media_id', 'page')), 81 | 'place_show': ('/2/place/show', ('id',)), 82 | 'places_user_show': ('/2/places/show', ('user',)), 83 | 'events_show': ('/2/events/show', ('user',)), 84 | 'event_show': ('/2/event/show', ('id',)), 85 | 'tags_show': ('/2/tags/show', ('tag',)), 86 | } 87 | 88 | POST_URIS = { 89 | 'upload': ('/2/upload', ('message', 'media')), 90 | 'comments_create': ('/2/comments/create', ('message_id', 'message')), 91 | 'faces_create': ('/2/faces/create', ('media_id', 'top_coord', 'left_coord')), # additional optional params 92 | 'event_create': ('/2/event/create', ('name')), # additional optional params 93 | 'event_add': ('/2/event/add', ('event_id', 'media_id')), # no workie! 94 | 'tags_create': ('/2/tags/create', ('media_id', 'tags')), 95 | } 96 | 97 | PUT_URIS = { 98 | 'faces_edit': ('/2/faces/edit', ('tag_id', 'top_coord', 'left_coord')), 99 | } 100 | 101 | DELETE_URIS = { 102 | 'comments_delete': ('/2/comments/delete', ('comment_id')), 103 | 'faces_delete': ('/2/faces/delete', ('tag_id')), 104 | 'event_delete': ('/2/event/delete', ('event_id')), 105 | 'event_remove': ('/2/event/remove', ('event_id', 'media_id')), 106 | 'tags_delete': ('/2/tags/delete', ('media_id', 'tag_id')), 107 | } 108 | 109 | def __init__(self, consumer_key=None, consumer_secret=None, 110 | service_key=None, access_token=None): 111 | """ 112 | An object for interacting with the Twitpic API. 113 | 114 | The arguments listed below are generally required for most calls. 115 | 116 | Args: 117 | consumer_key: 118 | Twitter API Key [optional] 119 | consumer_secret: 120 | Twitter API Secret [optional] 121 | access_token: 122 | Authorized access_token in string format. [optional] 123 | service_key: 124 | Twitpic service key used to interact with the API. [optional] 125 | 126 | NOTE: 127 | The TwitPic OAuth Client does NOT support fetching 128 | an access_token. Use your favorite Twitter API Client to 129 | retrieve this. 130 | 131 | """ 132 | self.server = self.SERVER 133 | self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) 134 | self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() 135 | self.service_key = service_key 136 | self.format = self.FORMAT 137 | 138 | if access_token: 139 | self.access_token = oauth.OAuthToken.from_string(access_token) 140 | 141 | def set_comsumer(self, consumer_key, consumer_secret): 142 | self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret) 143 | 144 | def set_access_token(self, access_token): 145 | self.access_token = oauth.OAuthToken.from_string(access_token) 146 | 147 | def set_service_key(self, service_key): 148 | self.service_key = service_key 149 | 150 | def _encode_multipart_formdata(self, fields=None): 151 | BOUNDARY = '-------tHISiStheMulTIFoRMbOUNDaRY' 152 | CRLF = '\r\n' 153 | L = [] 154 | filedata = None 155 | media = fields.get('media', '') 156 | 157 | if media: 158 | filedata = self._get_filedata(media) 159 | del fields['media'] 160 | 161 | if fields: 162 | for (key, value) in fields.items(): 163 | L.append('--' + BOUNDARY) 164 | L.append('Content-Disposition: form-data; name="%s"' % str(key)) 165 | L.append('') 166 | L.append(str(value)) 167 | 168 | if filedata: 169 | for (filename, value) in [(media, filedata)]: 170 | L.append('--' + BOUNDARY) 171 | L.append('Content-Disposition: form-data; name="media"; \ 172 | filename="%s"' % (str(filename),)) 173 | L.append('Content-Type: %s' % self._get_content_type(media)) 174 | L.append('') 175 | L.append(value.getvalue()) 176 | 177 | L.append('--' + BOUNDARY + '--') 178 | L.append('') 179 | body = CRLF.join(L) 180 | content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 181 | 182 | return content_type, body 183 | 184 | def _get_content_type(self, media): 185 | return mimetypes.guess_type(media)[0] or 'application/octet-stream' 186 | 187 | def _get_filedata(self, media): 188 | # Check self.image is an url, file path, or nothing. 189 | prog = re.compile('((https?|ftp|gopher|telnet|file|notes|ms-help):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)') 190 | 191 | if prog.match(media): 192 | return StringIO.StringIO(urllib2.urlopen(media).read()) 193 | elif os.path.exists(media): 194 | return StringIO.StringIO(open(media, 'rb').read()) 195 | else: 196 | return None 197 | 198 | def _post_call(self, method, params, uri, required): 199 | if not self.consumer: 200 | raise TwitPicError("Missing Twitter consumer keys") 201 | if not self.access_token: 202 | raise TwitPicError("Missing access_token") 203 | if not self.service_key: 204 | raise TwitPicError("Missing TwitPic service key") 205 | 206 | for req_param in required: 207 | if req_param not in params: 208 | raise TwitPicError('"' + req_param + '" parameter is not provided.') 209 | 210 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( 211 | self.consumer, 212 | self.access_token, 213 | http_url=self.USER_INFO_URL 214 | ) 215 | 216 | # Sign our request before setting Twitpic-only parameters 217 | oauth_request.sign_request(self.signature_method, self.consumer, self.access_token) 218 | 219 | # Set TwitPic parameters 220 | oauth_request.set_parameter('key', self.service_key) 221 | 222 | for key, value in params.iteritems(): 223 | oauth_request.set_parameter(key, value) 224 | 225 | # Build request body parameters. 226 | params = oauth_request.parameters 227 | content_type, content_body = self._encode_multipart_formdata(params) 228 | 229 | # Get the oauth headers. 230 | oauth_headers = oauth_request.to_header(realm='http://api.twitter.com/') 231 | 232 | # Add the headers required by TwitPic and any additional headers. 233 | headers = { 234 | 'X-Verify-Credentials-Authorization': oauth_headers['Authorization'], 235 | 'X-Auth-Service-Provider': self.USER_INFO_URL, 236 | 'User-Agent': 'Python-TwitPic2', 237 | 'Content-Type': content_type 238 | } 239 | 240 | # Build our url 241 | url = '%s%s.%s' % (self.server, uri, self.format) 242 | 243 | # Make the request. 244 | req = urllib2.Request(url, content_body, headers) 245 | 246 | try: 247 | # Get the response. 248 | response = urllib2.urlopen(req) 249 | except urllib2.HTTPError, e: 250 | raise TwitPicError(e) 251 | 252 | if self.format == 'json': 253 | return self.parse_json(response.read()) 254 | elif self.format == 'xml': 255 | return self.parse_xml(response.read()) 256 | 257 | def read(self, method, params, format=None): 258 | """ 259 | Use this method for all GET URI calls. 260 | 261 | An access_token or service_key is not required for this method. 262 | 263 | Args: 264 | method: 265 | name that references which GET URI to use. 266 | params: 267 | dictionary of parameters needed for the selected method call. 268 | format: 269 | response format. default is json. options are (xml, json) 270 | 271 | NOTE: 272 | faces_show is actually a POST method. However, since data 273 | is being retrieved and not created, it seemed appropriate 274 | to keep this call under the GET method calls. Tokens and keys 275 | will be required for this call as well. 276 | 277 | """ 278 | uri, required = self.GET_URIS.get(method, (None, None)) 279 | if uri is None: 280 | raise TwitPicError('Unidentified Method: ' + method) 281 | 282 | if format: 283 | self.format = format 284 | 285 | if method == 'faces_show': 286 | return self._post_call(method, params, uri, required) 287 | 288 | for req_param in required: 289 | if req_param not in params: 290 | raise TwitPicError('"' + req_param + '" parameter is not provided.') 291 | 292 | # Build our GET url 293 | request_params = urllib.urlencode(params) 294 | url = '%s%s.%s?%s' % (self.server, uri, self.format, request_params) 295 | 296 | # Make the request. 297 | req = urllib2.Request(url) 298 | 299 | try: 300 | # Get the response. 301 | response = urllib2.urlopen(req) 302 | except urllib2.HTTPError, e: 303 | raise TwitPicError(e) 304 | 305 | if self.format == 'json': 306 | return self.parse_json(response.read()) 307 | elif self.format == 'xml': 308 | return self.parse_xml(response.read()) 309 | 310 | def create(self, method, params, format=None): 311 | """ 312 | Use this method for all POST URI calls. 313 | 314 | Args: 315 | method: 316 | name that references which POST URI to use. 317 | params: 318 | dictionary of parameters needed for the selected method call. 319 | format: 320 | response format. default is json. options are (xml, json) 321 | 322 | NOTE: 323 | You do NOT have to pass the key param (service key). Service key 324 | should have been provided before calling this method. 325 | 326 | """ 327 | if 'key' in params: 328 | raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') 329 | 330 | uri, required = self.POST_URIS.get(method, (None, None)) 331 | 332 | if uri is None: 333 | raise TwitPicError('Unidentified Method: ' + method) 334 | 335 | if format: 336 | self.format = format 337 | 338 | return self._post_call(method, params, uri, required) 339 | 340 | def update(self, method, params, format=None): 341 | """ 342 | Use this method for all PUT URI calls. 343 | 344 | Args: 345 | method: 346 | name that references which PUT URI to use. 347 | params: 348 | dictionary of parameters needed for the selected method call. 349 | format: 350 | response format. default is json. options are (xml, json) 351 | 352 | """ 353 | if 'key' in params: 354 | raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') 355 | 356 | uri, required = self.PUT_URIS.get(method, (None, None)) 357 | 358 | if uri is None: 359 | raise TwitPicError('Unidentified Method: ' + method) 360 | 361 | if format: 362 | self.format = format 363 | 364 | return self._post_call(method, params, uri, required) 365 | 366 | def remove(self, method, params, format=None): 367 | """ 368 | Use this method for all DELETE URI calls. 369 | 370 | Args: 371 | method: 372 | name that references which DELETE URI to use. 373 | params: 374 | dictionary of parameters needed for the selected method call. 375 | format: 376 | response format. default is json. options are (xml, json) 377 | 378 | """ 379 | if 'key' in params: 380 | raise TwitPicError('"key" parameter should be provided by set_service_key method or initializer method.') 381 | 382 | uri, required = self.DELETE_URIS.get(method, (None, None)) 383 | 384 | if uri is None: 385 | raise TwitPicError('Unidentified Method: ' + method) 386 | 387 | if format: 388 | self.format = format 389 | 390 | return self._post_call(method, params, uri, required) 391 | 392 | def parse_xml(self, xml_response): 393 | try: 394 | dom = xml.parseString(xml_response) 395 | node = dom.firstChild 396 | if node.nodeName == 'errors': 397 | return node.firstChild.nodeValue 398 | else: 399 | return dom 400 | except ExpatError, e: 401 | raise TwitPicError('XML Parsing Error: ' + e) 402 | 403 | def parse_json(self, json_response): 404 | try: 405 | result = json.loads(json_response) 406 | if result.has_key('errors'): 407 | return result['errors']['code'] 408 | else: 409 | return result 410 | except ValueError, e: 411 | raise TwitPicError('JSON Parsing Error: ' + e) 412 | --------------------------------------------------------------------------------