1 import mimetools
2 import os
3 import time
4 import urllib
5 import urllib2
6 from xml.dom import minidom
7
8 import utils
9
11 """This exception class represents any kind of error raised by the
12 Flickr API."""
13 pass
14
16 """The abstract class representing the machinery of the request/response
17 formats.
18
19 Any implementor *must* override:
20 * response_format
21 * _get()
22 * _get_backend_params()
23 * _handle_error()
24 * _parse_response()
25
26 * auth_checkToken()
27 * auth_getToken()
28 * auth_getFrob()
29
30 For the authentication step also "authenticate_step_one()" can be overridden.
31
32 This backend uses REST as a Request format."""
33 - def __init__(self, api_key, secret_key):
34 self.api_key = api_key
35 self.secret_key = secret_key
36 self.token = None
37 self.frob = None
38
39
40
41 self.response_format = None
42 self.cache = utils.TokenCache(self.api_key)
43
44 - def _get(self, method, auth, **params):
45 """Retrieves the data from the Flickr server.
46
47 * method: the name of the remote method to invoke.
48 * auth: True if the call has to be authenticated. False otherwise.
49 * params: any argument for the call."""
50 raise NotImplementedError
51
53 """Used to setup specific backend parameters for the HTTP request.
54
55 To be overridden.
56
57 Must return a dict() of params."""
58 raise NotImplementedError
59
61 """Here goes all the error handling for the response.
62
63 To be overridden.
64
65 It should raise BackendError extracting the proper message data
66 from the response.
67 The response format is already converted by _parse_response."""
68 raise NotImplementedError
69
71 """Should convert the response into valid Python data types.
72
73 To be overridden."""
74 raise NotImplementedError
75
77 """Calls the remote API method.
78
79 * api_method_name is the Flickr name (eg. flickr.photos.getInfo)
80 * auth is a boolean indicating whether or not the call requires authentication
81 * params contains any additional parameter"""
82 response = self._get(api_method_name, auth, **params)
83 response = self._parse_response(response)
84 self._handle_error(response)
85 return response
86
88 """All the authentication machinery.
89
90 * requested_perms can be 'read', 'write', 'delete'
91
92 See: <http://www.flickr.com/services/api/auth.spec.html>
93
94 Takes also care of storing the token on the file system."""
95 self.token = self.token or self.cache.get()
96
97 if self.token:
98 self.token, perms = self.auth_checkToken()
99
100
101 if perms == 'read' and requested_perms in ('write', 'delete'):
102 self.token = None
103 if perms == 'write' and requested_perms == 'delete':
104 self.token = None
105
106 if not self.token:
107 self.authenticate_step_one(requested_perms)
108 self.token = self.auth_getToken()
109 self.cache.put(self.token)
110
111
113 """ This implement the first step of authentication machinery.
114
115 Note: override this to do your first step. By default uses the system
116 browser to authenticate and desktop authentication."""
117
118 frob = self.auth_getFrob()
119 auth_url = self._get_authentication_url(perms, frob)
120 utils.open_url_in_browser(auth_url)
121 time.sleep(sleep_time)
122
124 d = dict(api_key=self.api_key, frob=frob, perms=perms)
125 d['api_sig'] = utils.get_api_signature(self.secret_key, **d)
126 auth_url = "%s?%s" % (utils.AUTH_API_URL, urllib.urlencode(d))
127 return auth_url
128
130 """flickr.auth.checkToken:
131 Returns the credentials attached to an authentication token.
132 This method call must be signed.
133
134 To be overridden.
135
136 See: <http://www.flickr.com/services/api/flickr.auth.checkToken.html>"""
137 raise NotImplementedError
138
140 """flickr.auth.getFrob:
141 Returns a frob to be used during authentication.
142 This method call must be signed.
143
144 To be overridden.
145
146 See: <http://www.flickr.com/services/api/flickr.auth.getFrob.html>"""
147 raise NotImplementedError
148
150 """flickr.auth.getToken:
151 Returns the auth token for the given frob, if one has been attached.
152 This method call must be signed.
153
154 To be overridden.
155
156 See: <http://www.flickr.com/services/api/flickr.auth.getToken.html>"""
157 raise NotImplementedError
158
159 - def photo_upload(self, filename=None, photo_data=None,
160 data_content_type='image/jpeg', **params):
161 """Uploading photos:
162 It works outside the normal Flickr API framework because it
163 involves sending binary files over the wire.
164
165 You can specify either the filename or pass the binary image data.
166 The image content type defaults to JPEG.
167
168 See: <http://www.flickr.com/services/api/upload.api.html>
169 """
170 params['api_key'] = self.api_key
171 params['auth_token'] = self.token
172 params['api_sig'] = utils.get_api_signature(self.secret_key, **params)
173
174
175
176 boundary = mimetools.choose_boundary()
177 request_body = []
178
179
180 for field in ('api_key', 'auth_token', 'api_sig'):
181 request_body.append("--%s\r\n" % boundary)
182 request_body.append(
183 'Content-Disposition: form-data; name="%s"\r\n\r\n' % field)
184 request_body.append("%s\r\n" % params[field])
185
186
187 for field in ('title', 'description', 'tags', 'is_public', 'is_friend',
188 'is_family', 'safety_level', 'content_type', 'hidden'):
189 if field in params:
190 request_body.append("--%s\r\n" % boundary)
191 request_body.append(
192 'Content-Disposition: form-data; name="%s"\r\n\r\n' % field)
193 request_body.append("%s\r\n" % params[field])
194
195
196 request_body.append("--%s\r\n" % boundary)
197 request_body.append(
198 'Content-Disposition: form-data;'
199 'name="photo"; filename="%s"\r\n' % filename)
200 request_body.append(
201 'Content-Type: %s\r\n\r\n' % data_content_type or 'image/jpeg')
202
203 if filename and photo_data is None:
204 path = os.path.abspath(os.path.normpath(filename))
205 f = open(path, "rb")
206 photo_data = f.read()
207 f.close()
208
209 request_data = []
210 request_data.append("".join(request_body).encode('utf-8'))
211 request_data.append(photo_data)
212 request_data.append(('\r\n--%s--' % boundary).encode('utf-8'))
213 request_data = "".join(request_data)
214
215 request = urllib2.Request(utils.UPLOAD_URL)
216 request.add_data(request_data)
217 request.add_header(
218 "Content-Type", "multipart/form-data; boundary=%s" % boundary)
219
220 response = urllib2.urlopen(request)
221 photo_data = response.read()
222
223 def _handle_error(photo_data):
224 dom = minidom.parseString(photo_data)
225 rsp = dom.getElementsByTagName('rsp')[0]
226 if rsp.getAttribute('stat') == 'fail':
227 err = rsp.getElementsByTagName('err')[0]
228 code = err.getAttribute('code')
229 message = err.getAttribute('msg')
230 value = "%s: %s" % (code, message)
231 raise BackendError(value)
232
233 def _get_photo_id(photo_data):
234 dom = minidom.parseString(photo_data)
235 photo_id_tag = dom.getElementsByTagName('photoid')[0]
236 return photo_id_tag.firstChild.nodeValue
237
238 _handle_error(photo_data)
239 photo_id = _get_photo_id(photo_data)
240 return photo_id
241