#!/usr/bin/python # # Some random Python script for doing flickr stuff. # # Before using, be sure to go down and fill in values for: # # flickrAPIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # flickrSecret = "yyyyyyyyyyyyyyyy" # # Inspired largely by Michele Campeotto's flickrclient and Aaron Swartz' # xmltramp... but I wanted to get a better idea of how python worked in # those regards, so I mostly worked those components out for myself. Of # course, flickrclient and xmltramp are much more well-rounded. # # http://micampe.it/things/flickrclient # http://www.aaronsw.com/2002/xmltramp/ # # Could this script be better? Hell yeah. My python skillz are far # from 'leet. But at least it's a working example of the new auth API. # Well, I /think/ it works... # # Release 1: initial release # Release 2: added upload functionality # Release 3: code cleanup, convert to doc strings # # I hereby grant this source to the public domain. # # Beej Jorgensen, July 2005 # import sys import md5 import string import urllib import httplib import os.path import xml.dom.minidom ######################################################################## # XML functionality ######################################################################## #----------------------------------------------------------------------- class XMLNode: """XMLNode -- generic class for holding an XML node xmlStr = \"\"\" Name0 Name1 \"\"\" f = ParseXML(xmlStr) print f.elementName print f['foo'] print f.name print f.name[0].elementName print f.name[0]["bar"] print f.name[0].elementText print f.name[1].elementName print f.name[1]["bar"] print f.name[1]["baz"] """ def __init__(self): """Construct an empty XML node.""" self.elementName="" self.elementText="" self.attrib={} self.xml="" def __setitem__(self, key, item): """Store a node's attribute in the attrib hash.""" self.attrib[key] = item def __getitem__(self, key): """Retrieve a node's attribute from the attrib hash.""" return self.attrib[key] #----------------------------------------------------------------------- def ParseXML(xmlStr, storeXML=False): """Convert an XML string into a nice instance tree of XMLNodes. xmlStr -- the XML to parse storeXML -- if True, stores the XML string in the root XMLNode.xml """ def __ParseXMLElement(element, thisNode): """Recursive call to process this XMLNode.""" thisNode.elementName = element.nodeName #print element.nodeName # add element attributes as attributes to this node for i in range(element.attributes.length): an = element.attributes.item(i) thisNode[an.name] = an.nodeValue for a in element.childNodes: if a.nodeType == xml.dom.Node.ELEMENT_NODE: child = XMLNode() try: list = getattr(thisNode, a.nodeName) except AttributeError: setattr(thisNode, a.nodeName, []) # add the child node as an attrib to this node list = getattr(thisNode, a.nodeName); #print "appending child: %s to %s" % (a.nodeName, thisNode.elementName) list.append(child); __ParseXMLElement(a, child) elif a.nodeType == xml.dom.Node.TEXT_NODE: thisNode.elementText += a.nodeValue return thisNode dom = xml.dom.minidom.parseString(xmlStr) # get the root rootNode = XMLNode() if storeXML: rootNode.xml = xmlStr return __ParseXMLElement(dom.firstChild, rootNode) ######################################################################## # Flickr functionality ######################################################################## #----------------------------------------------------------------------- class Flickr: """Encapsulated flickr functionality. Example usage: flickr = Flickr(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 Flickr instance for a given API key and secret.""" self.apiKey = apiKey self.secret = secret #------------------------------------------------------------------- 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 This might create a new handler each time the method is called--I'm not sure. But I also don't know how to break the nested function out. example usage: flickr.auth_getFrob(api_key="AAAAAA") rsp = flickr.favorites_getList(api_key=flickrAPIKey, \\ auth_token=token) """ def handler(_self = self, _method = method, **arg): _method = "flickr." + _method.replace("_", ".") url = "http://" + Flickr.flickrHost + Flickr.flickrRESTForm arg["method"] = _method postData = urllib.urlencode(arg) + "&api_sig=" + self.sign(arg) #print postData f = urllib.urlopen(url, postData) data = f.read() f.close() return ParseXML(data, True) return handler #------------------------------------------------------------------- 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 Flickr.auth_getFrob() """ data = {"api_key": self.apiKey, "frob": frob, "perms": perms} data["api_sig"] = self.sign(data) return "http://%s%s?%s" % (Flickr.flickrHost, Flickr.flickrAuthForm, \ urllib.urlencode(data)) #------------------------------------------------------------------- def upload(self, filename, **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: api_key auth_token -- documentation mistakenly calls this "auth_hash" 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" """ arg["api_sig"] = self.sign(arg) url = "http://" + Flickr.flickrHost + Flickr.flickrUploadForm # construct POST data boundary = "===beej=jorgensen==========7d45e178b0434" headers = { \ "Content-Type": "multipart/form-data; boundary=%s" % boundary, \ "Host": "www.flickr.com" \ #"Content-Length": 0 \ } body = "" # required params for a in ('api_key', 'auth_token', 'api_sig'): body += "--%s\r\n" % (boundary) body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" body += "%s\r\n" % (arg[a]) # optional params for a in ('title', 'description', 'tags', 'is_public' \ 'is_friend', 'is_family'): if arg.has_key(a): body += "--%s\r\n" % (boundary) body += "Content-Disposition: form-data; name=\""+a+"\"\r\n\r\n" body += "%s\r\n" % (arg[a]) body += "--%s\r\n" % (boundary) body += "Content-Disposition: form-data; name=\"photo\";" body += " filename=\"%s\"\r\n" % filename body += "Content-Type: image/jpeg\r\n\r\n" try: fp = file(filename, "rb") jpegData = fp.read() fp.close() postData = body.encode("utf_8") + jpegData + \ ("--%s--" % (boundary)).encode("utf_8") conn = httplib.HTTPConnection(Flickr.flickrHost) conn.request("POST", Flickr.flickrUploadForm, postData, headers) response = conn.getresponse() rspXML = response.read() conn.close() except IOError: return None return ParseXML(rspXML) ######################################################################## # App functionality ######################################################################## #----------------------------------------------------------------------- def testFailure(rsp): """Exit app if the rsp XMLNode indicates failure.""" if rsp['stat'] == "fail": sys.stderr.write("%s: error %s: %s\n" % (rsp.elementName, \ rsp.err[0]['code'], rsp.err[0]['msg'])) sys.exit(1) #----------------------------------------------------------------------- def getCachedTokenPath(): """Return the directory holding the app data.""" return os.path.expanduser("~/.flickr") #----------------------------------------------------------------------- def getCachedTokenFilename(apiKey): """Return the full pathname of the cached token file.""" return "%s/%s_auth.xml" % (getCachedTokenPath(), apiKey) #----------------------------------------------------------------------- def getCachedToken(apiKey): """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(getCachedTokenFilename(apiKey), "r") data = f.read() f.close() rsp = ParseXML(data) return rsp.auth[0].token[0].elementText except IOError: return None #----------------------------------------------------------------------- def setCachedToken(apiKey, xml): """Cache a token for later use. The cached tag is stored by simply saving the entire RSP response containing the auth element. """ path = getCachedTokenPath() if not os.path.exists(path): os.mkdir(path) f = file(getCachedTokenFilename(apiKey), "w") f.write(xml) f.close() #----------------------------------------------------------------------- def getToken(flickr, apiKey): """Get a token either from the cache, or make a new one from the frob. This first attempts to find a token in the user's token cache on disk. If that fails (or if the token is no longer valid based on flickr.auth.checkToken) a new frob is acquired. The frob is validated by having the user log into flickr (with lynx), and subsequently a valid token is retrieved. The newly minted token is then cached locally for the next run. """ # see if we have a saved token token = getCachedToken(apiKey) # see if it's valid if token != None: rsp = flickr.auth_checkToken(api_key=apiKey, auth_token=token) if rsp['stat'] != "ok": token = None # get a new token if we need one if token == None: # get the frob rsp = flickr.auth_getFrob(api_key=apiKey) testFailure(rsp) frob = rsp.frob[0].elementText # validate online os.system("lynx '%s'" % (flickr.getAuthURL("write", frob))) # get a token rsp = flickr.auth_getToken(api_key=apiKey, frob=frob) testFailure(rsp) token = rsp.auth[0].token[0].elementText # store the auth info for next time setCachedToken(flickrAPIKey, rsp.xml) return token #======================================================================= def main(argv): # my flickr information: flickrAPIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # beej's key flickrSecret = "yyyyyyyyyyyyyyyy" # beej's "secret" # make a new Flickr instance flickr = Flickr(flickrAPIKey, flickrSecret) # do the whole whatever-it-takes to get a valid token: token = getToken(flickr, flickrAPIKey) # get my favorites rsp = flickr.favorites_getList(api_key=flickrAPIKey,auth_token=token) testFailure(rsp) # and print them for a in rsp.photos[0].photo: print "%10s: %s" % (a['id'], a['title']) # upload the file foo.jpg #rsp = flickr.upload("foo.jpg", api_key=flickrAPIKey, auth_token=token, \ # title="This is the title", description="This is the description", \ # tags="tag1 tag2 tag3") #if rsp == None: # sys.stderr.write("can't find file\n") #else: # testFailure(rsp) # Now go get drunk. You've earned it. return 0 # run the main if we're not being imported: if __name__ == "__main__": sys.exit(main(sys.argv))