""" PyS60 FlickrAPI - Flickr API implementation for Python for Series 60 Copyright (C) 2006 Teemu Harju This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. PyS60 FlickrAPI is based on Beej's awesome Python Flickr API. http://beej.us/flickr/flickrapi/ teemu.harju@gmail.com $Author$ $Rev$ $LastChangedDate$ """ import sys import md5 import string import urllib import httplib import mimetools import os.path import os import time import socket from __main__ import APPPATH import xmllib ######################################################################## # Exceptions ######################################################################## class UploadException(Exception): pass class FlickrException(Exception): def __init__(self, msg): self.msg = msg def __str__(self): return repr(self.msg) ######################################################################## # Class method wrapper (Python recipe 52304) ######################################################################## class Callable: """Callable -- simple wrapper for class methods PyS60 does not support @classmethod so this is needed. """ def __init__(self, anycallable): self.__call__ = anycallable ######################################################################## # HTTP post functionality for upload ######################################################################## class HTTP: def post_multipart(host, selector, fields, files): """ Post fields and files to an http host as multipart/form-data. fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files Return the server's response page. """ content_type, body = HTTP.encode_multipart_formdata(fields, files) h = httplib.HTTP(host) h.putrequest('POST', selector) h.putheader('content-type', content_type) h.putheader('content-length', str(len(body))) h.endheaders() h.send(body) errcode, errmsg, headers = h.getreply() return h.file.read() post_multipart = Callable(post_multipart) def encode_multipart_formdata(fields, files): """ fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files Return (content_type, body) ready for httplib.HTTP instance """ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' CRLF = '\r\n' L = [] for (key, value) in fields.iteritems(): L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key.encode("ascii", "replace")) L.append('') L.append(value.encode("ascii", "replace")) for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename.encode("ascii", "replace"))) L.append('Content-Type: "image/jpeg"') L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body encode_multipart_formdata = Callable(encode_multipart_formdata) ######################################################################## # XML functionality ######################################################################## class XMLNode: 'A parsed XML element' def __init__(self, name="", attrib={}): self.elementName = name self.elementText = '' self.attrib = attrib self.xml = '' #def __setitem__(self, key, item): # self.attrib[key] = item def __getitem__(self, key): return self.attrib[key] def parseXML(xmlStr, storeXML=False): parser = Xml2Obj() rootNode = parser.Parse(xmlStr) if storeXML: rootNode.xml = xmlStr return rootNode parseXML = Callable(parseXML) # class method wrapper class Xml2Obj: 'XML to Object' def __init__(self): self.root = None self.nodeStack = [] def StartElement(self, name, attributes): elementName = name.encode() 'SAX start element even handler' # Instantiate an XMLNode object element = XMLNode(elementName, attributes) # Push element onto the stack and make it a child of parent # children are list attributes of a node if len(self.nodeStack) > 0: parent = self.nodeStack[-1] childList = None try: childList = getattr(parent, elementName) childList.append(element) setattr(parent, elementName, childList) except AttributeError: 'The first child for the parent' setattr(parent, elementName, [element]) else: self.root = element self.nodeStack.append(element) def EndElement(self,name): 'SAX end element event handler' self.nodeStack = self.nodeStack[:-1] def CharacterData(self,data): 'SAX character data event handler' if string.strip(data): data = data.encode("ascii", "replace") element = self.nodeStack[-1] element.elementText += data return def Parse(self,data): parser = xmllib.XMLParser() parser.unknown_starttag = self.StartElement parser.unknown_endtag = self.EndElement parser.handle_data = self.CharacterData parser.feed(data) parser.close() # Create a SAX parser #Parser = expat.ParserCreate() # SAX event handlers #Parser.StartElementHandler = self.StartElement #Parser.EndElementHandler = self.EndElement #Parser.CharacterDataHandler = self.CharacterData # Parse the XML File #ParserStatus = Parser.Parse(data, 1) return self.root ######################################################################## # Flickr functionality ######################################################################## #----------------------------------------------------------------------- class FlickrAPI: """Encapsulated flickr functionality. Example usage: flickr = FlickrAPI(flickrAPIKey, flickrSecret) rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token) """ flickrHost = "flickr.com" flickrRESTForm = "/services/rest/" flickrAuthForm = "/services/auth/" flickrUploadForm = "/services/upload/" #------------------------------------------------------------------- def __init__(self, apiKey, secret): """Construct a new FlickrAPI instance for a given API key and secret.""" self.apiKey = apiKey self.secret = secret self.username = None self.fullname = None self.__handlerCache={} #------------------------------------------------------------------- def __sign(self, data): """Calculate the flickr signature for a set of params. data -- a hash of all the params and values to be hashed, e.g. {"api_key":"AAAA", "auth_token":"TTTT"} """ dataName = self.secret keys = data.keys() keys.sort() for a in keys: dataName += (a + data[a]) #print dataName hash = md5.new() hash.update(dataName) return hash.hexdigest() #------------------------------------------------------------------- def __getattr__(self, method, **arg): """Handle all the flickr API calls. This is Michele Campeotto's cleverness, wherein he writes a general handler for methods not defined, and assumes they are flickr methods. He then converts them to a form to be passed as the method= parameter, and goes from there. http://micampe.it/things/flickrclient My variant is the same basic thing, except it tracks if it has already created a handler for a specific call or not. example usage: flickr.auth_getFrob(api_key="AAAAAA") rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\ auth_token=token) """ if not self.__handlerCache.has_key(method): def handler(_self = self, _method = method, **arg): _method = "flickr." + _method.replace("_", ".") url = "http://" + FlickrAPI.flickrHost + \ FlickrAPI.flickrRESTForm arg["method"] = _method postData = urllib.urlencode(arg) + "&api_sig=" + \ _self.__sign(arg) #print "--url---------------------------------------------" #print url #print "--postData----------------------------------------" #print postData f = urllib.urlopen(url, postData) data = f.read() #print "--response----------------------------------------" #print data f.close() return XMLNode.parseXML(data, True) self.__handlerCache[method] = handler; return self.__handlerCache[method] #------------------------------------------------------------------- def __getAuthURL(self, perms, frob): """Return the authorization URL to get a token. This is the URL the app will launch a browser toward if it needs a new token. perms -- "read", "write", or "delete" frob -- picked up from an earlier call to FlickrAPI.auth_getFrob() """ data = {"api_key": self.apiKey, "frob": frob, "perms": perms} data["api_sig"] = self.__sign(data) return "http://%s%s?%s" % (FlickrAPI.flickrHost, \ FlickrAPI.flickrAuthForm, urllib.urlencode(data)) #------------------------------------------------------------------- def upload(self, filename=None, jpegData=None, **arg): """Upload a file to flickr. Be extra careful you spell the parameters correctly, or you will get a rather cryptic "Invalid Signature" error on the upload! Supported parameters: One of filename or jpegData must be specified by name when calling this method: filename -- name of a file to upload jpegData -- array of jpeg data to upload api_key auth_token title description tags -- space-delimited list of tags, "tag1 tag2 tag3" is_public -- "1" or "0" is_friend -- "1" or "0" is_family -- "1" or "0" """ if filename == None and jpegData == None or \ filename != None and jpegData != None: raise UploadException("filename OR jpegData must be specified") # verify key names for a in arg.keys(): if a != "api_key" and a != "auth_token" and a != "title" and \ a != "description" and a != "tags" and a != "is_public" and \ a != "is_friend" and a != "is_family": sys.stderr.write("FlickrAPI: warning: unknown parameter " \ "\"%s\" sent to FlickrAPI.upload\n" % (a)) arg["api_sig"] = self.__sign(arg) url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm if filename != None: fp = file(filename, "rb") data = fp.read() fp.close() else: data = jpegData rspXML = HTTP.post_multipart(FlickrAPI.flickrHost, FlickrAPI.flickrUploadForm, arg, [("photo", filename, data)]) return XMLNode.parseXML(rspXML) #----------------------------------------------------------------------- #@classmethod def testFailure(rsp, exit=True): """Exit app if the rsp XMLNode indicates failure.""" if rsp['stat'] == "fail": #sys.stderr.write("%s\n" % (cls.getPrintableError(rsp))) #if exit: sys.exit(1) raise FlickrException(FlickrAPI.getPrintableError(rsp)) testFailure = Callable(testFailure) #----------------------------------------------------------------------- #@classmethod def getPrintableError(rsp): """Return a printed error message string.""" return "%s: error %s: %s" % (rsp.elementName, \ FlickrAPI.getRspErrorCode(rsp), FlickrAPI.getRspErrorMsg(rsp)) getPrintableError = Callable(getPrintableError) #----------------------------------------------------------------------- #@classmethod def getRspErrorCode(rsp): """Return the error code of a response, or 0 if no error.""" if rsp['stat'] == "fail": return rsp.err[0]['code'] return 0 getRspErrorCode = Callable(getRspErrorCode) #----------------------------------------------------------------------- #@classmethod def getRspErrorMsg(rsp): """Return the error message of a response, or "Success" if no error.""" if rsp['stat'] == "fail": return rsp.err[0]['msg'] return "Success" getRspErrorMsg = Callable(getRspErrorMsg) #----------------------------------------------------------------------- def __getCachedTokenPath(self): """Return the directory holding the app data.""" #path = os.path.join(sys.path[0], "flickr", self.apiKey) path = APPPATH return unicode(path) #----------------------------------------------------------------------- def __getCachedTokenFilename(self): """Return the full pathname of the cached token file.""" filename = os.path.join(self.__getCachedTokenPath(), "auth.xml") return unicode(filename) #----------------------------------------------------------------------- def __getCachedToken(self): """Read and return a cached token, or None if not found. The token is read from the cached token file, which is basically the entire RSP response containing the auth element. """ try: f = file(self.__getCachedTokenFilename(), "r") data = f.read() f.close() rsp = XMLNode.parseXML(data) return rsp.auth[0].token[0].elementText except IOError: return None #----------------------------------------------------------------------- def __setCachedToken(self, xml): """Cache a token for later use. The cached tag is stored by simply saving the entire RSP response containing the auth element. """ path = self.__getCachedTokenPath() if not os.path.exists(path): os.makedirs(path) f = file(self.__getCachedTokenFilename(), "w") f.write(xml) f.close() #----------------------------------------------------------------------- def checkToken(self, perms="read"): """Checks if the token exists in the cache and whether it is valid. If cached token is valid it is returned. Otherwise None is returned. """ # see if we have a saved token token = self.__getCachedToken() rsp = None # see if it's valid if token != None: rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token) if rsp['stat'] != "ok": token = None else: # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].elementText if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None # if token is still valid if token != None: # store user information self.username = rsp.auth[0].user[0]['username'] self.fullname = rsp.auth[0].user[0]['fullname'] return token #----------------------------------------------------------------------- def validate(self, perms="read"): """Validates the application with given permissions. This function does not actually validate the application, but only returns the authentication URL that can then be shown to the user. This is done for implementations where browser cannot be launched from the Python. """ rsp = self.auth_getFrob(api_key=self.apiKey) self.testFailure(rsp) frob = rsp.frob[0].elementText return frob, self.__getAuthURL(perms, frob) #----------------------------------------------------------------------- def getToken(self, frob): """Gets a token for given frob and stores it in the cache for future use. """ rsp = self.auth_getToken(api_key=self.apiKey, frob=frob) self.testFailure(rsp) token = rsp.auth[0].token[0].elementText # store user information self.username = rsp.auth[0].user[0]['username'] self.fullname = rsp.auth[0].user[0]['fullname'] # store the auth info for next time self.__setCachedToken(rsp.xml) return token #----------------------------------------------------------------------- def getFullToken(self, miniToken): """Exchanges the mini-token for a full token. This is needed for the mobile applications. Returns the token with the flickr username and fullname of the authenticated user. """ data = {"api_key" : self.apiKey, "method" : "flickr.auth.getFullToken", "mini_token" : miniToken} rsp = self.auth_getFullToken(api_key=self.apiKey, mini_token=miniToken) self.testFailure(rsp) token = rsp.auth[0].token[0].elementText # store user information self.username = rsp.auth[0].user[0]['username'] self.fullname = rsp.auth[0].user[0]['fullname'] # cache the token for future use self.__setCachedToken(rsp.xml) return token ######################################################################## # App functionality ######################################################################## def main(argv): # flickr auth information: flickrAPIKey = "" # API key flickrSecret = "" # shared "secret" # make a new FlickrAPI instance fapi = FlickrAPI(flickrAPIKey, flickrSecret) # check if you already have proper token cached token = fapi.checkToken(perms="read") if token: pass # everythin ok else: # ask user to give the minitoken #miniToken = raw_input("Give the minitoken: ") miniToken = appuifw.query(u'Give the mini-token:', 'text') token = fapi.getFullToken(miniToken) # your favourites rsp = fapi.favorites_getList(api_key=flickrAPIKey,auth_token=token) fapi.testFailure(rsp) # and print them for a in rsp.photos[0].photo: print "%10s: %s" % (a['id'], a['title'].encode("ascii", "replace")) # upload the file foo.jpg rsp = fapi.upload(filename="e:\\Images\\test.jpg", api_key=flickrAPIKey, auth_token=token, title="This is the title", description="This is the description", tags="tag1 tag2 tag3", is_public="1") if rsp == None: sys.stderr.write("can't find file\n") else: fapi.testFailure(rsp) print "Photo uploaded..." return 0 # run the main if we're not being imported: if __name__ == "__main__": sys.exit(main(sys.argv))