Package flickrapi
[hide private]
[frames] | no frames]

Source Code for Package flickrapi

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3   
  4  '''A FlickrAPI interface. 
  5   
  6  The main functionality can be found in the `flickrapi.FlickrAPI` 
  7  class. 
  8   
  9  See `the FlickrAPI homepage`_ for more info. 
 10   
 11  .. _`the FlickrAPI homepage`: http://stuvel.eu/projects/flickrapi 
 12  ''' 
 13   
 14  __version__ = '1.4.4' 
 15  __all__ = ('FlickrAPI', 'IllegalArgumentException', 'FlickrError', 
 16             'CancelUpload', 'XMLNode', 'set_log_level', '__version__') 
 17  __author__ = u'Sybren St\u00fcvel'.encode('utf-8') 
 18   
 19  # Copyright (c) 2007 by the respective coders, see 
 20  # http://www.stuvel.eu/projects/flickrapi 
 21  # 
 22  # This code is subject to the Python licence, as can be read on 
 23  # http://www.python.org/download/releases/2.5.2/license/ 
 24  # 
 25  # For those without an internet connection, here is a summary. When this 
 26  # summary clashes with the Python licence, the latter will be applied. 
 27  # 
 28  # Permission is hereby granted, free of charge, to any person obtaining 
 29  # a copy of this software and associated documentation files (the 
 30  # "Software"), to deal in the Software without restriction, including 
 31  # without limitation the rights to use, copy, modify, merge, publish, 
 32  # distribute, sublicense, and/or sell copies of the Software, and to 
 33  # permit persons to whom the Software is furnished to do so, subject to 
 34  # the following conditions: 
 35  # 
 36  # The above copyright notice and this permission notice shall be 
 37  # included in all copies or substantial portions of the Software. 
 38  # 
 39  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 40  # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 41  # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 42  # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 43  # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 44  # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 45  # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
 46   
 47  import urllib 
 48  import urllib2 
 49  import os.path 
 50  import logging 
 51  import webbrowser 
 52   
 53  # Smartly import hashlib and fall back on md5 
 54  try: 
 55      from hashlib import md5 
 56  except ImportError: 
 57      from md5 import md5 
 58   
 59  from flickrapi.tokencache import (TokenCache, SimpleTokenCache, 
 60                                    LockingTokenCache) 
 61  from flickrapi.xmlnode import XMLNode 
 62  from flickrapi.multipart import Part, Multipart, FilePart 
 63  from flickrapi.exceptions import (IllegalArgumentException, FlickrError, 
 64                                    CancelUpload) 
 65  from flickrapi.cache import SimpleCache 
 66  from flickrapi import reportinghttp 
 67   
 68  logging.basicConfig() 
 69  LOG = logging.getLogger(__name__) 
 70  LOG.setLevel(logging.INFO) 
71 72 73 -def make_utf8(dictionary):
74 '''Encodes all Unicode strings in the dictionary to UTF-8. Converts 75 all other objects to regular strings. 76 77 Returns a copy of the dictionary, doesn't touch the original. 78 ''' 79 80 result = {} 81 82 for (key, value) in dictionary.iteritems(): 83 if isinstance(value, unicode): 84 value = value.encode('utf-8') 85 else: 86 value = str(value) 87 result[key] = value 88 89 return result
90
91 92 -def debug(method):
93 '''Method decorator for debugging method calls. 94 95 Using this automatically sets the log level to DEBUG. 96 ''' 97 98 LOG.setLevel(logging.DEBUG) 99 100 def debugged(*args, **kwargs): 101 LOG.debug("Call: %s(%s, %s)" % (method.__name__, args, 102 kwargs)) 103 result = method(*args, **kwargs) 104 LOG.debug("\tResult: %s" % result) 105 return result
106 107 return debugged 108 109 110 # REST parsers, {format: parser_method, ...}. Fill by using the 111 # @rest_parser(format) function decorator 112 rest_parsers = {}
113 114 115 -def rest_parser(format):
116 '''Method decorator, use this to mark a function as the parser for 117 REST as returned by Flickr. 118 ''' 119 120 def decorate_parser(method): 121 rest_parsers[format] = method 122 return method
123 124 return decorate_parser 125
126 127 -def require_format(required_format):
128 '''Method decorator, raises a ValueError when the decorated method 129 is called if the default format is not set to ``required_format``. 130 ''' 131 132 def decorator(method): 133 def decorated(self, *args, **kwargs): 134 # If everything is okay, call the method 135 if self.default_format == required_format: 136 return method(self, *args, **kwargs) 137 138 # Otherwise raise an exception 139 msg = 'Function %s requires that you use ' \ 140 'ElementTree ("etree") as the communication format, ' \ 141 'while the current format is set to "%s".' 142 raise ValueError(msg % (method.func_name, self.default_format))
143 144 return decorated 145 return decorator 146
147 148 -class FlickrAPI(object):
149 """Encapsulates Flickr functionality. 150 151 Example usage:: 152 153 flickr = flickrapi.FlickrAPI(api_key) 154 photos = flickr.photos_search(user_id='73509078@N00', per_page='10') 155 sets = flickr.photosets_getList(user_id='73509078@N00') 156 """ 157 158 flickr_host = "api.flickr.com" 159 flickr_rest_form = "/services/rest/" 160 flickr_auth_form = "/services/auth/" 161 flickr_upload_form = "/services/upload/" 162 flickr_replace_form = "/services/replace/" 163
164 - def __init__(self, api_key, secret=None, username=None, 165 token=None, format='etree', store_token=True, 166 cache=False):
167 """Construct a new FlickrAPI instance for a given API key 168 and secret. 169 170 api_key 171 The API key as obtained from Flickr. 172 173 secret 174 The secret belonging to the API key. 175 176 username 177 Used to identify the appropriate authentication token for a 178 certain user. 179 180 token 181 If you already have an authentication token, you can give 182 it here. It won't be stored on disk by the FlickrAPI instance. 183 184 format 185 The response format. Use either "xmlnode" or "etree" to get a 186 parsed response, or use any response format supported by Flickr 187 to get an unparsed response from method calls. It's also possible 188 to pass the ``format`` parameter on individual calls. 189 190 store_token 191 Disables the on-disk token cache if set to False (default is True). 192 Use this to ensure that tokens aren't read nor written to disk, for 193 example in web applications that store tokens in cookies. 194 195 cache 196 Enables in-memory caching of FlickrAPI calls - set to ``True`` to 197 use. If you don't want to use the default settings, you can 198 instantiate a cache yourself too: 199 200 >>> f = FlickrAPI(api_key='123') 201 >>> f.cache = SimpleCache(timeout=5, max_entries=100) 202 """ 203 204 self.api_key = api_key 205 self.secret = secret 206 self.default_format = format 207 208 self.__handler_cache = {} 209 210 if token: 211 # Use a memory-only token cache 212 self.token_cache = SimpleTokenCache() 213 self.token_cache.token = token 214 elif not store_token: 215 # Use an empty memory-only token cache 216 self.token_cache = SimpleTokenCache() 217 else: 218 # Use a real token cache 219 self.token_cache = TokenCache(api_key, username) 220 221 if cache: 222 self.cache = SimpleCache() 223 else: 224 self.cache = None
225
226 - def __repr__(self):
227 '''Returns a string representation of this object.''' 228 229 return '[FlickrAPI for key "%s"]' % self.api_key
230 __str__ = __repr__ 231
232 - def trait_names(self):
233 '''Returns a list of method names as supported by the Flickr 234 API. Used for tab completion in IPython. 235 ''' 236 237 try: 238 rsp = self.reflection_getMethods(format='etree') 239 except FlickrError: 240 return None 241 242 def tr(name): 243 '''Translates Flickr names to something that can be called 244 here. 245 246 >>> tr(u'flickr.photos.getInfo') 247 u'photos_getInfo' 248 ''' 249 250 return name[7:].replace('.', '_')
251 252 return [tr(m.text) for m in rsp.getiterator('method')]
253 254 @rest_parser('xmlnode')
255 - def parse_xmlnode(self, rest_xml):
256 '''Parses a REST XML response from Flickr into an XMLNode object.''' 257 258 rsp = XMLNode.parse(rest_xml, store_xml=True) 259 if rsp['stat'] == 'ok': 260 return rsp 261 262 err = rsp.err[0] 263 raise FlickrError(u'Error: %(code)s: %(msg)s' % err)
264 265 @rest_parser('etree')
266 - def parse_etree(self, rest_xml):
267 '''Parses a REST XML response from Flickr into 268 an ElementTree object.''' 269 270 try: 271 import xml.etree.ElementTree as ElementTree 272 except ImportError: 273 # For Python 2.4 compatibility: 274 try: 275 import elementtree.ElementTree as ElementTree 276 except ImportError: 277 raise ImportError("You need to install " 278 "ElementTree for using the etree format") 279 280 rsp = ElementTree.fromstring(rest_xml) 281 if rsp.attrib['stat'] == 'ok': 282 return rsp 283 284 err = rsp.find('err') 285 raise FlickrError(u'Error: %(code)s: %(msg)s' % err.attrib)
286
287 - def sign(self, dictionary):
288 """Calculate the flickr signature for a set of params. 289 290 data 291 a hash of all the params and values to be hashed, e.g. 292 ``{"api_key":"AAAA", "auth_token":"TTTT", "key": 293 u"value".encode('utf-8')}`` 294 295 """ 296 297 data = [self.secret] 298 for key in sorted(dictionary.keys()): 299 data.append(key) 300 datum = dictionary[key] 301 if isinstance(datum, unicode): 302 raise IllegalArgumentException("No Unicode allowed, " 303 "argument %s (%r) should have " 304 "been UTF-8 by now" 305 % (key, datum)) 306 data.append(datum) 307 md5_hash = md5(''.join(data)) 308 return md5_hash.hexdigest()
309
310 - def encode_and_sign(self, dictionary):
311 '''URL encodes the data in the dictionary, and signs it using the 312 given secret, if a secret was given. 313 ''' 314 315 dictionary = make_utf8(dictionary) 316 if self.secret: 317 dictionary['api_sig'] = self.sign(dictionary) 318 return urllib.urlencode(dictionary)
319
320 - def __getattr__(self, attrib):
321 """Handle all the regular Flickr API calls. 322 323 Example:: 324 325 flickr.auth_getFrob(api_key="AAAAAA") 326 etree = flickr.photos_getInfo(photo_id='1234') 327 etree = flickr.photos_getInfo(photo_id='1234', format='etree') 328 xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode') 329 json = flickr.photos_getInfo(photo_id='1234', format='json') 330 """ 331 332 # Refuse to act as a proxy for unimplemented special methods 333 if attrib.startswith('_'): 334 raise AttributeError("No such attribute '%s'" % attrib) 335 336 # Construct the method name and see if it's cached 337 method = "flickr." + attrib.replace("_", ".") 338 if method in self.__handler_cache: 339 return self.__handler_cache[method] 340 341 def handler(**args): 342 '''Dynamically created handler for a Flickr API call''' 343 344 if self.token_cache.token and not self.secret: 345 raise ValueError("Auth tokens cannot be used without " 346 "API secret") 347 348 # Set some defaults 349 defaults = {'method': method, 350 'auth_token': self.token_cache.token, 351 'api_key': self.api_key, 352 'format': self.default_format} 353 354 args = self.__supply_defaults(args, defaults) 355 356 return self.__wrap_in_parser(self.__flickr_call, 357 parse_format=args['format'], **args)
358 359 handler.method = method 360 self.__handler_cache[method] = handler 361 return handler 362
363 - def __supply_defaults(self, args, defaults):
364 '''Returns a new dictionary containing ``args``, augmented with defaults 365 from ``defaults``. 366 367 Defaults can be overridden, or completely removed by setting the 368 appropriate value in ``args`` to ``None``. 369 370 >>> f = FlickrAPI('123') 371 >>> f._FlickrAPI__supply_defaults( 372 ... {'foo': 'bar', 'baz': None, 'token': None}, 373 ... {'baz': 'foobar', 'room': 'door'}) 374 {'foo': 'bar', 'room': 'door'} 375 ''' 376 377 result = args.copy() 378 for key, default_value in defaults.iteritems(): 379 # Set the default if the parameter wasn't passed 380 if key not in args: 381 result[key] = default_value 382 383 for key, value in result.copy().iteritems(): 384 # You are able to remove a default by assigning None, and we can't 385 # pass None to Flickr anyway. 386 if result[key] is None: 387 del result[key] 388 389 return result
390
391 - def __flickr_call(self, **kwargs):
392 '''Performs a Flickr API call with the given arguments. The method name 393 itself should be passed as the 'method' parameter. 394 395 Returns the unparsed data from Flickr:: 396 397 data = self.__flickr_call(method='flickr.photos.getInfo', 398 photo_id='123', format='rest') 399 ''' 400 401 LOG.debug("Calling %s" % kwargs) 402 403 post_data = self.encode_and_sign(kwargs) 404 405 # Return value from cache if available 406 if self.cache and self.cache.get(post_data): 407 return self.cache.get(post_data) 408 409 url = "https://" + self.flickr_host + self.flickr_rest_form 410 flicksocket = urllib2.urlopen(url, post_data) 411 reply = flicksocket.read() 412 flicksocket.close() 413 414 # Store in cache, if we have one 415 if self.cache is not None: 416 self.cache.set(post_data, reply) 417 418 return reply
419
420 - def __wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs):
421 '''Wraps a method call in a parser. 422 423 The parser will be looked up by the ``parse_format`` specifier. If 424 there is a parser and ``kwargs['format']`` is set, it's set to 425 ``rest``, and the response of the method is parsed before it's 426 returned. 427 ''' 428 429 # Find the parser, and set the format to rest if we're supposed to 430 # parse it. 431 if parse_format in rest_parsers and 'format' in kwargs: 432 kwargs['format'] = 'rest' 433 434 LOG.debug('Wrapping call %s(self, %s, %s)' % (wrapped_method, args, 435 kwargs)) 436 data = wrapped_method(*args, **kwargs) 437 438 # Just return if we have no parser 439 if parse_format not in rest_parsers: 440 return data 441 442 # Return the parsed data 443 parser = rest_parsers[parse_format] 444 return parser(self, data)
445
446 - def auth_url(self, perms, frob):
447 """Return the authorization URL to get a token. 448 449 This is the URL the app will launch a browser toward if it 450 needs a new token. 451 452 perms 453 "read", "write", or "delete" 454 frob 455 picked up from an earlier call to FlickrAPI.auth_getFrob() 456 457 """ 458 459 encoded = self.encode_and_sign({"api_key": self.api_key, 460 "frob": frob, 461 "perms": perms}) 462 463 return "https://%s%s?%s" % (self.flickr_host, 464 self.flickr_auth_form, encoded)
465
466 - def web_login_url(self, perms):
467 '''Returns the web login URL to forward web users to. 468 469 perms 470 "read", "write", or "delete" 471 ''' 472 473 encoded = self.encode_and_sign({"api_key": self.api_key, 474 "perms": perms}) 475 476 return "https://%s%s?%s" % (self.flickr_host, 477 self.flickr_auth_form, encoded)
478
479 - def __extract_upload_response_format(self, kwargs):
480 '''Returns the response format given in kwargs['format'], or 481 the default format if there is no such key. 482 483 If kwargs contains 'format', it is removed from kwargs. 484 485 If the format isn't compatible with Flickr's upload response 486 type, a FlickrError exception is raised. 487 ''' 488 489 # Figure out the response format 490 format = kwargs.get('format', self.default_format) 491 if format not in rest_parsers and format != 'rest': 492 raise FlickrError('Format %s not supported for uploading ' 493 'photos' % format) 494 495 # The format shouldn't be used in the request to Flickr. 496 if 'format' in kwargs: 497 del kwargs['format'] 498 499 return format
500
501 - def upload(self, filename, callback=None, **kwargs):
502 """Upload a file to flickr. 503 504 Be extra careful you spell the parameters correctly, or you will 505 get a rather cryptic "Invalid Signature" error on the upload! 506 507 Supported parameters: 508 509 filename 510 name of a file to upload 511 callback 512 method that gets progress reports 513 title 514 title of the photo 515 description 516 description a.k.a. caption of the photo 517 tags 518 space-delimited list of tags, ``'''tag1 tag2 "long 519 tag"'''`` 520 is_public 521 "1" or "0" for a public resp. private photo 522 is_friend 523 "1" or "0" whether friends can see the photo while it's 524 marked as private 525 is_family 526 "1" or "0" whether family can see the photo while it's 527 marked as private 528 content_type 529 Set to "1" for Photo, "2" for Screenshot, or "3" for Other. 530 hidden 531 Set to "1" to keep the photo in global search results, "2" 532 to hide from public searches. 533 format 534 The response format. You can only choose between the 535 parsed responses or 'rest' for plain REST. 536 537 The callback method should take two parameters: 538 ``def callback(progress, done)`` 539 540 Progress is a number between 0 and 100, and done is a boolean 541 that's true only when the upload is done. 542 """ 543 544 return self.__upload_to_form(self.flickr_upload_form, 545 filename, callback, **kwargs)
546
547 - def replace(self, filename, photo_id, callback=None, **kwargs):
548 """Replace an existing photo. 549 550 Supported parameters: 551 552 filename 553 name of a file to upload 554 photo_id 555 the ID of the photo to replace 556 callback 557 method that gets progress reports 558 format 559 The response format. You can only choose between the 560 parsed responses or 'rest' for plain REST. Defaults to the 561 format passed to the constructor. 562 563 The callback parameter has the same semantics as described in the 564 ``upload`` function. 565 """ 566 567 if not photo_id: 568 raise IllegalArgumentException("photo_id must be specified") 569 570 kwargs['photo_id'] = photo_id 571 return self.__upload_to_form(self.flickr_replace_form, 572 filename, callback, **kwargs)
573
574 - def __upload_to_form(self, form_url, filename, callback, **kwargs):
575 '''Uploads a photo - can be used to either upload a new photo 576 or replace an existing one. 577 578 form_url must be either ``FlickrAPI.flickr_replace_form`` or 579 ``FlickrAPI.flickr_upload_form``. 580 ''' 581 582 if not filename: 583 raise IllegalArgumentException("filename must be specified") 584 if not self.token_cache.token: 585 raise IllegalArgumentException("Authentication is required") 586 587 # Figure out the response format 588 format = self.__extract_upload_response_format(kwargs) 589 590 # Update the arguments with the ones the user won't have to supply 591 arguments = {'auth_token': self.token_cache.token, 592 'api_key': self.api_key} 593 arguments.update(kwargs) 594 595 # Convert to UTF-8 if an argument is an Unicode string 596 kwargs = make_utf8(arguments) 597 598 if self.secret: 599 kwargs["api_sig"] = self.sign(kwargs) 600 url = "https://%s%s" % (self.flickr_host, form_url) 601 602 # construct POST data 603 body = Multipart() 604 605 for arg, value in kwargs.iteritems(): 606 part = Part({'name': arg}, value) 607 body.attach(part) 608 609 filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') 610 body.attach(filepart) 611 612 return self.__wrap_in_parser(self.__send_multipart, format, 613 url, body, callback)
614
615 - def __send_multipart(self, url, body, progress_callback=None):
616 '''Sends a Multipart object to an URL. 617 618 Returns the resulting unparsed XML from Flickr. 619 ''' 620 621 LOG.debug("Uploading to %s" % url) 622 request = urllib2.Request(url) 623 request.add_data(str(body)) 624 625 (header, value) = body.header() 626 request.add_header(header, value) 627 628 if not progress_callback: 629 # Just use urllib2 if there is no progress callback 630 # function 631 response = urllib2.urlopen(request) 632 return response.read() 633 634 def __upload_callback(percentage, done, seen_header=[False]): 635 '''Filters out the progress report on the HTTP header''' 636 637 # Call the user's progress callback when we've filtered 638 # out the HTTP header 639 if seen_header[0]: 640 return progress_callback(percentage, done) 641 642 # Remember the first time we hit 'done'. 643 if done: 644 seen_header[0] = True
645 646 response = reportinghttp.urlopen(request, __upload_callback) 647 return response.read() 648
649 - def validate_frob(self, frob, perms):
650 '''Lets the user validate the frob by launching a browser to 651 the Flickr website. 652 ''' 653 654 auth_url = self.auth_url(perms, frob) 655 try: 656 browser = webbrowser.get() 657 except webbrowser.Error: 658 if 'BROWSER' not in os.environ: 659 raise 660 browser = webbrowser.GenericBrowser(os.environ['BROWSER']) 661 662 browser.open(auth_url, True, True)
663
664 - def get_token_part_one(self, perms="read", auth_callback=None):
665 """Get a token either from the cache, or make a new one from 666 the frob. 667 668 This first attempts to find a token in the user's token cache 669 on disk. If that token is present and valid, it is returned by 670 the method. 671 672 If that fails (or if the token is no longer valid based on 673 flickr.auth.checkToken) a new frob is acquired. If an auth_callback 674 method has been specified it will be called. Otherwise the frob is 675 validated by having the user log into flickr (with a browser). 676 677 To get a proper token, follow these steps: 678 - Store the result value of this method call 679 - Give the user a way to signal the program that he/she 680 has authorized it, for example show a button that can be 681 pressed. 682 - Wait for the user to signal the program that the 683 authorization was performed, but only if there was no 684 cached token. 685 - Call flickrapi.get_token_part_two(...) and pass it the 686 result value you stored. 687 688 The newly minted token is then cached locally for the next 689 run. 690 691 perms 692 "read", "write", or "delete" 693 auth_callback 694 method to be called if authorization is needed. When not 695 passed, ``self.validate_frob(...)`` is called. You can 696 call this method yourself from the callback method too. 697 698 If authorization should be blocked, pass 699 ``auth_callback=False``. 700 701 The auth_callback method should take ``(frob, perms)`` as 702 parameters. 703 704 An example:: 705 706 (token, frob) = flickr.get_token_part_one(perms='write') 707 if not token: 708 raw_input("Press ENTER after you authorized this program") 709 flickr.get_token_part_two((token, frob)) 710 711 Also take a look at ``authenticate_console(perms)``. 712 """ 713 714 # Check our auth_callback parameter for correctness before we 715 # do anything 716 authenticate = self.validate_frob 717 if auth_callback is not None: 718 if hasattr(auth_callback, '__call__'): 719 # use the provided callback function 720 authenticate = auth_callback 721 elif auth_callback is False: 722 authenticate = None 723 else: 724 # Any non-callable non-False value is invalid 725 raise ValueError('Invalid value for auth_callback: %s' 726 % auth_callback) 727 728 # see if we have a saved token 729 token = self.token_cache.token 730 frob = None 731 732 # see if it's valid 733 if token: 734 LOG.debug("Trying cached token '%s'" % token) 735 try: 736 rsp = self.auth_checkToken(auth_token=token, format='xmlnode') 737 738 # see if we have enough permissions 739 tokenPerms = rsp.auth[0].perms[0].text 740 if tokenPerms == "read" and perms != "read": 741 token = None 742 elif tokenPerms == "write" and perms == "delete": 743 token = None 744 except FlickrError: 745 LOG.debug("Cached token invalid") 746 self.token_cache.forget() 747 token = None 748 749 # get a new token if we need one 750 if not token: 751 # If we can't authenticate, it's all over. 752 if not authenticate: 753 raise FlickrError('Authentication required but ' 754 'blocked using auth_callback=False') 755 756 # get the frob 757 LOG.debug("Getting frob for new token") 758 rsp = self.auth_getFrob(auth_token=None, format='xmlnode') 759 760 frob = rsp.frob[0].text 761 authenticate(frob, perms) 762 763 return (token, frob)
764
765 - def get_token_part_two(self, (token, frob)):
766 """Part two of getting a token, 767 see ``get_token_part_one(...)`` for details.""" 768 769 # If a valid token was obtained in the past, we're done 770 if token: 771 LOG.debug("get_token_part_two: no need, token already there") 772 self.token_cache.token = token 773 return token 774 775 LOG.debug("get_token_part_two: " 776 "getting a new token for frob '%s'" % frob) 777 778 return self.get_token(frob)
779
780 - def get_token(self, frob):
781 '''Gets the token given a certain frob. Used by ``get_token_part_two`` and 782 by the web authentication method. 783 ''' 784 785 # get a token 786 rsp = self.auth_getToken(frob=frob, auth_token=None, format='xmlnode') 787 788 token = rsp.auth[0].token[0].text 789 LOG.debug("get_token: new token '%s'" % token) 790 791 # store the auth info for next time 792 self.token_cache.token = token 793 794 return token
795
796 - def authenticate_console(self, perms='read', auth_callback=None):
797 '''Performs the authentication, assuming a console program. 798 799 Gets the token, if needed starts the browser and waits for the user to 800 press ENTER before continuing. 801 802 See ``get_token_part_one(...)`` for an explanation of the 803 parameters. 804 ''' 805 806 (token, frob) = self.get_token_part_one(perms, auth_callback) 807 if not token: 808 raw_input("Press ENTER after you authorized this program") 809 810 self.get_token_part_two((token, frob))
811 812 @require_format('etree')
813 - def __data_walker(self, method, **params):
814 '''Calls 'method' with page=0, page=1 etc. until the total 815 number of pages has been visited. Yields the photos 816 returned. 817 818 Assumes that ``method(page=n, **params).findall('*/photos')`` 819 results in a list of photos, and that the toplevel element of 820 the result contains a 'pages' attribute with the total number 821 of pages. 822 ''' 823 824 page = 1 825 total = 1 # We don't know that yet, update when needed 826 while page <= total: 827 # Fetch a single page of photos 828 LOG.debug('Calling %s(page=%i of %i, %s)' % 829 (method.func_name, page, total, params)) 830 rsp = method(page=page, **params) 831 832 photoset = rsp.getchildren()[0] 833 total = int(photoset.get('pages')) 834 835 photos = rsp.findall('*/photo') 836 837 # Yield each photo 838 for photo in photos: 839 yield photo 840 841 # Ready to get the next page 842 page += 1
843 844 @require_format('etree')
845 - def walk_set(self, photoset_id, per_page=50, **kwargs):
846 '''walk_set(self, photoset_id, per_page=50, ...) -> \ 847 generator, yields each photo in a single set. 848 849 :Parameters: 850 photoset_id 851 the photoset ID 852 per_page 853 the number of photos that are fetched in one call to 854 Flickr. 855 856 Other arguments can be passed, as documented in the 857 flickr.photosets.getPhotos_ API call in the Flickr API 858 documentation, except for ``page`` because all pages will be 859 returned eventually. 860 861 .. _flickr.photosets.getPhotos: 862 http://www.flickr.com/services/api/flickr.photosets.getPhotos.html 863 864 Uses the ElementTree format, incompatible with other formats. 865 ''' 866 867 return self.__data_walker(self.photosets_getPhotos, 868 photoset_id=photoset_id, 869 per_page=per_page, **kwargs)
870 871 @require_format('etree')
872 - def walk(self, per_page=50, **kwargs):
873 '''walk(self, user_id=..., tags=..., ...) -> generator, \ 874 yields each photo in a search query result 875 876 Accepts the same parameters as flickr.photos.search_ API call, 877 except for ``page`` because all pages will be returned 878 eventually. 879 880 .. _flickr.photos.search: 881 http://www.flickr.com/services/api/flickr.photos.search.html 882 883 Also see `walk_set`. 884 ''' 885 886 return self.__data_walker(self.photos_search, 887 per_page=per_page, **kwargs)
888
889 890 -def set_log_level(level):
891 '''Sets the log level of the logger used by the FlickrAPI module. 892 893 >>> import flickrapi 894 >>> import logging 895 >>> flickrapi.set_log_level(logging.INFO) 896 ''' 897 898 import flickrapi.tokencache 899 900 LOG.setLevel(level) 901 flickrapi.tokencache.LOG.setLevel(level)
902 903 904 if __name__ == "__main__": 905 print "Running doctests" 906 import doctest 907 doctest.testmod() 908 print "Tests OK" 909