Package elisa :: Package extern :: Module flickrest
[hide private]
[frames] | no frames]

Source Code for Module elisa.extern.flickrest

  1  # flickrpc -- a Flickr client library. 
  2  # 
  3  # Copyright (C) 2007 Ross Burton <ross@burtonini.com> 
  4  # 
  5  # This program is free software; you can redistribute it and/or modify it under 
  6  # the terms of the GNU Lesser General Public License as published by the Free 
  7  # Software Foundation; either version 2, or (at your option) any later version. 
  8  # 
  9  # This program is distributed in the hope that it will be useful, but WITHOUT 
 10  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 11  # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
 12  # details. 
 13  # 
 14  # You should have received a copy of the GNU General Public License along with 
 15  # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 
 16  # St, Fifth Floor, Boston, MA 02110-1301 USA 
 17   
 18  import logging, md5, os, mimetools, urllib 
 19  from twisted.internet import defer 
 20  from twisted.python.failure import Failure 
 21  from twisted.web import client 
 22  try: 
 23      from xml.etree import ElementTree 
 24  except ImportError: 
 25      from elementtree import ElementTree 
 26   
27 -class FlickrError(Exception):
28 - def __init__(self, code, message):
29 Exception.__init__(self) 30 self.code = int(code) 31 self.message = message
32
33 - def __str__(self):
34 return "%d: %s" % (self.code, self.message)
35 36 (SIZE_SQUARE, 37 SIZE_THUMB, 38 SIZE_SMALL, 39 SIZE_MEDIUM, 40 SIZE_LARGE) = range (0, 5) 41
42 -class Flickr:
43 endpoint = "http://api.flickr.com/services/rest/?" 44
45 - def __init__(self, api_key, secret, perms="read"):
46 self.__methods = {} 47 self.api_key = api_key 48 self.secret = secret 49 self.perms = perms 50 self.token = None 51 self.logger = logging.getLogger('flickrest')
52
53 - def __repr__(self):
54 return "<FlickREST>"
55
56 - def __getTokenFile(self):
57 """Get the filename that contains the authentication token for the API key""" 58 return os.path.expanduser(os.path.join("~", ".flickr", self.api_key, "auth.xml"))
59
60 - def clear_cached(self):
61 """Remove any cached information on disk.""" 62 token = self.__getTokenFile() 63 if os.path.exists(token): 64 os.remove(token)
65
66 - def __sign(self, kwargs):
67 kwargs['api_key'] = self.api_key 68 # If authenticating we don't yet have a token 69 if self.token: 70 kwargs['auth_token'] = self.token 71 # I know this is less efficient than working with lists, but this is 72 # much more readable. 73 sig = reduce(lambda sig, key: sig + key + str(kwargs[key]), 74 sorted(kwargs.keys()), 75 self.secret) 76 kwargs['api_sig'] = md5.new(sig).hexdigest()
77
78 - def __call(self, method, kwargs):
79 kwargs["method"] = method 80 self.__sign(kwargs) 81 self.logger.info("Calling %s" % method) 82 return client.getPage(Flickr.endpoint, method="POST", 83 headers={"Content-Type": "application/x-www-form-urlencoded"}, 84 postdata=urllib.urlencode(kwargs))
85
86 - def __cb(self, data, method):
87 self.logger.info("%s returned" % method) 88 xml = ElementTree.XML(data) 89 if xml.tag == "rsp" and xml.get("stat") == "ok": 90 return xml 91 elif xml.tag == "rsp" and xml.get("stat") == "fail": 92 err = xml.find("err") 93 raise FlickrError(err.get("code"), err.get("msg")) 94 else: 95 # Fake an error in this case 96 raise FlickrError(0, "Invalid response")
97
98 - def __getattr__(self, method):
99 method = "flickr." + method.replace("_", ".") 100 if not self.__methods.has_key(method): 101 def proxy(method=method, **kwargs): 102 return self.__call(method, kwargs).addCallback(self.__cb, method)
103 self.__methods[method] = proxy 104 return self.__methods[method]
105 106 @staticmethod
107 - def __encodeForm(inputs):
108 """ 109 Takes a dict of inputs and returns a multipart/form-data string 110 containing the utf-8 encoded data. Keys must be strings, values 111 can be either strings or file-like objects. 112 """ 113 boundary = mimetools.choose_boundary() 114 lines = [] 115 for key, val in inputs.items(): 116 lines.append("--" + boundary.encode("utf-8")) 117 header = 'Content-Disposition: form-data; name="%s";' % key 118 # Objects with name value are probably files 119 if hasattr(val, 'name'): 120 header += 'filename="%s";' % os.path.split(val.name)[1] 121 lines.append(header) 122 header = "Content-Type: application/octet-stream" 123 lines.append(header) 124 lines.append("") 125 # If there is a read attribute, it is a file-like object, so read all the data 126 if hasattr(val, 'read'): 127 lines.append(val.read()) 128 # Otherwise just hope it is string-like and encode it to 129 # UTF-8. TODO: this breaks when val is binary data. 130 else: 131 lines.append(val.encode('utf-8')) 132 # Add final boundary. 133 lines.append("--" + boundary.encode("utf-8")) 134 return (boundary, '\r\n'.join(lines))
135 136 # TODO: add is_public, is_family, is_friends arguments
137 - def upload(self, filename=None, imageData=None, title=None, desc=None, tags=None):
138 # Sanity check the arguments 139 if filename is None and imageData is None: 140 raise ValueError("Need to pass either filename or imageData") 141 if filename and imageData: 142 raise ValueError("Cannot pass both filename and imageData") 143 144 kwargs = {} 145 if title: 146 kwargs['title'] = title 147 if desc: 148 kwargs['description'] = desc 149 if tags: 150 kwargs['tags'] = tags 151 self.__sign(kwargs) 152 153 if imageData: 154 kwargs['photo'] = imageData 155 else: 156 kwargs['photo'] = file(filename, "rb") 157 158 (boundary, form) = self.__encodeForm(kwargs) 159 headers= { 160 "Content-Type": "multipart/form-data; boundary=%s" % boundary, 161 "Content-Length": str(len(form)) 162 } 163 164 self.logger.info("Calling upload") 165 return client.getPage("http://api.flickr.com/services/upload/", method="POST", 166 headers=headers, postdata=form).addCallback(self.__cb, "upload")
167
168 - def authenticate_2(self, state):
169 def gotToken(e): 170 # Set the token 171 self.token = e.find("auth/token").text 172 # Cache the authentication 173 filename = self.__getTokenFile() 174 path = os.path.dirname(filename) 175 if not os.path.exists(path): 176 os.makedirs(path, 0700) 177 f = file(filename, "w") 178 f.write(ElementTree.tostring(e)) 179 f.close() 180 # Callback to the user 181 return True
182 return self.auth_getToken(frob=state['frob']).addCallback(gotToken) 183
184 - def authenticate_1(self):
185 """Attempts to log in to Flickr. The return value is a Twisted Deferred 186 object that callbacks when the first part of the authentication is 187 completed. If the result passed to the deferred callback is None, then 188 the required authentication was locally cached and you are 189 authenticated. Otherwise the result is a dictionary, you should open 190 the URL specified by the 'url' key and instruct the user to follow the 191 instructions. Once that is done, pass the state to 192 flickrest.authenticate_2().""" 193 194 filename = self.__getTokenFile() 195 if os.path.exists(filename): 196 try: 197 e = ElementTree.parse(filename).getroot() 198 self.token = e.find("auth/token").text 199 return defer.succeed(None) 200 except: 201 # TODO: print the exception to stderr? 202 pass 203 204 def gotFrob(xml): 205 frob = xml.find("frob").text 206 keys = { 'perms': self.perms, 207 'frob': frob } 208 self.__sign(keys) 209 url = "http://flickr.com/services/auth/?api_key=%(api_key)s&perms=%(perms)s&frob=%(frob)s&api_sig=%(api_sig)s" % keys 210 return {'url': url, 'frob': frob}
211 return self.auth_getFrob().addCallback(gotFrob) 212 213 @staticmethod
214 - def get_photo_url(photo, size=SIZE_MEDIUM):
215 if photo is None: 216 return None 217 218 # Handle medium as the default 219 suffix = "" 220 if size == SIZE_SQUARE: 221 suffix = "_s" 222 elif size == SIZE_THUMB: 223 suffix = "_t" 224 elif size == SIZE_SMALL: 225 suffix = "_m" 226 elif size == SIZE_LARGE: 227 suffix = "_b" 228 229 return "http://static.flickr.com/%s/%s_%s%s.jpg" % (photo.get("server"), photo.get("id"), photo.get("secret"), suffix)
230