def __init__(self, api_key, secret, username=None, token=None, format='etree', store_token=True, cache=False, token_cache_location=None, timeout=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. 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(u'123', u'123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) token_cache_location If not None, determines where the authentication tokens are stored. timeout Optional request timeout as float in seconds. """ self.default_format = format self._handler_cache = {} if isinstance(api_key, six.binary_type): api_key = api_key.decode('ascii') if isinstance(secret, six.binary_type): secret = secret.decode('ascii') if token: assert isinstance(token, auth.FlickrAccessToken) # Use a memory-only token cache self.token_cache = tokencache.SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = tokencache.SimpleTokenCache() else: # Use a real token cache self.token_cache = tokencache.OAuthTokenCache(api_key, username or '', path=token_cache_location) self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache, default_timeout=timeout) if cache: self.cache = SimpleCache() else: self.cache = None
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
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') """ REST_URL = 'https://api.flickr.com/services/rest/' UPLOAD_URL = 'https://up.flickr.com/services/upload/' REPLACE_URL = 'https://up.flickr.com/services/replace/' def __init__(self, api_key, secret, username=None, token=None, format='etree', store_token=True, cache=False, token_cache_location=None, timeout=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. 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(u'123', u'123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) token_cache_location If not None, determines where the authentication tokens are stored. timeout Optional request timeout as float in seconds. """ self.default_format = format self._handler_cache = {} if isinstance(api_key, six.binary_type): api_key = api_key.decode('ascii') if isinstance(secret, six.binary_type): secret = secret.decode('ascii') if token: assert isinstance(token, auth.FlickrAccessToken) # Use a memory-only token cache self.token_cache = tokencache.SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = tokencache.SimpleTokenCache() else: # Use a real token cache self.token_cache = tokencache.OAuthTokenCache(api_key, username or '', path=token_cache_location) self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache, default_timeout=timeout) 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.flickr_oauth.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 return [m.text[7:] 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(six.u('Error: %(code)s: %(msg)s') % err, code=err['code']) @rest_parser('parsed-json', 'json') def parse_json(self, json_string): """Parses a JSON response from Flickr.""" if isinstance(json_string, six.binary_type): json_string = json_string.decode('utf-8') import json parsed = json.loads(json_string) if parsed.get('stat', '') == 'fail': raise FlickrError(six.u('Error: %(code)s: %(message)s') % parsed, code=parsed['code']) return parsed @rest_parser('etree') def parse_etree(self, rest_xml): """Parses a REST XML response from Flickr into an ElementTree object.""" try: from lxml import etree as ElementTree LOG.info('REST Parser: using lxml.etree') except ImportError: try: import xml.etree.cElementTree as ElementTree LOG.info('REST Parser: using xml.etree.cElementTree') except ImportError: try: import xml.etree.ElementTree as ElementTree LOG.info('REST Parser: using xml.etree.ElementTree') except ImportError: try: import elementtree.cElementTree as ElementTree LOG.info('REST Parser: elementtree.cElementTree') except ImportError: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError("You need to install " "ElementTree to use the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok': return rsp err = rsp.find('err') code = err.attrib.get('code', None) raise FlickrError(six.u('Error: %(code)s: %(msg)s') % err.attrib, code=code) def __getattr__(self, method_name): """Returns a CallBuilder for the given method name.""" # Refuse to do anything with special methods if method_name.startswith('_'): raise AttributeError(method_name) # Compatibility with old way of calling, i.e. flickrobj.photos_getInfo(...) if '_' in method_name: method_name = method_name.replace('_', '.') return CallBuilder(self, method_name='flickr.' + method_name) def do_flickr_call(self, _method_name, timeout=None, **kwargs): """Handle all the regular Flickr API calls. Example:: 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') """ params = kwargs.copy() # Set some defaults defaults = {'method': _method_name, 'format': self.default_format} if 'jsoncallback' not in kwargs: defaults['nojsoncallback'] = 1 params = self._supply_defaults(params, defaults) LOG.info('Calling %s', defaults) return self._wrap_in_parser(self._flickr_call, parse_format=params['format'], timeout=timeout, **params) 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``. """ result = args.copy() for key, default_value in six.iteritems(defaults): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in six.iteritems(result.copy()): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if value is None: del result[key] return result def _flickr_call(self, timeout=None, **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) # Return value from cache if available if self.cache and self.cache.get(kwargs): return self.cache.get(kwargs) reply = self.flickr_oauth.do_request(self.REST_URL, kwargs, timeout=timeout) # Store in cache, if we have one if self.cache is not None: self.cache.set(kwargs, 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_parsers[parse_format][1] 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][0] return parser(self, data) 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 response_format = kwargs.get('format', self.default_format) if response_format not in rest_parsers and response_format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % response_format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return response_format def upload(self, filename, fileobj=None, timeout=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 fileobj an optional file-like object from which the data can be read 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. timeout Optional timeout for the HTTP request, as float in seconds. The ``fileobj`` parameter can be used to monitor progress via a callback method. For example:: class FileWithCallback(object): def __init__(self, filename, callback): self.file = open(filename, 'rb') self.callback = callback # the following attributes and methods are required self.len = os.path.getsize(path) self.fileno = self.file.fileno self.tell = self.file.tell def read(self, size): if self.callback: self.callback(self.tell() * 100 // self.len) return self.file.read(size) fileobj = FileWithCallback(filename, callback) rsp = flickr.upload(filename, fileobj, parameters) The callback method takes one parameter: ``def callback(progress)`` Progress is a number between 0 and 100. """ return self._upload_to_form(self.UPLOAD_URL, filename, fileobj, timeout=timeout, **kwargs) def replace(self, filename, photo_id, fileobj=None, timeout=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload fileobj an optional file-like object from which the data can be read photo_id the ID of the photo to replace 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. timeout Optional timeout for the HTTP request, as float in seconds. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self._upload_to_form(self.REPLACE_URL, filename, fileobj, timeout=timeout, **kwargs) def _upload_to_form(self, form_url, filename, fileobj=None, timeout=None, **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") kwargs['api_key'] = self.flickr_oauth.key # Figure out the response format response_format = self._extract_upload_response_format(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_bytes(kwargs) return self._wrap_in_parser(self.flickr_oauth.do_upload, response_format, filename, form_url, kwargs, fileobj, timeout=timeout) def token_valid(self, perms=u'read'): """Verifies the cached token with Flickr. If the token turns out to be invalid, or with permissions lower than required, the token is erased from the token cache. @return: True if the token is valid for the requested parameters, False otherwise. """ token = self.token_cache.token if not token: return False # Check token for validity self.flickr_oauth.token = token try: resp = self.auth.oauth.checkToken(format='etree') token_perms = resp.findtext('oauth/perms') if token_perms == token.access_level and token.has_level(perms): # Token is valid, and for the expected permissions. return True except FlickrError: # There was an error talking to Flickr, we assume this is due # to an invalid token. pass # Token was for other permissions, so erase it as it is # not usable for this request. self.flickr_oauth.token = None del self.token_cache.token return False @authenticator def authenticate_console(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Shows the URL the user should visit on stdout, then waits for the user to authorize the program. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_console(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_via_browser(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Starts the browser and waits for the user to authorize the app before continuing. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_browser(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_for_test(self, perms=u'read'): """Skips a bit of the authentication/authorization, for unit tests. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_for_test(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token def get_request_token(self, oauth_callback=None): """Requests a new request token. Updates this OAuthFlickrInterface object to use the request token on the following authentication calls. @param oauth_callback: the URL the user is sent to after granting the token access. If the callback is None, a local web server is started on a random port, and the callback will be http://localhost:randomport/ If you do not have a web-app and you also do not want to start a local web server, pass oauth_callback='oob' and have your application accept the verifier from the user instead. """ self.flickr_oauth.get_request_token(oauth_callback=oauth_callback) def auth_url(self, perms=u'read'): """Returns the URL the user should visit to authenticate the given oauth Token. Use this method in webapps, where you can redirect the user to the returned URL. After authorization by the user, the browser is redirected to the callback URL, which will contain the OAuth verifier. Set the 'verifier' property on this object in order to use it. In stand-alone apps, authenticate_via_browser(...) may be easier instead. """ return self.flickr_oauth.auth_url(perms=perms) def get_access_token(self, verifier=None): """Exchanges the request token for an access token. Also stores the access token for easy authentication of subsequent calls. @param verifier: the verifier code, in case you used out-of-band communication of the verifier code. """ if verifier is not None: self.flickr_oauth.verifier = verifier self.token_cache.token = self.flickr_oauth.get_access_token() @require_format('etree') def data_walker(self, method, searchstring='*/photo', **params): """Calls 'method' with page=0, page=1 etc. until the total number of pages has been visited. Yields the photos returned. Assumes that ``method(page=n, **params).findall(searchstring)`` results in a list of interesting elements (defaulting to photos), and that the toplevel element of the result contains a 'pages' attribute with the total number of pages. """ page = 1 total = 1 # We don't know that yet, update when needed while page <= total: # Fetch a single page of photos LOG.debug('Calling %s(page=%i of %i, %s)' % (method.__name__, page, total, params)) rsp = method(page=page, **params) photoset = rsp.getchildren()[0] total = int(photoset.get('pages')) photos = rsp.findall(searchstring) # Yield each photo for photo in photos: yield photo # Ready to get the next page page += 1 @require_format('etree') def walk_contacts(self, per_page=50, **kwargs): """walk_contacts(self, per_page=50, ...) -> \ generator, yields each contact of the calling user. :Parameters: per_page the number of contacts that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.contacts.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.contacts.getList: https://www.flickr.com/services/api/flickr.contacts.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.contacts_getList, searchstring='*/contact', per_page=per_page, **kwargs) @require_format('etree') def walk_photosets(self, per_page=50, **kwargs): """walk_photosets(self, per_page=50, ...) -> \ generator, yields each photoset belonging to a user. :Parameters: per_page the number of photosets that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getList: https://www.flickr.com/services/api/flickr.photosets.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getList, searchstring='*/photoset', per_page=per_page, **kwargs) @require_format('etree') def walk_set(self, photoset_id, per_page=50, **kwargs): """walk_set(self, photoset_id, per_page=50, ...) -> \ generator, yields each photo in a single set. :Parameters: photoset_id the photoset ID per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getPhotos: https://www.flickr.com/services/api/flickr.photosets.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getPhotos, photoset_id=photoset_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user(self, user_id='me', per_page=50, **kwargs): """walk_user(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream. :Parameters: user_id the user ID, or 'me' per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.people.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.people.getPhotos: https://www.flickr.com/services/api/flickr.people.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.people_getPhotos, user_id=user_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user_updates(self, min_date, per_page=50, **kwargs): """walk_user_updates(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream updated \ after ``min_date`` :Parameters: min_date per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photos.recentlyUpdated API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.recentlyUpdated: https://www.flickr.com/services/api/flickr.photos.recentlyUpdated.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photos_recentlyUpdated, min_date=min_date, per_page=per_page, **kwargs) @require_format('etree') def walk(self, per_page=50, **kwargs): """walk(self, user_id=..., tags=..., ...) -> generator, \ yields each photo in a search query result Accepts the same parameters as flickr.photos.search_ API call, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.search: https://www.flickr.com/services/api/flickr.photos.search.html Also see `walk_set`. """ return self.data_walker(self.photos.search, per_page=per_page, **kwargs)
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)
def __init__(self, api_key, secret, username=None, token=None, format='etree', store_token=True, cache=False, token_cache_location=None, timeout=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. 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(u'123', u'123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) token_cache_location If not None, determines where the authentication tokens are stored. timeout Optional request timeout as float in seconds. """ self.default_format = format self._handler_cache = {} if isinstance(api_key, six.binary_type): api_key = api_key.decode('ascii') if isinstance(secret, six.binary_type): secret = secret.decode('ascii') if token: assert isinstance(token, auth.FlickrAccessToken) # Use a memory-only token cache self.token_cache = tokencache.SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = tokencache.SimpleTokenCache() else: # Use a real token cache self.token_cache = tokencache.OAuthTokenCache( api_key, username or '', path=token_cache_location) self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache, default_timeout=timeout) if cache: self.cache = SimpleCache() else: self.cache = None
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') """ REST_URL = 'https://api.flickr.com/services/rest/' UPLOAD_URL = 'https://up.flickr.com/services/upload/' REPLACE_URL = 'https://up.flickr.com/services/replace/' def __init__(self, api_key, secret, username=None, token=None, format='etree', store_token=True, cache=False, token_cache_location=None, timeout=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. 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(u'123', u'123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) token_cache_location If not None, determines where the authentication tokens are stored. timeout Optional request timeout as float in seconds. """ self.default_format = format self._handler_cache = {} if isinstance(api_key, six.binary_type): api_key = api_key.decode('ascii') if isinstance(secret, six.binary_type): secret = secret.decode('ascii') if token: assert isinstance(token, auth.FlickrAccessToken) # Use a memory-only token cache self.token_cache = tokencache.SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = tokencache.SimpleTokenCache() else: # Use a real token cache self.token_cache = tokencache.OAuthTokenCache( api_key, username or '', path=token_cache_location) self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache, default_timeout=timeout) 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.flickr_oauth.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 return [m.text[7:] 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(six.u('Error: %(code)s: %(msg)s') % err, code=err['code']) @rest_parser('parsed-json', 'json') def parse_json(self, json_string): """Parses a JSON response from Flickr.""" if isinstance(json_string, six.binary_type): json_string = json_string.decode('utf-8') import json parsed = json.loads(json_string) if parsed.get('stat', '') == 'fail': raise FlickrError(six.u('Error: %(code)s: %(message)s') % parsed, code=parsed['code']) return parsed @rest_parser('etree') def parse_etree(self, rest_xml): """Parses a REST XML response from Flickr into an ElementTree object.""" try: from lxml import etree as ElementTree LOG.info('REST Parser: using lxml.etree') except ImportError: try: import xml.etree.cElementTree as ElementTree LOG.info('REST Parser: using xml.etree.cElementTree') except ImportError: try: import xml.etree.ElementTree as ElementTree LOG.info('REST Parser: using xml.etree.ElementTree') except ImportError: try: import elementtree.cElementTree as ElementTree LOG.info('REST Parser: elementtree.cElementTree') except ImportError: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError( "You need to install " "ElementTree to use the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok': return rsp err = rsp.find('err') code = err.attrib.get('code', None) raise FlickrError(six.u('Error: %(code)s: %(msg)s') % err.attrib, code=code) def __getattr__(self, method_name): """Returns a CallBuilder for the given method name.""" # Refuse to do anything with special methods if method_name.startswith('_'): raise AttributeError(method_name) # Compatibility with old way of calling, i.e. flickrobj.photos_getInfo(...) if '_' in method_name: method_name = method_name.replace('_', '.') return CallBuilder(self, method_name='flickr.' + method_name) def do_flickr_call(self, method_name, timeout=None, **kwargs): """Handle all the regular Flickr API calls. Example:: 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') """ params = kwargs.copy() # Set some defaults defaults = {'method': method_name, 'format': self.default_format} if 'jsoncallback' not in kwargs: defaults['nojsoncallback'] = 1 params = self._supply_defaults(params, defaults) LOG.info('Calling %s', defaults) return self._wrap_in_parser(self._flickr_call, parse_format=params['format'], timeout=timeout, **params) 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``. """ result = args.copy() for key, default_value in six.iteritems(defaults): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in six.iteritems(result.copy()): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if value is None: del result[key] return result def _flickr_call(self, timeout=None, **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) # Return value from cache if available if self.cache and self.cache.get(kwargs): return self.cache.get(kwargs) reply = self.flickr_oauth.do_request(self.REST_URL, kwargs, timeout=timeout) # Store in cache, if we have one if self.cache is not None: self.cache.set(kwargs, 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_parsers[parse_format][1] 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][0] return parser(self, data) 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 response_format = kwargs.get('format', self.default_format) if response_format not in rest_parsers and response_format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % response_format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return response_format def upload(self, filename, fileobj=None, timeout=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 fileobj an optional file-like object from which the data can be read 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. timeout Optional timeout for the HTTP request, as float in seconds. The ``fileobj`` parameter can be used to monitor progress via a callback method. For example:: class FileWithCallback(object): def __init__(self, filename, callback): self.file = open(filename, 'rb') self.callback = callback # the following attributes and methods are required self.len = os.path.getsize(path) self.fileno = self.file.fileno self.tell = self.file.tell def read(self, size): if self.callback: self.callback(self.tell() * 100 // self.len) return self.file.read(size) fileobj = FileWithCallback(filename, callback) rsp = flickr.upload(filename, fileobj, parameters) The callback method takes one parameter: ``def callback(progress)`` Progress is a number between 0 and 100. """ return self._upload_to_form(self.UPLOAD_URL, filename, fileobj, timeout=timeout, **kwargs) def replace(self, filename, photo_id, fileobj=None, timeout=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload fileobj an optional file-like object from which the data can be read photo_id the ID of the photo to replace 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. timeout Optional timeout for the HTTP request, as float in seconds. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self._upload_to_form(self.REPLACE_URL, filename, fileobj, timeout=timeout, **kwargs) def _upload_to_form(self, form_url, filename, fileobj=None, timeout=None, **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") kwargs['api_key'] = self.flickr_oauth.key # Figure out the response format response_format = self._extract_upload_response_format(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_bytes(kwargs) return self._wrap_in_parser(self.flickr_oauth.do_upload, response_format, filename, form_url, kwargs, fileobj, timeout=timeout) def token_valid(self, perms=u'read'): """Verifies the cached token with Flickr. If the token turns out to be invalid, or with permissions lower than required, the token is erased from the token cache. @return: True if the token is valid for the requested parameters, False otherwise. """ token = self.token_cache.token if not token: return False # Check token for validity self.flickr_oauth.token = token try: resp = self.auth.oauth.checkToken(format='etree') token_perms = resp.findtext('oauth/perms') if token_perms == token.access_level and token.has_level(perms): # Token is valid, and for the expected permissions. return True except FlickrError: # There was an error talking to Flickr, we assume this is due # to an invalid token. pass # Token was for other permissions, so erase it as it is # not usable for this request. self.flickr_oauth.token = None del self.token_cache.token return False @authenticator def authenticate_console(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Shows the URL the user should visit on stdout, then waits for the user to authorize the program. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_console(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_via_browser(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Starts the browser and waits for the user to authorize the app before continuing. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_browser(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_for_test(self, perms=u'read'): """Skips a bit of the authentication/authorization, for unit tests. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_for_test(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token def get_request_token(self, oauth_callback=None): """Requests a new request token. Updates this OAuthFlickrInterface object to use the request token on the following authentication calls. @param oauth_callback: the URL the user is sent to after granting the token access. If the callback is None, a local web server is started on a random port, and the callback will be http://localhost:randomport/ If you do not have a web-app and you also do not want to start a local web server, pass oauth_callback='oob' and have your application accept the verifier from the user instead. """ self.flickr_oauth.get_request_token(oauth_callback=oauth_callback) def auth_url(self, perms=u'read'): """Returns the URL the user should visit to authenticate the given oauth Token. Use this method in webapps, where you can redirect the user to the returned URL. After authorization by the user, the browser is redirected to the callback URL, which will contain the OAuth verifier. Set the 'verifier' property on this object in order to use it. In stand-alone apps, authenticate_via_browser(...) may be easier instead. """ return self.flickr_oauth.auth_url(perms=perms) def get_access_token(self, verifier=None): """Exchanges the request token for an access token. Also stores the access token for easy authentication of subsequent calls. @param verifier: the verifier code, in case you used out-of-band communication of the verifier code. """ if verifier is not None: self.flickr_oauth.verifier = verifier self.token_cache.token = self.flickr_oauth.get_access_token() @require_format('etree') def data_walker(self, method, searchstring='*/photo', **params): """Calls 'method' with page=0, page=1 etc. until the total number of pages has been visited. Yields the photos returned. Assumes that ``method(page=n, **params).findall(searchstring)`` results in a list of interesting elements (defaulting to photos), and that the toplevel element of the result contains a 'pages' attribute with the total number of pages. """ page = 1 total = 1 # We don't know that yet, update when needed while page <= total: # Fetch a single page of photos LOG.debug('Calling %s(page=%i of %i, %s)' % (method.__name__, page, total, params)) rsp = method(page=page, **params) photoset = rsp.getchildren()[0] total = int(photoset.get('pages')) photos = rsp.findall(searchstring) # Yield each photo for photo in photos: yield photo # Ready to get the next page page += 1 @require_format('etree') def walk_contacts(self, per_page=50, **kwargs): """walk_contacts(self, per_page=50, ...) -> \ generator, yields each contact of the calling user. :Parameters: per_page the number of contacts that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.contacts.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.contacts.getList: http://www.flickr.com/services/api/flickr.contacts.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.contacts_getList, searchstring='*/contact', per_page=per_page, **kwargs) @require_format('etree') def walk_photosets(self, per_page=50, **kwargs): """walk_photosets(self, per_page=50, ...) -> \ generator, yields each photoset belonging to a user. :Parameters: per_page the number of photosets that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getList: http://www.flickr.com/services/api/flickr.photosets.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getList, searchstring='*/photoset', per_page=per_page, **kwargs) @require_format('etree') def walk_set(self, photoset_id, per_page=50, **kwargs): """walk_set(self, photoset_id, per_page=50, ...) -> \ generator, yields each photo in a single set. :Parameters: photoset_id the photoset ID per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getPhotos: http://www.flickr.com/services/api/flickr.photosets.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getPhotos, photoset_id=photoset_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user(self, user_id='me', per_page=50, **kwargs): """walk_user(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream. :Parameters: user_id the user ID, or 'me' per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.people.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.people.getPhotos: http://www.flickr.com/services/api/flickr.people.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.people_getPhotos, user_id=user_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user_updates(self, min_date, per_page=50, **kwargs): """walk_user_updates(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream updated \ after ``min_date`` :Parameters: min_date per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photos.recentlyUpdated API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.recentlyUpdated: http://www.flickr.com/services/api/flickr.photos.recentlyUpdated.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photos_recentlyUpdated, min_date=min_date, per_page=per_page, **kwargs) @require_format('etree') def walk(self, per_page=50, **kwargs): """walk(self, user_id=..., tags=..., ...) -> generator, \ yields each photo in a search query result Accepts the same parameters as flickr.photos.search_ API call, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.search: http://www.flickr.com/services/api/flickr.photos.search.html Also see `walk_set`. """ return self.data_walker(self.photos.search, per_page=per_page, **kwargs)
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)