├── .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 |
--------------------------------------------------------------------------------