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