-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathflickr.py
371 lines (288 loc) · 14 KB
/
flickr.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#!/usr/bin/env python
""" Python-Flickr """
'''
For Flickr API documentation, visit: https://www.flickr.com/services/api/
'''
__author__ = 'Mike Helmick <[email protected]>'
__version__ = '0.4.0'
import codecs
import mimetools
import mimetypes
import urllib
import urllib2
from io import BytesIO
import httplib2
import oauth2 as oauth
try:
from urlparse import parse_qsl
except ImportError:
from cgi import parse_qsl
try:
import simplejson as json
except ImportError:
try:
import json
except ImportError:
try:
from django.utils import simplejson as json
except ImportError:
raise ImportError('A json library is required to use this python library. Lol, yay for being verbose. ;)')
# We need to import a XML Parser because Flickr doesn't return JSON for photo uploads -_-
try:
from lxml import etree
except ImportError:
try:
# Python 2.5
import xml.etree.cElementTree as etree
except ImportError:
try:
# Python 2.5
import xml.etree.ElementTree as etree
except ImportError:
try:
#normal cElementTree install
import cElementTree as etree
except ImportError:
try:
# normal ElementTree install
import elementtree.ElementTree as etree
except ImportError:
raise ImportError('Failed to import ElementTree from any known place')
writer = codecs.lookup('utf-8')[3]
def get_content_type(filename):
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
def iter_fields(fields):
"""Iterate over fields.
Supports list of (k, v) tuples and dicts.
"""
if isinstance(fields, dict):
return ((k, v) for k, v in fields.iteritems())
return ((k, v) for k, v in fields)
class FlickrAPIError(Exception):
""" Generic error class, catch-all for most Tumblpy issues.
from Tumblpy import FlickrAPIError, FlickrAuthError
"""
def __init__(self, msg, error_code=None):
self.msg = msg
self.code = error_code
if error_code is not None and error_code < 100:
raise FlickrAuthError(msg, error_code)
def __str__(self):
return repr(self.msg)
class FlickrAuthError(FlickrAPIError):
""" Raised when you try to access a protected resource and it fails due to some issue with your authentication. """
def __init__(self, msg, error_code=None):
self.msg = msg
self.code = error_code
def __str__(self):
return repr(self.msg)
class FlickrAPI(object):
def __init__(self, api_key=None, api_secret=None, oauth_token=None, oauth_token_secret=None, callback_url=None, headers=None, client_args=None):
if not api_key or not api_secret:
raise FlickrAPIError('Please supply an api_key and api_secret.')
self.api_key = api_key
self.api_secret = api_secret
self.oauth_token = oauth_token
self.oauth_token_secret = oauth_token_secret
self.callback_url = callback_url
self.api_base = 'https://api.flickr.com/services'
self.up_api_base = 'https://up.flickr.com/services'
self.rest_api_url = '%s/rest' % self.api_base
self.upload_api_url = '%s/upload/' % self.up_api_base
self.replace_api_url = '%s/replace/' % self.up_api_base
self.request_token_url = 'https://www.flickr.com/services/oauth/request_token'
self.access_token_url = 'https://www.flickr.com/services/oauth/access_token'
self.authorize_url = 'https://www.flickr.com/services/oauth/authorize'
self.headers = headers
if self.headers is None:
self.headers = {'User-agent': 'Python-Flickr v%s' % __version__}
self.consumer = None
self.token = None
client_args = client_args or {}
if self.api_key is not None and self.api_secret is not None:
self.consumer = oauth.Consumer(self.api_key, self.api_secret)
if self.oauth_token is not None and self.oauth_token_secret is not None:
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 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)
def get_authentication_tokens(self, perms=None):
""" Returns an authorization url to give to your user.
Parameters:
perms - If None, this is ignored and uses your applications default perms. If set, will overwrite applications perms; acceptable perms (read, write, delete)
* read - permission to read private information
* write - permission to add, edit and delete photo metadata (includes 'read')
* delete - permission to delete photos (includes 'write' and 'read')
"""
request_args = {}
resp, content = self.client.request('%s?oauth_callback=%s' % (self.request_token_url, self.callback_url), 'GET', **request_args)
if resp['status'] != '200':
raise FlickrAuthError('There was a problem retrieving an authentication url.')
request_tokens = dict(parse_qsl(content))
auth_url_params = {
'oauth_token': request_tokens['oauth_token']
}
accepted_perms = ('read', 'write', 'delete')
if perms and perms in accepted_perms:
auth_url_params['perms'] = perms
request_tokens['auth_url'] = '%s?%s' % (self.authorize_url, urllib.urlencode(auth_url_params))
return request_tokens
def get_auth_tokens(self, oauth_verifier):
""" Returns 'final' tokens to store and used to make authorized calls to Flickr.
Parameters:
oauth_token - oauth_token returned from when the user is redirected after hitting the get_auth_url() function
verifier - oauth_verifier returned from when the user is redirected after hitting the get_auth_url() function
"""
params = {
'oauth_verifier': oauth_verifier,
}
resp, content = self.client.request('%s?%s' % (self.access_token_url, urllib.urlencode(params)), 'GET')
if resp['status'] != '200':
raise FlickrAuthError('Getting access tokens failed: %s Response Status' % resp['status'])
return dict(parse_qsl(content))
def api_request(self, endpoint=None, method='GET', params={}, files=None, replace=False):
self.headers.update({'Content-Type': 'application/json'})
self.headers.update({'Content-Length': '0'})
if endpoint is None and files is None:
raise FlickrAPIError('Please supply an API endpoint to hit.')
qs = {
'format': 'json',
'nojsoncallback': 1,
'method': endpoint,
'api_key': self.api_key
}
if method == 'POST':
if files is not None:
# To upload/replace file, we need to create a fake request
# to sign parameters that are not multipart before we add
# the multipart file to the parameters...
# OAuth is not meant to sign multipart post data
http_url = self.replace_api_url if replace else self.upload_api_url
faux_req = oauth.Request.from_consumer_and_token(self.consumer,
token=self.token,
http_method="POST",
http_url=http_url,
parameters=params)
faux_req.sign_request(oauth.SignatureMethod_HMAC_SHA1(),
self.consumer,
self.token)
all_upload_params = dict(parse_qsl(faux_req.to_postdata()))
# For Tumblr, all media (photos, videos)
# are sent with the 'data' parameter
all_upload_params['photo'] = (files.name, files.read())
body, content_type = self.encode_multipart_formdata(all_upload_params)
self.headers.update({
'Content-Type': content_type,
'Content-Length': str(len(body))
})
req = urllib2.Request(http_url, body, self.headers)
try:
req = urllib2.urlopen(req)
except urllib2.HTTPError, e:
# Making a fake resp var because urllib2.urlopen doesn't
# return a tuple like OAuth2 client.request does
resp = {'status': e.code}
content = e.read()
# After requests is finished, delete Content Length & Type so
# requests after don't use same Length and take (i.e 20 sec)
del self.headers['Content-Type']
del self.headers['Content-Length']
# If no error, assume response was 200
resp = {'status': 200}
content = req.read()
content = etree.XML(content)
stat = content.get('stat') or 'ok'
if stat == 'fail':
if content.find('.//err') is not None:
code = content.findall('.//err[@code]')
msg = content.findall('.//err[@msg]')
if len(code) > 0:
if len(msg) == 0:
msg = 'An error occurred making your Flickr API request.'
else:
msg = msg[0].get('msg')
code = int(code[0].get('code'))
content = {
'stat': 'fail',
'code': code,
'message': msg
}
else:
photoid = content.find('.//photoid')
if photoid is not None:
photoid = photoid.text
content = {
'stat': 'ok',
'photoid': photoid
}
else:
url = self.rest_api_url + '?' + urllib.urlencode(qs) + '&' + urllib.urlencode(params)
resp, content = self.client.request(url, 'POST', headers=self.headers)
else:
params.update(qs)
resp, content = self.client.request('%s?%s' % (self.rest_api_url, urllib.urlencode(params)), 'GET', headers=self.headers)
#try except for if content is able to be decoded
try:
if type(content) != dict:
content = json.loads(content)
except ValueError:
raise FlickrAPIError('Content is not valid JSON, unable to be decoded.')
status = int(resp['status'])
if status < 200 or status >= 300:
raise FlickrAPIError('Flickr returned a Non-200 response.', error_code=status)
if content.get('stat') and content['stat'] == 'fail':
raise FlickrAPIError('Flickr returned error code: %d. Message: %s' % \
(content['code'], content['message']),
error_code=content['code'])
return dict(content)
def get(self, endpoint=None, params=None):
params = params or {}
return self.api_request(endpoint, method='GET', params=params)
def post(self, endpoint=None, params=None, files=None, replace=False):
params = params or {}
return self.api_request(endpoint, method='POST', params=params, files=files, replace=replace)
# Thanks urllib3 <3
def encode_multipart_formdata(self, fields, boundary=None):
"""
Encode a dictionary of ``fields`` using the multipart/form-data mime format.
:param fields:
Dictionary of fields or list of (key, value) field tuples. The key is
treated as the field name, and the value as the body of the form-data
bytes. If the value is a tuple of two elements, then the first element
is treated as the filename of the form-data section.
Field names and filenames must be unicode.
:param boundary:
If not specified, then a random boundary will be generated using
:func:`mimetools.choose_boundary`.
"""
body = BytesIO()
if boundary is None:
boundary = mimetools.choose_boundary()
for fieldname, value in iter_fields(fields):
body.write('--%s\r\n' % (boundary))
if isinstance(value, tuple):
filename, data = value
writer(body).write('Content-Disposition: form-data; name="%s"; '
'filename="%s"\r\n' % (fieldname, filename))
body.write('Content-Type: %s\r\n\r\n' %
(get_content_type(filename)))
else:
data = value
writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
% (fieldname))
body.write(b'Content-Type: text/plain\r\n\r\n')
if isinstance(data, int):
data = str(data) # Backwards compatibility
if isinstance(data, unicode):
writer(body).write(data)
else:
body.write(data)
body.write(b'\r\n')
body.write('--%s--\r\n' % (boundary))
content_type = 'multipart/form-data; boundary=%s' % boundary
return body.getvalue(), content_type