├── .gitignore ├── MANIFEST.in ├── README.rst ├── flickr.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.mo 2 | *.egg-info 3 | *.egg 4 | *.EGG 5 | *.EGG-INFO 6 | bin 7 | build 8 | develop-eggs 9 | downloads 10 | eggs 11 | fake-eggs 12 | parts 13 | dist 14 | .installed.cfg 15 | .mr.developer.cfg 16 | .hg 17 | .bzr 18 | .svn 19 | *.pyc 20 | *.pyo 21 | *.tmp* -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python-Flickr 2 | ============= 3 | 4 | Python-Flickr is A Python library to interface with `Flickr REST API `_ & OAuth 5 | 6 | Features 7 | -------- 8 | 9 | * Photo Uploading 10 | * Retrieve user information 11 | * Common Flickr methods 12 | - Add/edit/delete comments 13 | - Add/edit/delete notes 14 | - And many more (very dynamic library)!! 15 | * All responses return as nice dicts 16 | 17 | Installation 18 | ------------ 19 | 20 | Installing Python-Flickr is simple: :: 21 | 22 | $ pip install python-flickr 23 | 24 | Usage 25 | ----- 26 | 27 | Authorization URL 28 | ~~~~~~~~~~~~~~~~~ 29 | :: 30 | 31 | f = FlickrAPI(api_key='*your app key*', 32 | api_secret='*your app secret*', 33 | callback_url='http://www.example.com/callback/') 34 | 35 | auth_props = f.get_authentication_tokens() 36 | auth_url = auth_props['auth_url'] 37 | 38 | #Store this token in a session or something for later use in the next step. 39 | oauth_token = auth_props['oauth_token'] 40 | oauth_token_secret = auth_props['oauth_token_secret'] 41 | 42 | print 'Connect with Flickr via: %s' % auth_url 43 | 44 | Once you click "Allow" be sure that there is a URL set up to handle getting finalized tokens and possibly adding them to your database to use their information at a later date. 45 | 46 | 47 | Handling the Callback 48 | ~~~~~~~~~~~~~~~~~~~~~ 49 | :: 50 | 51 | # oauth_token and oauth_token_secret come from the previous step 52 | # if needed, store those in a session variable or something 53 | 54 | f = FlickrAPI(api_key='*your app key*', 55 | api_secret='*your app secret*', 56 | oauth_token=oauth_token, 57 | oauth_token_secret=oauth_token_secret) 58 | 59 | authorized_tokens = f.get_auth_tokens(oauth_verifier) 60 | 61 | final_oauth_token = authorized_tokens['oauth_token'] 62 | final_oauth_token_secret = authorized_tokens['oauth_token_secret'] 63 | 64 | # Save those tokens to the database for a later use? 65 | 66 | 67 | Getting the Users recent activity feed 68 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 69 | :: 70 | 71 | # Get the final tokens from the database or wherever you have them stored 72 | 73 | f = FlickrAPI(api_key='*your app key*', 74 | api_secret='*your app secret*', 75 | oauth_token=final_tokens['oauth_token'], 76 | oauth_token_secret=final_tokens['oauth_token_secret']) 77 | 78 | recent_activity = f.get('flickr.activity.userComments') 79 | print recent_activity 80 | 81 | 82 | Add comment on a photo 83 | ~~~~~~~~~~~~~~~~~~~~~~ 84 | :: 85 | 86 | # Assume you are using the FlickrAPI instance from the previous section 87 | 88 | add_comment = f.post('flickr.photos.comments.addComment', 89 | params={'photo_id': '6620847285', 'comment_text': 'This is a test comment.'}) 90 | 91 | #This returns the comment id if successful. 92 | print add_comment 93 | 94 | 95 | Remove comment on a photo 96 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 97 | :: 98 | 99 | # Assume you are using the FlickrAPI instance from the previous section 100 | # If the comment is already deleted, it will throw a FlickrAPIError (In this case, with code 2: Comment not found.) 101 | 102 | del_comment = f.post('flickr.photos.comments.deleteComment', params={'comment_id':'45887890-6620847285-72157628767110559'}) 103 | print del_comment 104 | 105 | 106 | Upload a photo 107 | ~~~~~~~~~~~~~~ 108 | :: 109 | 110 | # Assume you are using the FlickrAPI instance from the previous section 111 | 112 | files = open('/path/to/file/image.jpg', 'rb') 113 | add_photo = f.post(params={'title':'Test Title!'}, files=files) 114 | 115 | print add_photo # Returns the photo id of the newly added photo 116 | 117 | 118 | Catching errors 119 | ~~~~~~~~~~~~~~~ 120 | :: 121 | 122 | # Assume you are using the FlickrAPI instance from the previous section 123 | 124 | try: 125 | # This comment was already deleted 126 | del_comment = f.post('flickr.photos.comments.deleteComment', params={'comment_id':'45887890-6620847285-72157628767110559'}) 127 | except FlickrAPIError, e: 128 | print e.msg 129 | print e.code 130 | print 'Something bad happened :(' 131 | -------------------------------------------------------------------------------- /flickr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ Python-Flickr """ 4 | ''' 5 | For Flickr API documentation, visit: https://www.flickr.com/services/api/ 6 | ''' 7 | 8 | __author__ = 'Mike Helmick ' 9 | __version__ = '0.4.0' 10 | 11 | import codecs 12 | import mimetools 13 | import mimetypes 14 | import urllib 15 | import urllib2 16 | from io import BytesIO 17 | 18 | import httplib2 19 | 20 | import oauth2 as oauth 21 | 22 | try: 23 | from urlparse import parse_qsl 24 | except ImportError: 25 | from cgi import parse_qsl 26 | 27 | try: 28 | import simplejson as json 29 | except ImportError: 30 | try: 31 | import json 32 | except ImportError: 33 | try: 34 | from django.utils import simplejson as json 35 | except ImportError: 36 | raise ImportError('A json library is required to use this python library. Lol, yay for being verbose. ;)') 37 | 38 | 39 | # We need to import a XML Parser because Flickr doesn't return JSON for photo uploads -_- 40 | try: 41 | from lxml import etree 42 | except ImportError: 43 | try: 44 | # Python 2.5 45 | import xml.etree.cElementTree as etree 46 | except ImportError: 47 | try: 48 | # Python 2.5 49 | import xml.etree.ElementTree as etree 50 | except ImportError: 51 | try: 52 | #normal cElementTree install 53 | import cElementTree as etree 54 | except ImportError: 55 | try: 56 | # normal ElementTree install 57 | import elementtree.ElementTree as etree 58 | except ImportError: 59 | raise ImportError('Failed to import ElementTree from any known place') 60 | 61 | writer = codecs.lookup('utf-8')[3] 62 | 63 | 64 | def get_content_type(filename): 65 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 66 | 67 | 68 | def iter_fields(fields): 69 | """Iterate over fields. 70 | 71 | Supports list of (k, v) tuples and dicts. 72 | """ 73 | if isinstance(fields, dict): 74 | return ((k, v) for k, v in fields.iteritems()) 75 | return ((k, v) for k, v in fields) 76 | 77 | 78 | class FlickrAPIError(Exception): 79 | """ Generic error class, catch-all for most Tumblpy issues. 80 | 81 | from Tumblpy import FlickrAPIError, FlickrAuthError 82 | """ 83 | def __init__(self, msg, error_code=None): 84 | self.msg = msg 85 | self.code = error_code 86 | if error_code is not None and error_code < 100: 87 | raise FlickrAuthError(msg, error_code) 88 | 89 | def __str__(self): 90 | return repr(self.msg) 91 | 92 | 93 | class FlickrAuthError(FlickrAPIError): 94 | """ Raised when you try to access a protected resource and it fails due to some issue with your authentication. """ 95 | def __init__(self, msg, error_code=None): 96 | self.msg = msg 97 | self.code = error_code 98 | 99 | def __str__(self): 100 | return repr(self.msg) 101 | 102 | 103 | class FlickrAPI(object): 104 | def __init__(self, api_key=None, api_secret=None, oauth_token=None, oauth_token_secret=None, callback_url=None, headers=None, client_args=None): 105 | if not api_key or not api_secret: 106 | raise FlickrAPIError('Please supply an api_key and api_secret.') 107 | 108 | self.api_key = api_key 109 | self.api_secret = api_secret 110 | self.oauth_token = oauth_token 111 | self.oauth_token_secret = oauth_token_secret 112 | self.callback_url = callback_url 113 | 114 | self.api_base = 'https://api.flickr.com/services' 115 | self.up_api_base = 'https://up.flickr.com/services' 116 | self.rest_api_url = '%s/rest' % self.api_base 117 | self.upload_api_url = '%s/upload/' % self.up_api_base 118 | self.replace_api_url = '%s/replace/' % self.up_api_base 119 | self.request_token_url = 'https://www.flickr.com/services/oauth/request_token' 120 | self.access_token_url = 'https://www.flickr.com/services/oauth/access_token' 121 | self.authorize_url = 'https://www.flickr.com/services/oauth/authorize' 122 | 123 | self.headers = headers 124 | if self.headers is None: 125 | self.headers = {'User-agent': 'Python-Flickr v%s' % __version__} 126 | 127 | self.consumer = None 128 | self.token = None 129 | 130 | client_args = client_args or {} 131 | 132 | if self.api_key is not None and self.api_secret is not None: 133 | self.consumer = oauth.Consumer(self.api_key, self.api_secret) 134 | 135 | if self.oauth_token is not None and self.oauth_token_secret is not None: 136 | self.token = oauth.Token(oauth_token, oauth_token_secret) 137 | 138 | # Filter down through the possibilities here - if they have a token, if they're first stage, etc. 139 | if self.consumer is not None and self.token is not None: 140 | self.client = oauth.Client(self.consumer, self.token, **client_args) 141 | elif self.consumer is not None: 142 | self.client = oauth.Client(self.consumer, **client_args) 143 | else: 144 | # If they don't do authentication, but still want to request unprotected resources, we need an opener. 145 | self.client = httplib2.Http(**client_args) 146 | 147 | def get_authentication_tokens(self, perms=None): 148 | """ Returns an authorization url to give to your user. 149 | 150 | Parameters: 151 | perms - If None, this is ignored and uses your applications default perms. If set, will overwrite applications perms; acceptable perms (read, write, delete) 152 | * read - permission to read private information 153 | * write - permission to add, edit and delete photo metadata (includes 'read') 154 | * delete - permission to delete photos (includes 'write' and 'read') 155 | """ 156 | 157 | request_args = {} 158 | resp, content = self.client.request('%s?oauth_callback=%s' % (self.request_token_url, self.callback_url), 'GET', **request_args) 159 | 160 | if resp['status'] != '200': 161 | raise FlickrAuthError('There was a problem retrieving an authentication url.') 162 | 163 | request_tokens = dict(parse_qsl(content)) 164 | 165 | auth_url_params = { 166 | 'oauth_token': request_tokens['oauth_token'] 167 | } 168 | 169 | accepted_perms = ('read', 'write', 'delete') 170 | if perms and perms in accepted_perms: 171 | auth_url_params['perms'] = perms 172 | 173 | request_tokens['auth_url'] = '%s?%s' % (self.authorize_url, urllib.urlencode(auth_url_params)) 174 | return request_tokens 175 | 176 | def get_auth_tokens(self, oauth_verifier): 177 | """ Returns 'final' tokens to store and used to make authorized calls to Flickr. 178 | 179 | Parameters: 180 | oauth_token - oauth_token returned from when the user is redirected after hitting the get_auth_url() function 181 | verifier - oauth_verifier returned from when the user is redirected after hitting the get_auth_url() function 182 | """ 183 | 184 | params = { 185 | 'oauth_verifier': oauth_verifier, 186 | } 187 | 188 | resp, content = self.client.request('%s?%s' % (self.access_token_url, urllib.urlencode(params)), 'GET') 189 | if resp['status'] != '200': 190 | raise FlickrAuthError('Getting access tokens failed: %s Response Status' % resp['status']) 191 | 192 | return dict(parse_qsl(content)) 193 | 194 | def api_request(self, endpoint=None, method='GET', params={}, files=None, replace=False): 195 | self.headers.update({'Content-Type': 'application/json'}) 196 | self.headers.update({'Content-Length': '0'}) 197 | 198 | if endpoint is None and files is None: 199 | raise FlickrAPIError('Please supply an API endpoint to hit.') 200 | 201 | qs = { 202 | 'format': 'json', 203 | 'nojsoncallback': 1, 204 | 'method': endpoint, 205 | 'api_key': self.api_key 206 | } 207 | 208 | if method == 'POST': 209 | 210 | if files is not None: 211 | # To upload/replace file, we need to create a fake request 212 | # to sign parameters that are not multipart before we add 213 | # the multipart file to the parameters... 214 | # OAuth is not meant to sign multipart post data 215 | http_url = self.replace_api_url if replace else self.upload_api_url 216 | faux_req = oauth.Request.from_consumer_and_token(self.consumer, 217 | token=self.token, 218 | http_method="POST", 219 | http_url=http_url, 220 | parameters=params) 221 | 222 | faux_req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), 223 | self.consumer, 224 | self.token) 225 | 226 | all_upload_params = dict(parse_qsl(faux_req.to_postdata())) 227 | 228 | # For Tumblr, all media (photos, videos) 229 | # are sent with the 'data' parameter 230 | all_upload_params['photo'] = (files.name, files.read()) 231 | body, content_type = self.encode_multipart_formdata(all_upload_params) 232 | 233 | self.headers.update({ 234 | 'Content-Type': content_type, 235 | 'Content-Length': str(len(body)) 236 | }) 237 | 238 | req = urllib2.Request(http_url, body, self.headers) 239 | try: 240 | req = urllib2.urlopen(req) 241 | except urllib2.HTTPError, e: 242 | # Making a fake resp var because urllib2.urlopen doesn't 243 | # return a tuple like OAuth2 client.request does 244 | resp = {'status': e.code} 245 | content = e.read() 246 | 247 | # After requests is finished, delete Content Length & Type so 248 | # requests after don't use same Length and take (i.e 20 sec) 249 | del self.headers['Content-Type'] 250 | del self.headers['Content-Length'] 251 | 252 | # If no error, assume response was 200 253 | resp = {'status': 200} 254 | 255 | content = req.read() 256 | content = etree.XML(content) 257 | 258 | stat = content.get('stat') or 'ok' 259 | 260 | if stat == 'fail': 261 | if content.find('.//err') is not None: 262 | code = content.findall('.//err[@code]') 263 | msg = content.findall('.//err[@msg]') 264 | 265 | if len(code) > 0: 266 | if len(msg) == 0: 267 | msg = 'An error occurred making your Flickr API request.' 268 | else: 269 | msg = msg[0].get('msg') 270 | 271 | code = int(code[0].get('code')) 272 | 273 | content = { 274 | 'stat': 'fail', 275 | 'code': code, 276 | 'message': msg 277 | } 278 | else: 279 | photoid = content.find('.//photoid') 280 | if photoid is not None: 281 | photoid = photoid.text 282 | 283 | content = { 284 | 'stat': 'ok', 285 | 'photoid': photoid 286 | } 287 | 288 | else: 289 | url = self.rest_api_url + '?' + urllib.urlencode(qs) + '&' + urllib.urlencode(params) 290 | resp, content = self.client.request(url, 'POST', headers=self.headers) 291 | else: 292 | params.update(qs) 293 | resp, content = self.client.request('%s?%s' % (self.rest_api_url, urllib.urlencode(params)), 'GET', headers=self.headers) 294 | 295 | #try except for if content is able to be decoded 296 | try: 297 | if type(content) != dict: 298 | content = json.loads(content) 299 | except ValueError: 300 | raise FlickrAPIError('Content is not valid JSON, unable to be decoded.') 301 | 302 | status = int(resp['status']) 303 | if status < 200 or status >= 300: 304 | raise FlickrAPIError('Flickr returned a Non-200 response.', error_code=status) 305 | 306 | if content.get('stat') and content['stat'] == 'fail': 307 | raise FlickrAPIError('Flickr returned error code: %d. Message: %s' % \ 308 | (content['code'], content['message']), 309 | error_code=content['code']) 310 | 311 | return dict(content) 312 | 313 | def get(self, endpoint=None, params=None): 314 | params = params or {} 315 | return self.api_request(endpoint, method='GET', params=params) 316 | 317 | def post(self, endpoint=None, params=None, files=None, replace=False): 318 | params = params or {} 319 | return self.api_request(endpoint, method='POST', params=params, files=files, replace=replace) 320 | 321 | # Thanks urllib3 <3 322 | def encode_multipart_formdata(self, fields, boundary=None): 323 | """ 324 | Encode a dictionary of ``fields`` using the multipart/form-data mime format. 325 | 326 | :param fields: 327 | Dictionary of fields or list of (key, value) field tuples. The key is 328 | treated as the field name, and the value as the body of the form-data 329 | bytes. If the value is a tuple of two elements, then the first element 330 | is treated as the filename of the form-data section. 331 | 332 | Field names and filenames must be unicode. 333 | 334 | :param boundary: 335 | If not specified, then a random boundary will be generated using 336 | :func:`mimetools.choose_boundary`. 337 | """ 338 | body = BytesIO() 339 | if boundary is None: 340 | boundary = mimetools.choose_boundary() 341 | 342 | for fieldname, value in iter_fields(fields): 343 | body.write('--%s\r\n' % (boundary)) 344 | 345 | if isinstance(value, tuple): 346 | filename, data = value 347 | writer(body).write('Content-Disposition: form-data; name="%s"; ' 348 | 'filename="%s"\r\n' % (fieldname, filename)) 349 | body.write('Content-Type: %s\r\n\r\n' % 350 | (get_content_type(filename))) 351 | else: 352 | data = value 353 | writer(body).write('Content-Disposition: form-data; name="%s"\r\n' 354 | % (fieldname)) 355 | body.write(b'Content-Type: text/plain\r\n\r\n') 356 | 357 | if isinstance(data, int): 358 | data = str(data) # Backwards compatibility 359 | 360 | if isinstance(data, unicode): 361 | writer(body).write(data) 362 | else: 363 | body.write(data) 364 | 365 | body.write(b'\r\n') 366 | 367 | body.write('--%s--\r\n' % (boundary)) 368 | 369 | content_type = 'multipart/form-data; boundary=%s' % boundary 370 | 371 | return body.getvalue(), content_type 372 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='python-flickr', 7 | version='0.4.0', 8 | install_requires=['httplib2', 'oauth2', 'simplejson'], 9 | author='Mike Helmick', 10 | author_email='me@michaelhelmick.com', 11 | license='MIT License', 12 | url='https://github.com/michaelhelmick/python-flickr/', 13 | keywords='python flickr oauth api', 14 | description='A Python Library to interface with Flickr REST API, OAuth & JSON Responses', 15 | long_description=open('README.rst').read(), 16 | download_url="https://github.com/michaelhelmick/python-flickr/zipball/master", 17 | py_modules=["flickr"], 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'Topic :: Communications :: Chat', 24 | 'Topic :: Internet' 25 | ] 26 | ) 27 | --------------------------------------------------------------------------------