1
2
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
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
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)
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
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
108
109 rest_parsers = {}
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
137
138 return decorated
139 return decorator
140
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
205 self.token_cache = SimpleTokenCache()
206 self.token_cache.token = token
207 elif not store_token:
208
209 self.token_cache = SimpleTokenCache()
210 else:
211
212 self.token_cache = TokenCache(api_key, username)
213
214 if cache:
215 self.cache = SimpleCache()
216 else:
217 self.cache = None
218
220 '''Returns a string representation of this object.'''
221
222
223 return '[FlickrAPI for key "%s"]' % self.api_key
224 __str__ = __repr__
225
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')
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')
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
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
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
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
325 if attrib.startswith('_'):
326 raise AttributeError("No such attribute '%s'" % attrib)
327
328
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
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
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
372 if key not in args:
373 result[key] = default_value
374
375 for key, value in result.copy().iteritems():
376
377
378 if result[key] is None:
379 del result[key]
380
381 return result
382
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
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
407 if self.cache is not None:
408 self.cache.set(post_data, reply)
409
410 return reply
411
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
421
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
430 if parse_format not in rest_parsers:
431 return data
432
433
434 parser = rest_parsers[parse_format]
435 return parser(self, data)
436
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
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
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
607
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
623
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
631
632 if seen_header[0]:
633 return progress_callback(percentage, done)
634
635
636 if done:
637 seen_header[0] = True
638
639 response = reportinghttp.urlopen(request, __upload_callback)
640 return response.read()
641
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
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
707
708 authenticate = self.validate_frob
709 if auth_callback is not None:
710 if hasattr(auth_callback, '__call__'):
711
712 authenticate = auth_callback
713 elif auth_callback is False:
714 authenticate = None
715 else:
716
717 raise ValueError('Invalid value for auth_callback: %s'
718 % auth_callback)
719
720
721
722 token = self.token_cache.token
723 frob = None
724
725
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
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
741 if not token:
742
743 if not authenticate:
744 raise FlickrError('Authentication required but '
745 'blocked using auth_callback=False')
746
747
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
757 """Part two of getting a token, see ``get_token_part_one(...)`` for details."""
758
759
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
770 '''Gets the token given a certain frob. Used by ``get_token_part_two`` and
771 by the web authentication method.
772 '''
773
774
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
781 self.token_cache.token = token
782
783 return token
784
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')
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
813 while page <= total:
814
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
825 for photo in photos:
826 yield photo
827
828
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
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