class TokenCacheTest(unittest.TestCase): def setUp(self): import tempfile from flickrapi.tokencache import TokenCache # Use mkdtemp() for backward compatibility with Python 2.7 self.tc_path = tempfile.mkdtemp() self.tc = TokenCache('123-456', path=self.tc_path) def tearDown(self): tc_path = getattr(self, 'tc_path', None) if tc_path is not None: import shutil shutil.rmtree(self.tc_path) def test_get_set_del(self): self.assertIsNone(self.tc.token) # Check setting token, both in memory and on disk. self.tc.token = u'nümbér' self.assertEqual(self.tc.token, u'nümbér') on_disk = open(self.tc.get_cached_token_filename(), 'rb').read() self.assertEqual(on_disk.decode('utf8'), u'nümbér') # Erase from in-RAM cache and try again, to read from disk. self.tc.memory.clear() self.assertEqual(self.tc.token, u'nümbér') del self.tc.token self.assertIsNone(self.tc.token) self.tc.token = u'nümbér' self.tc.forget() self.assertIsNone(self.tc.token) def test_username(self): """Username should impact the location of the cache on disk.""" from flickrapi.tokencache import TokenCache user_tc = TokenCache(u'123-456', username=u'frøbel', path=self.tc_path) tc_path = self.tc.get_cached_token_filename() user_path = user_tc.get_cached_token_filename() self.assertNotEqual(tc_path, user_path) self.assertNotIn(u'frøbel', tc_path) self.assertIn(u'frøbel', user_path)
class TokenCacheTest(unittest.TestCase): def setUp(self): import tempfile from flickrapi.tokencache import TokenCache # Use mkdtemp() for backward compatibility with Python 2.7 self.tc_path = tempfile.mkdtemp() self.tc = TokenCache('123-456', path=self.tc_path) def tearDown(self): tc_path = getattr(self, 'tc_path', None) if tc_path is not None: import shutil shutil.rmtree(self.tc_path) def test_get_set_del(self): self.assertIsNone(self.tc.token) # Check setting token, both in memory and on disk. self.tc.token = u'nümbér' self.assertEquals(self.tc.token, u'nümbér') on_disk = open(self.tc.get_cached_token_filename(), 'rb').read() self.assertEquals(on_disk.decode('utf8'), u'nümbér') # Erase from in-RAM cache and try again, to read from disk. self.tc.memory.clear() self.assertEquals(self.tc.token, u'nümbér') del self.tc.token self.assertIsNone(self.tc.token) self.tc.token = u'nümbér' self.tc.forget() self.assertIsNone(self.tc.token) def test_username(self): """Username should impact the location of the cache on disk.""" from flickrapi.tokencache import TokenCache user_tc = TokenCache(u'123-456', username=u'frøbel', path=self.tc_path) tc_path = self.tc.get_cached_token_filename() user_path = user_tc.get_cached_token_filename() self.assertNotEquals(tc_path, user_path) self.assertNotIn(u'frøbel', tc_path) self.assertIn(u'frøbel', user_path)
class FlickrAPI(object): """Encapsulates Flickr functionality. Example usage:: flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') """ flickr_host = "api.flickr.com" flickr_rest_form = "/services/rest/" flickr_auth_form = "/services/auth/" flickr_upload_form = "/services/upload/" flickr_replace_form = "/services/replace/" def __init__(self, api_key, secret=None, username=None, token=None, format='etree', store_token=True, cache=False): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance. format The response format. Use either "xmlnode" or "etree" to get a parsed response, or use any response format supported by Flickr to get an unparsed response from method calls. It's also possible to pass the ``format`` parameter on individual calls. store_token Disables the on-disk token cache if set to False (default is True). Use this to ensure that tokens aren't read nor written to disk, for example in web applications that store tokens in cookies. cache Enables in-memory caching of FlickrAPI calls - set to ``True`` to use. If you don't want to use the default settings, you can instantiate a cache yourself too: >>> f = FlickrAPI(api_key='123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) """ self.api_key = api_key self.secret = secret self.default_format = format self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = SimpleTokenCache() else: # Use a real token cache self.token_cache = TokenCache(api_key, username) if cache: self.cache = SimpleCache() else: self.cache = None def __repr__(self): '''Returns a string representation of this object.''' return '[FlickrAPI for key "%s"]' % self.api_key __str__ = __repr__ def trait_names(self): '''Returns a list of method names as supported by the Flickr API. Used for tab completion in IPython. ''' try: rsp = self.reflection_getMethods(format='etree') except FlickrError: return None def tr(name): '''Translates Flickr names to something that can be called here. >>> tr(u'flickr.photos.getInfo') u'photos_getInfo' ''' return name[7:].replace('.', '_') return [tr(m.text) for m in rsp.getiterator('method')] @rest_parser('xmlnode') def parse_xmlnode(self, rest_xml): '''Parses a REST XML response from Flickr into an XMLNode object.''' rsp = XMLNode.parse(rest_xml, store_xml=True) if rsp['stat'] == 'ok': return rsp err = rsp.err[0] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) @rest_parser('etree') def parse_etree(self, rest_xml): '''Parses a REST XML response from Flickr into an ElementTree object.''' try: import xml.etree.ElementTree as ElementTree except ImportError: # For Python 2.4 compatibility: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError("You need to install " "ElementTree for using the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok': return rsp err = rsp.find('err') raise FlickrError(u'Error: %(code)s: %(msg)s' % err.attrib) def sign(self, dictionary): """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", "key": u"value".encode('utf-8')}`` """ data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException("No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def __getattr__(self, attrib): """Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") etree = flickr.photos_getInfo(photo_id='1234') etree = flickr.photos_getInfo(photo_id='1234', format='etree') xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode') json = flickr.photos_getInfo(photo_id='1234', format='json') """ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith('_'): raise AttributeError("No such attribute '%s'" % attrib) # Construct the method name and see if it's cached method = "flickr." + attrib.replace("_", ".") if method in self.__handler_cache: return self.__handler_cache[method] def handler(**args): '''Dynamically created handler for a Flickr API call''' if self.token_cache.token and not self.secret: raise ValueError("Auth tokens cannot be used without " "API secret") # Set some defaults defaults = {'method': method, 'auth_token': self.token_cache.token, 'api_key': self.api_key, 'format': self.default_format} args = self.__supply_defaults(args, defaults) return self.__wrap_in_parser(self.__flickr_call, parse_format=args['format'], **args) handler.method = method self.__handler_cache[method] = handler return handler def __supply_defaults(self, args, defaults): '''Returns a new dictionary containing ``args``, augmented with defaults from ``defaults``. Defaults can be overridden, or completely removed by setting the appropriate value in ``args`` to ``None``. >>> f = FlickrAPI('123') >>> f._FlickrAPI__supply_defaults( ... {'foo': 'bar', 'baz': None, 'token': None}, ... {'baz': 'foobar', 'room': 'door'}) {'foo': 'bar', 'room': 'door'} ''' result = args.copy() for key, default_value in defaults.iteritems(): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in result.copy().iteritems(): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if result[key] is None: del result[key] return result def __flickr_call(self, **kwargs): '''Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self.__flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') ''' LOG.debug("Calling %s" % kwargs) post_data = self.encode_and_sign(kwargs) # Return value from cache if available if self.cache and self.cache.get(post_data): return self.cache.get(post_data) url = "http://" + self.flickr_host + self.flickr_rest_form flicksocket = urllib2.urlopen(url, post_data) reply = flicksocket.read() flicksocket.close() # Store in cache, if we have one if self.cache is not None: self.cache.set(post_data, reply) return reply def __wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs): '''Wraps a method call in a parser. The parser will be looked up by the ``parse_format`` specifier. If there is a parser and ``kwargs['format']`` is set, it's set to ``rest``, and the response of the method is parsed before it's returned. ''' # Find the parser, and set the format to rest if we're supposed to # parse it. if parse_format in rest_parsers and 'format' in kwargs: kwargs['format'] = 'rest' LOG.debug('Wrapping call %s(self, %s, %s)' % (wrapped_method, args, kwargs)) data = wrapped_method(*args, **kwargs) # Just return if we have no parser if parse_format not in rest_parsers: return data # Return the parsed data parser = rest_parsers[parse_format] return parser(self, data) def auth_url(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() """ encoded = self.encode_and_sign({ "api_key": self.api_key, "frob": frob, "perms": perms}) return "http://%s%s?%s" % (self.flickr_host, \ self.flickr_auth_form, encoded) def web_login_url(self, perms): '''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' encoded = self.encode_and_sign({ "api_key": self.api_key, "perms": perms}) return "http://%s%s?%s" % (self.flickr_host, \ self.flickr_auth_form, encoded) def __extract_upload_response_format(self, kwargs): '''Returns the response format given in kwargs['format'], or the default format if there is no such key. If kwargs contains 'format', it is removed from kwargs. If the format isn't compatible with Flickr's upload response type, a FlickrError exception is raised. ''' # Figure out the response format format = kwargs.get('format', self.default_format) if format not in rest_parsers and format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return format def upload(self, filename, callback=None, **kwargs): """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: filename name of a file to upload callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private content_type Set to "1" for Photo, "2" for Screenshot, or "3" for Other. hidden Set to "1" to keep the photo in global search results, "2" to hide from public searches. format The response format. You can only choose between the parsed responses or 'rest' for plain REST. The callback method should take two parameters: ``def callback(progress, done)`` Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. """ return self.__upload_to_form(self.flickr_upload_form, filename, callback, **kwargs) def replace(self, filename, photo_id, callback=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload photo_id the ID of the photo to replace callback method that gets progress reports format The response format. You can only choose between the parsed responses or 'rest' for plain REST. Defaults to the format passed to the constructor. The callback parameter has the same semantics as described in the ``upload`` function. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self.__upload_to_form(self.flickr_replace_form, filename, callback, **kwargs) def __upload_to_form(self, form_url, filename, callback, **kwargs): '''Uploads a photo - can be used to either upload a new photo or replace an existing one. form_url must be either ``FlickrAPI.flickr_replace_form`` or ``FlickrAPI.flickr_upload_form``. ''' if not filename: raise IllegalArgumentException("filename must be specified") if not self.token_cache.token: raise IllegalArgumentException("Authentication is required") # Figure out the response format format = self.__extract_upload_response_format(kwargs) # Update the arguments with the ones the user won't have to supply arguments = {'auth_token': self.token_cache.token, 'api_key': self.api_key} arguments.update(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_utf8(arguments) if self.secret: kwargs["api_sig"] = self.sign(kwargs) url = "http://%s%s" % (self.flickr_host, form_url) # construct POST data body = Multipart() for arg, value in kwargs.iteritems(): part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__wrap_in_parser(self.__send_multipart, format, url, body, callback) def __send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting unparsed XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if not progress_callback: # Just use urllib2 if there is no progress callback # function response = urllib2.urlopen(request) return response.read() def __upload_callback(percentage, done, seen_header=[False]): '''Filters out the progress report on the HTTP header''' # Call the user's progress callback when we've filtered # out the HTTP header if seen_header[0]: return progress_callback(percentage, done) # Remember the first time we hit 'done'. if done: seen_header[0] = True response = reportinghttp.urlopen(request, __upload_callback) return response.read() def validate_frob(self, frob, perms): '''Lets the user validate the frob by launching a browser to the Flickr website. ''' auth_url = self.auth_url(perms, frob) try: browser = webbrowser.get() except webbrowser.Error: if 'BROWSER' not in os.environ: raise browser = webbrowser.GenericBrowser(os.environ['BROWSER']) browser.open(auth_url, True, True) def get_token_part_one(self, perms="read", auth_callback=None): """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 token is present and valid, it is returned by the method. If that fails (or if the token is no longer valid based on flickr.auth.checkToken) a new frob is acquired. If an auth_callback method has been specified it will be called. Otherwise the frob is validated by having the user log into flickr (with a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" auth_callback method to be called if authorization is needed. When not passed, ``self.validate_frob(...)`` is called. You can call this method yourself from the callback method too. If authorization should be blocked, pass ``auth_callback=False``. The auth_callback method should take ``(frob, perms)`` as parameters. An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) Also take a look at ``authenticate_console(perms)``. """ # Check our auth_callback parameter for correctness before we # do anything authenticate = self.validate_frob if auth_callback is not None: if hasattr(auth_callback, '__call__'): # use the provided callback function authenticate = auth_callback elif auth_callback is False: authenticate = None else: # Any non-callable non-False value is invalid raise ValueError('Invalid value for auth_callback: %s' % auth_callback) # see if we have a saved token token = self.token_cache.token frob = None auth_url = None nsid = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(auth_token=token, format='xmlnode') # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None if token: nsid = rsp.auth[0].user[0]['nsid'] except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None # get a new token if we need one if not token: # If we can't authenticate, it's all over. if not authenticate: raise FlickrError('Authentication required but ' 'blocked using auth_callback=False') # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(auth_token=None, format='xmlnode') frob = rsp.frob[0].text #authenticate(frob, perms) auth_url = self.auth_url(perms, frob) return (token, frob, nsid, auth_url) def get_token_part_two(self, (token, frob)): """Part two of getting a token, see ``get_token_part_one(...)`` for details.""" # If a valid token was obtained in the past, we're done if token: LOG.debug("get_token_part_two: no need, token already there") self.token_cache.token = token return token LOG.debug("get_token_part_two: getting a new token for frob '%s'" % frob) return self.get_token(frob)
class FlickrAPI(object): """Encapsulates Flickr functionality. Example usage:: flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') """ flickr_host = "api.flickr.com" flickr_rest_form = "/services/rest/" flickr_auth_form = "/services/auth/" flickr_upload_form = "/services/upload/" flickr_replace_form = "/services/replace/" def __init__(self, api_key, secret=None, username=None, token=None, format='etree', store_token=True, cache=False): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance. format The response format. Use either "xmlnode" or "etree" to get a parsed response, or use any response format supported by Flickr to get an unparsed response from method calls. It's also possible to pass the ``format`` parameter on individual calls. store_token Disables the on-disk token cache if set to False (default is True). Use this to ensure that tokens aren't read nor written to disk, for example in web applications that store tokens in cookies. cache Enables in-memory caching of FlickrAPI calls - set to ``True`` to use. If you don't want to use the default settings, you can instantiate a cache yourself too: >>> f = FlickrAPI(api_key='123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) """ self.api_key = api_key self.secret = secret self.default_format = format self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = SimpleTokenCache() else: # Use a real token cache self.token_cache = TokenCache(api_key, username) if cache: self.cache = SimpleCache() else: self.cache = None def __repr__(self): '''Returns a string representation of this object.''' return '[FlickrAPI for key "%s"]' % self.api_key __str__ = __repr__ def trait_names(self): '''Returns a list of method names as supported by the Flickr API. Used for tab completion in IPython. ''' try: rsp = self.reflection_getMethods(format='etree') except FlickrError: return None def tr(name): '''Translates Flickr names to something that can be called here. >>> tr(u'flickr.photos.getInfo') u'photos_getInfo' ''' return name[7:].replace('.', '_') return [tr(m.text) for m in rsp.getiterator('method')] @rest_parser('xmlnode') def parse_xmlnode(self, rest_xml): '''Parses a REST XML response from Flickr into an XMLNode object.''' rsp = XMLNode.parse(rest_xml, store_xml=True) if rsp['stat'] == 'ok': return rsp err = rsp.err[0] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) @rest_parser('etree') def parse_etree(self, rest_xml): '''Parses a REST XML response from Flickr into an ElementTree object.''' try: import xml.etree.ElementTree as ElementTree except ImportError: # For Python 2.4 compatibility: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError("You need to install " "ElementTree for using the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok': return rsp err = rsp.find('err') raise FlickrError(u'Error: %(code)s: %(msg)s' % err.attrib) @rest_parser('json') def parse_json(self, json_string): '''Parses a REST JSON response from Flickr into an dict object.''' try: import json as json except ImportError: raise ImportError("You need to be able to import" "json for using the json format") json_string = json_string[14:-1] rsp = json.loads(json_string) if rsp['stat'] == 'ok': return rsp err = rsp['err'] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) def sign(self, dictionary): """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", "key": u"value".encode('utf-8')}`` """ data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException( "No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def __getattr__(self, attrib): """Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") etree = flickr.photos_getInfo(photo_id='1234') etree = flickr.photos_getInfo(photo_id='1234', format='etree') xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode') json = flickr.photos_getInfo(photo_id='1234', format='json') """ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith('_'): raise AttributeError("No such attribute '%s'" % attrib) # Construct the method name and see if it's cached method = "flickr." + attrib.replace("_", ".") if method in self.__handler_cache: return self.__handler_cache[method] def handler(**args): '''Dynamically created handler for a Flickr API call''' if self.token_cache.token and not self.secret: raise ValueError("Auth tokens cannot be used without " "API secret") # Set some defaults defaults = { 'method': method, 'auth_token': self.token_cache.token, 'api_key': self.api_key, 'format': self.default_format } args = self.__supply_defaults(args, defaults) return self.__wrap_in_parser(self.__flickr_call, parse_format=args['format'], **args) handler.method = method self.__handler_cache[method] = handler return handler def __supply_defaults(self, args, defaults): '''Returns a new dictionary containing ``args``, augmented with defaults from ``defaults``. Defaults can be overridden, or completely removed by setting the appropriate value in ``args`` to ``None``. >>> f = FlickrAPI('123') >>> f._FlickrAPI__supply_defaults( ... {'foo': 'bar', 'baz': None, 'token': None}, ... {'baz': 'foobar', 'room': 'door'}) {'foo': 'bar', 'room': 'door'} ''' result = args.copy() for key, default_value in defaults.iteritems(): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in result.copy().iteritems(): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if result[key] is None: del result[key] return result def __flickr_call(self, **kwargs): '''Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self.__flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') ''' LOG.debug("Calling %s" % kwargs) post_data = self.encode_and_sign(kwargs) # Return value from cache if available if self.cache and self.cache.get(post_data): return self.cache.get(post_data) url = "https://" + self.flickr_host + self.flickr_rest_form flicksocket = urllib2.urlopen(url, post_data) reply = flicksocket.read() flicksocket.close() # Store in cache, if we have one if self.cache is not None: self.cache.set(post_data, reply) return reply def __wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs): '''Wraps a method call in a parser. The parser will be looked up by the ``parse_format`` specifier. If there is a parser and ``kwargs['format']`` is set, it's set to ``rest``, and the response of the method is parsed before it's returned. ''' # Find the parser, and set the format to rest if we're supposed to # parse it. if parse_format in rest_parsers and 'format' in kwargs: kwargs['format'] = 'rest' if parse_format == "json": kwargs['format'] = 'json' LOG.debug('Wrapping call %s(self, %s, %s)' % (wrapped_method, args, kwargs)) data = wrapped_method(*args, **kwargs) # Just return if we have no parser if parse_format not in rest_parsers: return data # Return the parsed data parser = rest_parsers[parse_format] return parser(self, data) def auth_url(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() """ encoded = self.encode_and_sign({ "api_key": self.api_key, "frob": frob, "perms": perms }) return "http://%s%s?%s" % (self.flickr_host, \ self.flickr_auth_form, encoded) def web_login_url(self, perms): '''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' encoded = self.encode_and_sign({ "api_key": self.api_key, "perms": perms }) return "http://%s%s?%s" % (self.flickr_host, \ self.flickr_auth_form, encoded) def __extract_upload_response_format(self, kwargs): '''Returns the response format given in kwargs['format'], or the default format if there is no such key. If kwargs contains 'format', it is removed from kwargs. If the format isn't compatible with Flickr's upload response type, a FlickrError exception is raised. ''' # Figure out the response format format = kwargs.get('format', self.default_format) if format not in rest_parsers and format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return format def upload(self, filename, callback=None, **kwargs): """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: filename name of a file to upload callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private content_type Set to "1" for Photo, "2" for Screenshot, or "3" for Other. hidden Set to "1" to keep the photo in global search results, "2" to hide from public searches. format The response format. You can only choose between the parsed responses or 'rest' for plain REST. The callback method should take two parameters: ``def callback(progress, done)`` Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. """ return self.__upload_to_form(self.flickr_upload_form, filename, callback, **kwargs) def replace(self, filename, photo_id, callback=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload photo_id the ID of the photo to replace callback method that gets progress reports format The response format. You can only choose between the parsed responses or 'rest' for plain REST. Defaults to the format passed to the constructor. The callback parameter has the same semantics as described in the ``upload`` function. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self.__upload_to_form(self.flickr_replace_form, filename, callback, **kwargs) def __upload_to_form(self, form_url, filename, callback, **kwargs): '''Uploads a photo - can be used to either upload a new photo or replace an existing one. form_url must be either ``FlickrAPI.flickr_replace_form`` or ``FlickrAPI.flickr_upload_form``. ''' if not filename: raise IllegalArgumentException("filename must be specified") if not self.token_cache.token: raise IllegalArgumentException("Authentication is required") # Figure out the response format format = self.__extract_upload_response_format(kwargs) # Update the arguments with the ones the user won't have to supply arguments = { 'auth_token': self.token_cache.token, 'api_key': self.api_key } arguments.update(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_utf8(arguments) if self.secret: kwargs["api_sig"] = self.sign(kwargs) url = "http://%s%s" % (self.flickr_host, form_url) # construct POST data body = Multipart() for arg, value in kwargs.iteritems(): part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__wrap_in_parser(self.__send_multipart, format, url, body, callback) def __send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting unparsed XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if not progress_callback: # Just use urllib2 if there is no progress callback # function response = urllib2.urlopen(request) return response.read() def __upload_callback(percentage, done, seen_header=[False]): '''Filters out the progress report on the HTTP header''' # Call the user's progress callback when we've filtered # out the HTTP header if seen_header[0]: return progress_callback(percentage, done) # Remember the first time we hit 'done'. if done: seen_header[0] = True response = reportinghttp.urlopen(request, __upload_callback) return response.read() def validate_frob(self, frob, perms): '''Lets the user validate the frob by launching a browser to the Flickr website. ''' auth_url = self.auth_url(perms, frob) try: browser = webbrowser.get() except webbrowser.Error: if 'BROWSER' not in os.environ: raise browser = webbrowser.GenericBrowser(os.environ['BROWSER']) browser.open(auth_url, True, True) def get_token_part_one(self, perms="read", auth_callback=None): """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 token is present and valid, it is returned by the method. If that fails (or if the token is no longer valid based on flickr.auth.checkToken) a new frob is acquired. If an auth_callback method has been specified it will be called. Otherwise the frob is validated by having the user log into flickr (with a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" auth_callback method to be called if authorization is needed. When not passed, ``self.validate_frob(...)`` is called. You can call this method yourself from the callback method too. If authorization should be blocked, pass ``auth_callback=False``. The auth_callback method should take ``(frob, perms)`` as parameters. An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) Also take a look at ``authenticate_console(perms)``. """ # Check our auth_callback parameter for correctness before we # do anything authenticate = self.validate_frob if auth_callback is not None: if hasattr(auth_callback, '__call__'): # use the provided callback function authenticate = auth_callback elif auth_callback is False: authenticate = None else: # Any non-callable non-False value is invalid raise ValueError('Invalid value for auth_callback: %s' % auth_callback) # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(auth_token=token, format='xmlnode') # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None # get a new token if we need one if not token: # If we can't authenticate, it's all over. if not authenticate: raise FlickrError('Authentication required but ' 'blocked using auth_callback=False') # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(auth_token=None, format='xmlnode') frob = rsp.frob[0].text authenticate(frob, perms) return (token, frob) def get_token_part_two(self, (token, frob)): """Part two of getting a token, see ``get_token_part_one(...)`` for details.""" # If a valid token was obtained in the past, we're done if token: LOG.debug("get_token_part_two: no need, token already there") self.token_cache.token = token return token LOG.debug("get_token_part_two: getting a new token for frob '%s'" % frob) return self.get_token(frob)
class FlickrAPI: """Encapsulates Flickr functionality. Example usage:: flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') """ flickr_host = "api.flickr.com" flickr_rest_form = "/services/rest/" flickr_auth_form = "/services/auth/" flickr_upload_form = "/services/upload/" flickr_replace_form = "/services/replace/" def __init__(self, api_key, secret=None, fail_on_error=True, username=None, token=None): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr secret The secret belonging to the API key fail_on_error If False, errors won't be checked by the FlickrAPI module. True by default, of course. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance """ self.api_key = api_key self.secret = secret self.fail_on_error = fail_on_error self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token else: # Use a real token cache self.token_cache = TokenCache(api_key, username) def __repr__(self): '''Returns a string representation of this object.''' return '[FlickrAPI for key "%s"]' % self.api_key __str__ = __repr__ def sign(self, dictionary): """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", "key": u"value".encode('utf-8')}`` """ def sorted(list): list = list[:] list.sort() return list data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException("No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5.new() md5_hash.update(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def __getattr__(self, attrib): """Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") xmlnode = flickr.photos_getInfo(photo_id='1234') json = flickr.photos_getInfo(photo_id='1234', format='json') """ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith('__'): raise AttributeError("No such attribute '%s'" % attrib) # Construct the method name and see if it's cached method = "flickr." + attrib.replace("_", ".") if method in self.__handler_cache: return self.__handler_cache[method] url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_rest_form def handler(**args): '''Dynamically created handler for a Flickr API call''' explicit_format = 'format' in args if self.token_cache.token and not self.secret: raise ValueError("Auth tokens cannot be used without " "API secret") # Set some defaults defaults = {'method': method, 'auth_token': self.token_cache.token, 'api_key': self.api_key, 'format': 'rest'} for key, default_value in defaults.iteritems(): if key not in args: args[key] = default_value # You are able to remove a default by assigning None if key in args and args[key] is None: del args[key] LOG.debug("Calling %s(%s)" % (method, args)) post_data = self.encode_and_sign(args) flicksocket = urllib.urlopen(url, post_data) data = flicksocket.read() flicksocket.close() # Return the raw response when the user requested # a specific format. if explicit_format: return data result = XMLNode.parse(data, True) if self.fail_on_error: FlickrAPI.test_failure(result, True) return result handler.method = method self.__handler_cache[method] = handler return handler def auth_url(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() """ encoded = self.encode_and_sign({ "api_key": self.api_key, "frob": frob, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) def web_login_url(self, perms): '''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' encoded = self.encode_and_sign({ "api_key": self.api_key, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) def upload(self, filename, callback=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: filename name of a file to upload callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private The callback method should take two parameters: def callback(progress, done) Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. For now, the callback gets a 'done' twice, once for the HTTP headers, once for the body. """ if not filename: raise IllegalArgumentException("filename must be specified") # verify key names required_params = ('api_key', 'auth_token', 'api_sig') optional_params = ('title', 'description', 'tags', 'is_public', 'is_friend', 'is_family') possible_args = required_params + optional_params for a in arg.keys(): if a not in possible_args: raise IllegalArgumentException("Unknown parameter " "'%s' sent to FlickrAPI.upload" % a) arguments = {'auth_token': self.token_cache.token, 'api_key': self.api_key} arguments.update(arg) # Convert to UTF-8 if an argument is an Unicode string arg = make_utf8(arguments) if self.secret: arg["api_sig"] = self.sign(arg) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_upload_form # construct POST data body = Multipart() for a in required_params + optional_params: if a not in arg: continue part = Part({'name': a}, arg[a]) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__send_multipart(url, body, callback) def replace(self, filename, photo_id): """Replace an existing photo. Supported parameters: filename name of a file to upload photo_id the ID of the photo to replace """ if not filename: raise IllegalArgumentException("filename must be specified") if not photo_id: raise IllegalArgumentException("photo_id must be specified") args = {'filename': filename, 'photo_id': photo_id, 'auth_token': self.token_cache.token, 'api_key': self.api_key} args = make_utf8(args) if self.secret: args["api_sig"] = self.sign(args) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_replace_form # construct POST data body = Multipart() for arg, value in args.iteritems(): # No part for the filename if value == 'filename': continue part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__send_multipart(url, body) def __send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if progress_callback: response = reportinghttp.urlopen(request, progress_callback) else: response = urllib2.urlopen(request) rspXML = response.read() result = XMLNode.parse(rspXML) if self.fail_on_error: FlickrAPI.test_failure(result, True) return result #@classmethod def test_failure(cls, rsp, exception_on_error=True): """Exit app if the rsp XMLNode indicates failure.""" if rsp['stat'] != "fail": return message = cls.get_printable_error(rsp) LOG.error(message) if exception_on_error: raise FlickrError(message) #@classmethod def get_printable_error(cls, rsp): """Return a printed error message string.""" return "%s: error %s: %s" % (rsp.name, \ cls.get_rsp_error_code(rsp), cls.get_rsp_error_msg(rsp)) #@classmethod def get_rsp_error_code(cls, rsp): """Return the error code of a response, or 0 if no error.""" if rsp['stat'] == "fail": return int(rsp.err[0]['code']) return 0 #@classmethod def get_rsp_error_msg(cls, rsp): """Return the error message of a response, or "Success" if no error.""" if rsp['stat'] == "fail": return rsp.err[0]['msg'] return "Success" test_failure = classmethod(test_failure) get_printable_error = classmethod(get_printable_error) get_rsp_error_code = classmethod(get_rsp_error_code) get_rsp_error_msg = classmethod(get_rsp_error_msg) def validate_frob(self, frob, perms): '''Lets the user validate the frob by launching a browser to the Flickr website. ''' auth_url = self.auth_url(perms, frob) webbrowser.open(auth_url, True, True) def get_token_part_one(self, perms="read"): """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 token is present and valid, it is returned by the method. 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 a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) """ # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(auth_token=token) # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text self.username = rsp.auth[0].user[0]['username'] if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None # get a new token if we need one if not token: # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(auth_token=None) self.test_failure(rsp) frob = rsp.frob[0].text # validate online self.validate_frob(frob, perms) return (token, frob) def get_token_part_two(self, (token, frob)): """Part two of getting a token, see ``get_token_part_one(...)`` for details.""" # If a valid token was obtained in the past, we're done if token: LOG.debug("get_token_part_two: no need, token already there") self.token_cache.token = token return token LOG.debug("get_token_part_two: getting a new token for frob '%s'" % frob) return self.get_token(frob)
class FlickrAPI: """Encapsulated flickr functionality. Example usage: flickr = FlickrAPI(flickrAPIKey, flickrSecret) rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token) """ flickrHost = "api.flickr.com" flickrRESTForm = "/services/rest/" flickrAuthForm = "/services/auth/" flickrUploadForm = "/services/upload/" flickrReplaceForm = "/services/replace/" #------------------------------------------------------------------- def __init__(self, apiKey, secret=None, fail_on_error=True): """Construct a new FlickrAPI instance for a given API key and secret.""" self.apiKey = apiKey self.secret = secret self.token_cache = TokenCache(apiKey) self.token = self.token_cache.token self.fail_on_error = fail_on_error self.__handlerCache={} def __repr__(self): return '[FlickrAPI for key "%s"]' % self.apiKey __str__ = __repr__ #------------------------------------------------------------------- def sign(self, dictionary): """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", "key": u"value".encode('utf-8')} """ data = [self.secret] keys = dictionary.keys() keys.sort() for key in keys: data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException("No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5.new() md5_hash.update(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = self.make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def make_utf8(self, dictionary): '''Encodes all Unicode strings in the dictionary to UTF-8. Converts all other objects to regular strings. Returns a copy of the dictionary, doesn't touch the original. ''' result = {} for (key, value) in dictionary.iteritems(): if isinstance(value, unicode): value = value.encode('utf-8') else: value = str(value) result[key] = value return result #------------------------------------------------------------------- def __getattr__(self, method): """Handle all the regular Flickr API calls. >>> flickr.auth_getFrob(apiKey="AAAAAA") >>> xmlnode = flickr.photos_getInfo(photo_id='1234') >>> json = flickr.photos_getInfo(photo_id='1234', format='json') """ # Refuse to act as a proxy for unimplemented special methods if method.startswith('__'): raise AttributeError("No such attribute '%s'" % method) if self.__handlerCache.has_key(method): # If we already have the handler, return it return self.__handlerCache.has_key(method) # Construct the method name and URL method = "flickr." + method.replace("_", ".") url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrRESTForm def handler(**args): '''Dynamically created handler for a Flickr API call''' # Set some defaults defaults = {'method': method, 'auth_token': self.token, 'api_key': self.apiKey, 'format': 'rest'} for key, default_value in defaults.iteritems(): if key not in args: args[key] = default_value # You are able to remove a default by assigning None if key in args and args[key] is None: del args[key] LOG.debug("Calling %s(%s)" % (method, args)) postData = self.encode_and_sign(args) f = urllib.urlopen(url, postData) data = f.read() f.close() # Return the raw response when a non-REST format # was chosen. if args['format'] != 'rest': return data result = XMLNode.parseXML(data, True) if self.fail_on_error: FlickrAPI.testFailure(result, True) return result 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() """ encoded = self.encode_and_sign({ "api_key": self.apiKey, "frob": frob, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickrHost, \ FlickrAPI.flickrAuthForm, encoded) def upload(self, filename, callback=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: filename -- name of a file to upload callback -- method that gets progress reports title description tags -- space-delimited list of tags, '''tag1 tag2 "long tag"''' is_public -- "1" or "0" is_friend -- "1" or "0" is_family -- "1" or "0" The callback method should take two parameters: def callback(progress, done) Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. For now, the callback gets a 'done' twice, once for the HTTP headers, once for the body. """ if not filename: raise IllegalArgumentException("filename must be specified") # verify key names required_params = ('api_key', 'auth_token', 'api_sig') optional_params = ('title', 'description', 'tags', 'is_public', 'is_friend', 'is_family') possible_args = required_params + optional_params for a in arg.keys(): if a not in possible_args: raise IllegalArgumentException("Unknown parameter '%s' sent to FlickrAPI.upload" % a) arguments = {'auth_token': self.token, 'api_key': self.apiKey} arguments.update(arg) # Convert to UTF-8 if an argument is an Unicode string arg = self.make_utf8(arguments) if self.secret: arg["api_sig"] = self.sign(arg) url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm # construct POST data body = Multipart() for a in required_params + optional_params: if a not in arg: continue part = Part({'name': a}, arg[a]) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.send_multipart(url, body, callback) def replace(self, filename, photo_id): """Replace an existing photo. Supported parameters: filename -- name of a file to upload photo_id -- the ID of the photo to replace """ if not filename: raise IllegalArgumentException("filename must be specified") if not photo_id: raise IllegalArgumentException("photo_id must be specified") args = {'filename': filename, 'photo_id': photo_id, 'auth_token': self.token, 'api_key': self.apiKey} args = self.make_utf8(args) if self.secret: args["api_sig"] = self.sign(args) url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrReplaceForm # construct POST data body = Multipart() for arg, value in args.iteritems(): # No part for the filename if value == 'filename': continue part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.send_multipart(url, body) def send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if progress_callback: response = reportinghttp.urlopen(request, progress_callback) else: response = urllib2.urlopen(request) rspXML = response.read() result = XMLNode.parseXML(rspXML) if self.fail_on_error: FlickrAPI.testFailure(result, True) return result #----------------------------------------------------------------------- @classmethod def testFailure(cls, rsp, exception_on_error=True): """Exit app if the rsp XMLNode indicates failure.""" if rsp['stat'] != "fail": return message = cls.getPrintableError(rsp) LOG.error(message) if exception_on_error: raise FlickrError(message) #----------------------------------------------------------------------- @classmethod def getPrintableError(cls, rsp): """Return a printed error message string.""" return "%s: error %s: %s" % (rsp.elementName, \ cls.getRspErrorCode(rsp), cls.getRspErrorMsg(rsp)) #----------------------------------------------------------------------- @classmethod def getRspErrorCode(cls, rsp): """Return the error code of a response, or 0 if no error.""" if rsp['stat'] == "fail": return rsp.err[0]['code'] return 0 #----------------------------------------------------------------------- @classmethod def getRspErrorMsg(cls, rsp): """Return the error message of a response, or "Success" if no error.""" if rsp['stat'] == "fail": return rsp.err[0]['msg'] return "Success" #----------------------------------------------------------------------- def validateFrob(self, frob, perms): auth_url = self.__getAuthURL(perms, frob) webbrowser.open(auth_url, True, True) #----------------------------------------------------------------------- def getTokenPartOne(self, perms="read"): """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 token is present and valid, it is returned by the method. 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 a browser). If the browser needs to take over the terminal, use fork=False, otherwise use fork=True. To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.getTokenPartTwo(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms--"read", "write", or "delete" An example: (token, frob) = flickr.getTokenPartOne(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.getTokenPartTwo((token, frob)) """ # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(api_key=self.apiKey, auth_token=token) # 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 except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None self.token = None # get a new token if we need one if not token: # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(api_key=self.apiKey, auth_token=None) self.testFailure(rsp) frob = rsp.frob[0].elementText # validate online self.validateFrob(frob, perms) return (token, frob) def getTokenPartTwo(self, (token, frob)): """Part two of getting a token, see getTokenPartOne(...) for details.""" # If a valid token was obtained, we're done if token: LOG.debug("getTokenPartTwo: no need, token already there") self.token = token return token LOG.debug("getTokenPartTwo: getting a new token for frob '%s'" % frob) # get a token rsp = self.auth_getToken(api_key=self.apiKey, frob=frob) self.testFailure(rsp) token = rsp.auth[0].token[0].elementText LOG.debug("getTokenPartTwo: new token '%s'" % token) # store the auth info for next time self.token_cache.token = rsp.xml self.token = token return token
class FlickrAPI: """Encapsulates Flickr functionality. Example usage:: flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') """ flickr_host = "api.flickr.com" flickr_rest_form = "/services/rest/" flickr_auth_form = "/services/auth/" flickr_upload_form = "/services/upload/" flickr_replace_form = "/services/replace/" def __init__(self, api_key, secret=None, fail_on_error=None, username=None, token=None, format='xmlnode', store_token=True, cache=False): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. fail_on_error If False, errors won't be checked by the FlickrAPI module. Deprecated, don't use this parameter, just handle the FlickrError exceptions. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance. format The response format. Use either "xmlnode" or "etree" to get a parsed response, or use any response format supported by Flickr to get an unparsed response from method calls. It's also possible to pass the ``format`` parameter on individual calls. store_token Disables the on-disk token cache if set to False (default is True). Use this to ensure that tokens aren't read nor written to disk, for example in web applications that store tokens in cookies. cache Enables in-memory caching of FlickrAPI calls - set to ``True`` to use. If you don't want to use the default settings, you can instantiate a cache yourself too: >>> f = FlickrAPI(api_key='123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) """ if fail_on_error is not None: LOG.warn("fail_on_error has been deprecated. Remove this " "parameter and just handle the FlickrError exceptions.") else: fail_on_error = True self.api_key = api_key self.secret = secret self.fail_on_error = fail_on_error self.default_format = format self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = SimpleTokenCache() else: # Use a real token cache self.token_cache = TokenCache(api_key, username) if cache: self.cache = SimpleCache() else: self.cache = None def __repr__(self): '''Returns a string representation of this object.''' return '[FlickrAPI for key "%s"]' % self.api_key __str__ = __repr__ def trait_names(self): '''Returns a list of method names as supported by the Flickr API. Used for tab completion in IPython. ''' rsp = self.reflection_getMethods(format='etree') def tr(name): '''Translates Flickr names to something that can be called here. >>> tr(u'flickr.photos.getInfo') u'photos_getInfo' ''' return name[7:].replace('.', '_') return [tr(m.text) for m in rsp.getiterator('method')] @rest_parser('xmlnode') def parse_xmlnode(self, rest_xml): '''Parses a REST XML response from Flickr into an XMLNode object.''' rsp = XMLNode.parse(rest_xml, store_xml=True) if rsp['stat'] == 'ok' or not self.fail_on_error: return rsp err = rsp.err[0] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) @rest_parser('etree') def parse_etree(self, rest_xml): '''Parses a REST XML response from Flickr into an ElementTree object.''' # Only import it here, to maintain Python 2.4 compatibility import xml.etree.ElementTree rsp = xml.etree.ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok' or not self.fail_on_error: return rsp err = rsp.find('err') raise FlickrError(u'Error: %s: %s' % ( err.attrib['code'], err.attrib['msg'])) def sign(self, dictionary): """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", "key": u"value".encode('utf-8')}`` """ data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException("No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5.new() md5_hash.update(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def __getattr__(self, attrib): """Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") xmlnode = flickr.photos_getInfo(photo_id='1234') xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode') json = flickr.photos_getInfo(photo_id='1234', format='json') etree = flickr.photos_getInfo(photo_id='1234', format='etree') """ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith('_'): raise AttributeError("No such attribute '%s'" % attrib) # Construct the method name and see if it's cached method = "flickr." + attrib.replace("_", ".") if method in self.__handler_cache: return self.__handler_cache[method] def handler(**args): '''Dynamically created handler for a Flickr API call''' if self.token_cache.token and not self.secret: raise ValueError("Auth tokens cannot be used without " "API secret") # Set some defaults defaults = {'method': method, 'auth_token': self.token_cache.token, 'api_key': self.api_key, 'format': self.default_format} args = self.__supply_defaults(args, defaults) return self.__wrap_in_parser(self.__flickr_call, parse_format=args['format'], **args) handler.method = method self.__handler_cache[method] = handler return handler def __supply_defaults(self, args, defaults): '''Returns a new dictionary containing ``args``, augmented with defaults from ``defaults``. Defaults can be overridden, or completely removed by setting the appropriate value in ``args`` to ``None``. >>> f = FlickrAPI('123') >>> f._FlickrAPI__supply_defaults( ... {'foo': 'bar', 'baz': None, 'token': None}, ... {'baz': 'foobar', 'room': 'door'}) {'foo': 'bar', 'room': 'door'} ''' result = args.copy() for key, default_value in defaults.iteritems(): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in result.copy().iteritems(): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if result[key] is None: del result[key] return result def __flickr_call(self, **kwargs): '''Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self.__flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') ''' LOG.debug("Calling %s" % kwargs) post_data = self.encode_and_sign(kwargs) # Return value from cache if available if self.cache and self.cache.get(post_data): return self.cache.get(post_data) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_rest_form flicksocket = urllib.urlopen(url, post_data) reply = flicksocket.read() flicksocket.close() # Store in cache, if we have one if self.cache is not None: self.cache.set(post_data, reply) return reply def __wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs): '''Wraps a method call in a parser. The parser will be looked up by the ``parse_format`` specifier. If there is a parser and ``kwargs['format']`` is set, it's set to ``rest``, and the response of the method is parsed before it's returned. ''' # Find the parser, and set the format to rest if we're supposed to # parse it. if parse_format in rest_parsers and 'format' in kwargs: kwargs['format'] = 'rest' LOG.debug('Wrapping call %s(self, %s, %s)' % (wrapped_method, args, kwargs)) data = wrapped_method(*args, **kwargs) # Just return if we have no parser if parse_format not in rest_parsers: return data # Return the parsed data parser = rest_parsers[parse_format] return parser(self, data) def auth_url(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() """ encoded = self.encode_and_sign({ "api_key": self.api_key, "frob": frob, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) def web_login_url(self, perms): '''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' encoded = self.encode_and_sign({ "api_key": self.api_key, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) def upload(self, filename, callback=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: filename name of a file to upload callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private The callback method should take two parameters: def callback(progress, done) Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. For now, the callback gets a 'done' twice, once for the HTTP headers, once for the body. """ if not filename: raise IllegalArgumentException("filename must be specified") # verify key names required_params = ('api_key', 'auth_token', 'api_sig') optional_params = ('title', 'description', 'tags', 'is_public', 'is_friend', 'is_family') possible_args = required_params + optional_params for a in arg.keys(): if a not in possible_args: raise IllegalArgumentException("Unknown parameter " "'%s' sent to FlickrAPI.upload" % a) arguments = {'auth_token': self.token_cache.token, 'api_key': self.api_key} arguments.update(arg) # Convert to UTF-8 if an argument is an Unicode string arg = make_utf8(arguments) if self.secret: arg["api_sig"] = self.sign(arg) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_upload_form # construct POST data body = Multipart() for a in required_params + optional_params: if a not in arg: continue part = Part({'name': a}, arg[a]) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__send_multipart(url, body, callback) def replace(self, filename, photo_id): """Replace an existing photo. Supported parameters: filename name of a file to upload photo_id the ID of the photo to replace """ if not filename: raise IllegalArgumentException("filename must be specified") if not photo_id: raise IllegalArgumentException("photo_id must be specified") args = {'filename': filename, 'photo_id': photo_id, 'auth_token': self.token_cache.token, 'api_key': self.api_key} args = make_utf8(args) if self.secret: args["api_sig"] = self.sign(args) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_replace_form # construct POST data body = Multipart() for arg, value in args.iteritems(): # No part for the filename if value == 'filename': continue part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__send_multipart(url, body) def __send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if progress_callback: response = reportinghttp.urlopen(request, progress_callback) else: response = urllib2.urlopen(request) rspXML = response.read() return self.parse_xmlnode(rspXML) @classmethod def test_failure(cls, rsp, exception_on_error=True): """Exit app if the rsp XMLNode indicates failure.""" LOG.warn("FlickrAPI.test_failure has been deprecated and will be " "removed in FlickrAPI version 1.2.") if rsp['stat'] != "fail": return message = cls.get_printable_error(rsp) LOG.error(message) if exception_on_error: raise FlickrError(message) @classmethod def get_printable_error(cls, rsp): """Return a printed error message string of an XMLNode Flickr response.""" LOG.warn("FlickrAPI.get_printable_error has been deprecated " "and will be removed in FlickrAPI version 1.2.") return "%s: error %s: %s" % (rsp.name, \ cls.get_rsp_error_code(rsp), cls.get_rsp_error_msg(rsp)) @classmethod def get_rsp_error_code(cls, rsp): """Return the error code of an XMLNode Flickr response, or 0 if no error. """ LOG.warn("FlickrAPI.get_rsp_error_code has been deprecated and will be " "removed in FlickrAPI version 1.2.") if rsp['stat'] == "fail": return int(rsp.err[0]['code']) return 0 @classmethod def get_rsp_error_msg(cls, rsp): """Return the error message of an XMLNode Flickr response, or "Success" if no error. """ LOG.warn("FlickrAPI.get_rsp_error_msg has been deprecated and will be " "removed in FlickrAPI version 1.2.") if rsp['stat'] == "fail": return rsp.err[0]['msg'] return "Success" def validate_frob(self, frob, perms): '''Lets the user validate the frob by launching a browser to the Flickr website. ''' auth_url = self.auth_url(perms, frob) webbrowser.open(auth_url, True, True) def get_token_part_one(self, perms="read"): """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 token is present and valid, it is returned by the method. 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 a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) """ # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(auth_token=token, format='xmlnode') # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None # get a new token if we need one if not token: # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(auth_token=None, format='xmlnode') self.test_failure(rsp) frob = rsp.frob[0].text # validate online self.validate_frob(frob, perms) return (token, frob) def get_token_part_two(self, (token, frob)): """Part two of getting a token, see ``get_token_part_one(...)`` for details.""" # If a valid token was obtained in the past, we're done if token: LOG.debug("get_token_part_two: no need, token already there") self.token_cache.token = token return token LOG.debug("get_token_part_two: getting a new token for frob '%s'" % frob) return self.get_token(frob)
class FlickrAPI: u"""Encapsulates Flickr functionality. Example usage:: flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') """ #$NON-NLS-1$ flickr_host = u"api.flickr.com" #$NON-NLS-1$ flickr_rest_form = u"/services/rest/" #$NON-NLS-1$ flickr_auth_form = u"/services/auth/" #$NON-NLS-1$ flickr_upload_form = u"/services/upload/" #$NON-NLS-1$ flickr_replace_form = u"/services/replace/" #$NON-NLS-1$ def __init__(self, api_key, secret=None, fail_on_error=None, username=None, token=None, format=u'xmlnode', store_token=True, cache=False): #$NON-NLS-1$ u"""Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. fail_on_error If False, errors won't be checked by the FlickrAPI module. Deprecated, don't use this parameter, just handle the FlickrError exceptions. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance. format The response format. Use either "xmlnode" or "etree" to get a parsed response, or use any response format supported by Flickr to get an unparsed response from method calls. It's also possible to pass the ``format`` parameter on individual calls. store_token Disables the on-disk token cache if set to False (default is True). Use this to ensure that tokens aren't read nor written to disk, for example in web applications that store tokens in cookies. cache Enables in-memory caching of FlickrAPI calls - set to ``True`` to use. If you don't want to use the default settings, you can instantiate a cache yourself too: >>> f = FlickrAPI(api_key='123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) """ #$NON-NLS-1$ if fail_on_error is not None: LOG.warn(u"fail_on_error has been deprecated. Remove this " #$NON-NLS-1$ u"parameter and just handle the FlickrError exceptions.") #$NON-NLS-1$ else: fail_on_error = True self.api_key = api_key self.secret = secret self.fail_on_error = fail_on_error self.default_format = format self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = SimpleTokenCache() else: # Use a real token cache self.token_cache = TokenCache(api_key, username) if cache: self.cache = SimpleCache() else: self.cache = None def __repr__(self): u'''Returns a string representation of this object.''' #$NON-NLS-1$ return u'[FlickrAPI for key "%s"]' % self.api_key #$NON-NLS-1$ __str__ = __repr__ def trait_names(self): u'''Returns a list of method names as supported by the Flickr API. Used for tab completion in IPython. ''' #$NON-NLS-1$ rsp = self.reflection_getMethods(format=u'etree') #$NON-NLS-1$ def tr(name): u'''Translates Flickr names to something that can be called here. >>> tr(u'flickr.photos.getInfo') u'photos_getInfo' ''' #$NON-NLS-1$ return name[7:].replace(u'.', u'_') #$NON-NLS-2$ #$NON-NLS-1$ return [tr(m.text) for m in rsp.getiterator(u'method')] #$NON-NLS-1$ @rest_parser(u'xmlnode') #$NON-NLS-1$ def parse_xmlnode(self, rest_xml): u'''Parses a REST XML response from Flickr into an XMLNode object.''' #$NON-NLS-1$ rsp = XMLNode.parse(rest_xml, store_xml=True) if rsp[u'stat'] == u'ok' or not self.fail_on_error: #$NON-NLS-2$ #$NON-NLS-1$ return rsp err = rsp.err[0] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) #$NON-NLS-1$ @rest_parser(u'etree') #$NON-NLS-1$ def parse_etree(self, rest_xml): u'''Parses a REST XML response from Flickr into an ElementTree object.''' #$NON-NLS-1$ # Only import it here, to maintain Python 2.4 compatibility import xml.etree.ElementTree rsp = xml.etree.ElementTree.fromstring(rest_xml) if rsp.attrib[u'stat'] == u'ok' or not self.fail_on_error: #$NON-NLS-2$ #$NON-NLS-1$ return rsp err = rsp.find(u'err') #$NON-NLS-1$ raise FlickrError(u'Error: %s: %s' % ( #$NON-NLS-1$ err.attrib[u'code'], err.attrib[u'msg'])) #$NON-NLS-2$ #$NON-NLS-1$ def sign(self, dictionary): u"""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", "key": u"value".encode('utf-8')}`` """ #$NON-NLS-1$ data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException(u"No Unicode allowed, " #$NON-NLS-1$ u"argument %s (%r) should have been UTF-8 by now" #$NON-NLS-1$ % (key, datum)) data.append(datum) md5_hash = md5.new() md5_hash.update(u''.join(data)) #$NON-NLS-1$ return md5_hash.hexdigest() def encode_and_sign(self, dictionary): u'''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' #$NON-NLS-1$ dictionary = make_utf8(dictionary) if self.secret: dictionary[u'api_sig'] = self.sign(dictionary) #$NON-NLS-1$ return urllib.urlencode(dictionary) def __getattr__(self, attrib): u"""Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") xmlnode = flickr.photos_getInfo(photo_id='1234') xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode') json = flickr.photos_getInfo(photo_id='1234', format='json') etree = flickr.photos_getInfo(photo_id='1234', format='etree') """ #$NON-NLS-1$ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith(u'_'): #$NON-NLS-1$ raise AttributeError(u"No such attribute '%s'" % attrib) #$NON-NLS-1$ # Construct the method name and see if it's cached method = u"flickr." + attrib.replace(u"_", u".") #$NON-NLS-3$ #$NON-NLS-2$ #$NON-NLS-1$ if method in self.__handler_cache: return self.__handler_cache[method] def handler(**args): u'''Dynamically created handler for a Flickr API call''' #$NON-NLS-1$ if self.token_cache.token and not self.secret: raise ValueError(u"Auth tokens cannot be used without " #$NON-NLS-1$ u"API secret") #$NON-NLS-1$ # Set some defaults defaults = {u'method': method, #$NON-NLS-1$ u'auth_token': self.token_cache.token, #$NON-NLS-1$ u'api_key': self.api_key, #$NON-NLS-1$ u'format': self.default_format} #$NON-NLS-1$ args = self.__supply_defaults(args, defaults) args = make_utf8(args) return self.__wrap_in_parser(self.__flickr_call, parse_format=args[u'format'], **args) #$NON-NLS-1$ handler.method = method self.__handler_cache[method] = handler return handler def __supply_defaults(self, args, defaults): u'''Returns a new dictionary containing ``args``, augmented with defaults from ``defaults``. Defaults can be overridden, or completely removed by setting the appropriate value in ``args`` to ``None``. >>> f = FlickrAPI('123') >>> f._FlickrAPI__supply_defaults( ... {'foo': 'bar', 'baz': None, 'token': None}, ... {'baz': 'foobar', 'room': 'door'}) {'foo': 'bar', 'room': 'door'} ''' #$NON-NLS-1$ result = args.copy() for key, default_value in defaults.iteritems(): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in result.copy().iteritems(): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if result[key] is None: del result[key] return result def __flickr_call(self, **kwargs): u'''Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self.__flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') ''' #$NON-NLS-1$ LOG.debug(u"Calling %s" % kwargs) #$NON-NLS-1$ post_data = self.encode_and_sign(kwargs) # Return value from cache if available if self.cache and self.cache.get(post_data): return self.cache.get(post_data) url = u"http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_rest_form #$NON-NLS-1$ flicksocket = urllib.urlopen(url, post_data) reply = flicksocket.read() flicksocket.close() # Store in cache, if we have one if self.cache is not None: self.cache.set(post_data, reply) return reply def __wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs): u'''Wraps a method call in a parser. The parser will be looked up by the ``parse_format`` specifier. If there is a parser and ``kwargs['format']`` is set, it's set to ``rest``, and the response of the method is parsed before it's returned. ''' #$NON-NLS-1$ # Find the parser, and set the format to rest if we're supposed to # parse it. if parse_format in rest_parsers and u'format' in kwargs: #$NON-NLS-1$ kwargs[u'format'] = u'rest' #$NON-NLS-2$ #$NON-NLS-1$ LOG.debug(u'Wrapping call %s(self, %s, %s)' % (wrapped_method, args, #$NON-NLS-1$ kwargs)) data = wrapped_method(*args, **kwargs) # Just return if we have no parser if parse_format not in rest_parsers: return data # Return the parsed data parser = rest_parsers[parse_format] return parser(self, data) def auth_url(self, perms, frob): u"""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() """ #$NON-NLS-1$ encoded = self.encode_and_sign({ u"api_key": self.api_key, #$NON-NLS-1$ u"frob": frob, #$NON-NLS-1$ u"perms": perms}) #$NON-NLS-1$ return u"http://%s%s?%s" % (FlickrAPI.flickr_host, FlickrAPI.flickr_auth_form, encoded) #$NON-NLS-1$ def web_login_url(self, perms): u'''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' #$NON-NLS-1$ encoded = self.encode_and_sign({ u"api_key": self.api_key, #$NON-NLS-1$ u"perms": perms}) #$NON-NLS-1$ return u"http://%s%s?%s" % (FlickrAPI.flickr_host, FlickrAPI.flickr_auth_form, encoded) #$NON-NLS-1$ def upload(self, fileStream, callback=None, **arg): u"""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: fileStream file to upload callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private The callback method should take two parameters: def callback(progress, done) Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. For now, the callback gets a 'done' twice, once for the HTTP headers, once for the body. """ #$NON-NLS-1$ if not fileStream: raise IllegalArgumentException(u"fileStream must be specified") #$NON-NLS-1$ # verify key names required_params = (u'api_key', u'auth_token', u'api_sig') #$NON-NLS-3$ #$NON-NLS-2$ #$NON-NLS-1$ optional_params = (u'title', u'description', u'tags', u'is_public', #$NON-NLS-4$ #$NON-NLS-3$ #$NON-NLS-2$ #$NON-NLS-1$ u'is_friend', u'is_family', u'hidden') #$NON-NLS-2$ #$NON-NLS-1$ #$NON-NLS-3$ possible_args = required_params + optional_params for a in arg.keys(): if a not in possible_args: raise IllegalArgumentException(u"Unknown parameter " #$NON-NLS-1$ u"'%s' sent to FlickrAPI.upload" % a) #$NON-NLS-1$ arguments = {u'auth_token': self.token_cache.token, #$NON-NLS-1$ u'api_key': self.api_key} #$NON-NLS-1$ arguments.update(arg) # Convert to UTF-8 if an argument is an Unicode string arg = make_utf8(arguments) if self.secret: arg[u"api_sig"] = self.sign(arg) #$NON-NLS-1$ url = u"http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_upload_form #$NON-NLS-1$ return self._sendFile(url, arg, fileStream) # # # construct POST data # body = Multipart() # # for a in required_params + optional_params: # if a not in arg: # continue # # part = Part({u'name': a}, arg[a]) #$NON-NLS-1$ # body.attach(part) # # filepart = FilePart({u'name': u'photo'}, filename, u'image/jpeg') #$NON-NLS-3$ #$NON-NLS-2$ #$NON-NLS-1$ # body.attach(filepart) # # return self.__send_multipart(url, body, callback) def replace(self, filename, fileStream, photo_id): u"""Replace an existing photo. Supported parameters: filename name of a file to upload fileStream file to upload photo_id the ID of the photo to replace """ #$NON-NLS-1$ if not fileStream: raise IllegalArgumentException(u"fileStream must be specified") #$NON-NLS-1$ if not photo_id: raise IllegalArgumentException(u"photo_id must be specified") #$NON-NLS-1$ args = {u'filename': filename, #$NON-NLS-1$ u'photo_id': photo_id, #$NON-NLS-1$ u'auth_token': self.token_cache.token, #$NON-NLS-1$ u'api_key': self.api_key} #$NON-NLS-1$ args = make_utf8(args) if self.secret: args[u"api_sig"] = self.sign(args) #$NON-NLS-1$ url = u"http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_replace_form #$NON-NLS-1$ del args[u"filename"] #$NON-NLS-1$ return self._sendFile(url, args, fileStream) # # construct POST data # body = Multipart() # # for arg, value in args.iteritems(): # # No part for the filename # if value == u'filename': #$NON-NLS-1$ # continue # # part = Part({u'name': arg}, value) #$NON-NLS-1$ # body.attach(part) # # filepart = FilePart({u'name': u'photo'}, filename, u'image/jpeg') #$NON-NLS-3$ #$NON-NLS-2$ #$NON-NLS-1$ # body.attach(filepart) # # return self.__send_multipart(url, body) def _sendFile(self, url, params, fileStream): params[u"photo"] = fileStream #$NON-NLS-1$ request = ZSimpleTextHTTPRequest(url) request.send(params) rspXML = request.getResponse() return self.parse_xmlnode(rspXML) # end sendFile() def __send_multipart(self, url, body, progress_callback=None): u'''Sends a Multipart object to an URL. Returns the resulting XML from Flickr. ''' #$NON-NLS-1$ LOG.debug(u"Uploading to %s" % url) #$NON-NLS-1$ request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if progress_callback: response = reportinghttp.urlopen(request, progress_callback) else: response = urllib2.urlopen(request) rspXML = response.read() return self.parse_xmlnode(rspXML) @classmethod def test_failure(cls, rsp, exception_on_error=True): u"""Exit app if the rsp XMLNode indicates failure.""" #$NON-NLS-1$ LOG.warn(u"FlickrAPI.test_failure has been deprecated and will be " #$NON-NLS-1$ u"removed in FlickrAPI version 1.2.") #$NON-NLS-1$ if rsp[u'stat'] != u"fail": #$NON-NLS-2$ #$NON-NLS-1$ return message = cls.get_printable_error(rsp) LOG.error(message) if exception_on_error: raise FlickrError(message) @classmethod def get_printable_error(cls, rsp): u"""Return a printed error message string of an XMLNode Flickr response.""" #$NON-NLS-1$ LOG.warn(u"FlickrAPI.get_printable_error has been deprecated " #$NON-NLS-1$ u"and will be removed in FlickrAPI version 1.2.") #$NON-NLS-1$ return u"%s: error %s: %s" % (rsp.name, cls.get_rsp_error_code(rsp), cls.get_rsp_error_msg(rsp)) #$NON-NLS-1$ @classmethod def get_rsp_error_code(cls, rsp): u"""Return the error code of an XMLNode Flickr response, or 0 if no error. """ #$NON-NLS-1$ LOG.warn(u"FlickrAPI.get_rsp_error_code has been deprecated and will be " #$NON-NLS-1$ u"removed in FlickrAPI version 1.2.") #$NON-NLS-1$ if rsp[u'stat'] == u"fail": #$NON-NLS-2$ #$NON-NLS-1$ return int(rsp.err[0][u'code']) #$NON-NLS-1$ return 0 @classmethod def get_rsp_error_msg(cls, rsp): u"""Return the error message of an XMLNode Flickr response, or "Success" if no error. """ #$NON-NLS-1$ LOG.warn(u"FlickrAPI.get_rsp_error_msg has been deprecated and will be " #$NON-NLS-1$ u"removed in FlickrAPI version 1.2.") #$NON-NLS-1$ if rsp[u'stat'] == u"fail": #$NON-NLS-2$ #$NON-NLS-1$ return rsp.err[0][u'msg'] #$NON-NLS-1$ return u"Success" #$NON-NLS-1$ def validate_frob(self, frob, perms): u'''Lets the user validate the frob by launching a browser to the Flickr website. ''' #$NON-NLS-1$ auth_url = self.auth_url(perms, frob) webbrowser.open(auth_url, True, True) def get_token_part_one(self, perms=u"read"): #$NON-NLS-1$ u"""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 token is present and valid, it is returned by the method. 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 a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) """ #$NON-NLS-1$ # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug(u"Trying cached token '%s'" % token) #$NON-NLS-1$ try: rsp = self.auth_checkToken(auth_token=token, format=u'xmlnode') #$NON-NLS-1$ # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text if tokenPerms == u"read" and perms != u"read": token = None #$NON-NLS-2$ #$NON-NLS-1$ elif tokenPerms == u"write" and perms == u"delete": token = None #$NON-NLS-2$ #$NON-NLS-1$ except FlickrError: LOG.debug(u"Cached token invalid") #$NON-NLS-1$ self.token_cache.forget() token = None # get a new token if we need one if not token: # get the frob LOG.debug(u"Getting frob for new token") #$NON-NLS-1$ rsp = self.auth_getFrob(auth_token=None, format=u'xmlnode') #$NON-NLS-1$ self.test_failure(rsp) frob = rsp.frob[0].text # validate online self.validate_frob(frob, perms) return (token, frob) def get_token_part_two(self, (token, frob)): u"""Part two of getting a token, see ``get_token_part_one(...)`` for details.""" #$NON-NLS-1$ # If a valid token was obtained in the past, we're done if token: LOG.debug(u"get_token_part_two: no need, token already there") #$NON-NLS-1$ self.token_cache.token = token return token LOG.debug(u"get_token_part_two: getting a new token for frob '%s'" % frob) #$NON-NLS-1$ return self.get_token(frob)