flickrapi-2.1.2/0000775000175000017500000000000012613632407014314 5ustar sybrensybren00000000000000flickrapi-2.1.2/UPGRADING.txt0000664000175000017500000000725712613631224016404 0ustar sybrensybren00000000000000Upgrading from previous versions ================================= From any 1.x release to 2.0: --------------------------------- For this release the main goal was to quickly transition from the obsolete authentication method to OAuth. As a result, some features of the 1.x version have been dropped. If you want any of those features back, let me know at: https://bitbucket.org/sybren/flickrapi/issues?status=new&status=open Authentication has been re-written to use OAuth. See the documentation on how to use this. Some results are: - You always have to pass both the API key and secret. In 1.x you could choose to pass only the API key, but this no longer works with OAuth. - The token cache is now based on SQLite3, and contains not only the authentication tokens, but also the user's full name, username and NSID. - For non-web applications, a local HTTP server is started to receive the verification code. This means that the user does not have to copy and paste the verification code to the application. - The authentication callback functionality is gone. I'm not sure how many people still need this now that we've moved to OAuth. - The upload progress-callback functionality has been dropped. This was a hack on top of httplib, so this no longer works using Requests and OAuth. - Persistent connections have been dropped. Flickr functions can be called with dotted notation. For example:: flickr.photos_getInfo(photo_id='123') now becomes: flickr.photos.getInfo(photo_id='123') ^ | note the change from underscore to dot. For backward compatibility the old underscore-notation still works. From 1.1 --------------------------------- Some methods have been deprecated in version 1.1, which are now removed. Those are the class methods: - test_failure - get_printable_error - get_rsp_error_code - get_rsp_error_msg The default parser format has been changed from XMLNode to ElementTree. Either convert your code to use the new ElementTree parser, or pass the ``format='xmlnode'`` parameter to the FlickrAPI constructor. The upload and replace methods now use the format parameter, so if you use ElementTree as the parser, you'll now also get an ElementTree response from uploading and replacing photos. To keep the old behaviour you can pass ``format='xmlnode'`` to those methods. From 0.15 --------------------------------- A lot of name changes have occurred in version 0.16 to follow PEP 8. Some properties have also had their name shortened. For example, an ``XMLNode`` now has a ``text`` property instead of ``elementText``. After all, the nodes describe XML elements, so what other text would there be? Here is a complete list of the publicly visible changes, broken down per class. Changes in the internals of the FlickrAPI aren't documented here. ``FlickrAPI`` The constructor has its parameter ``apiKey`` changed to ``api_key``. All methods names that were originally in "camelCase" are now written in Python style. For example, ``getTokenPartOne`` has been changed to ``get_token_part_one``. The same is true for the class variables that point to the Flickr API URLs. For example, ``flickrHost`` became ``flickr_host``. ``send_multipart`` became a private method. The ``main`` method was removed. It only served as a simple example, which was obsoleted by the documentation. ``XMLNode`` The method ``parseXML`` has become ``parse``, since it can't parse anything but XML, so there is no need to state the obvious. Properties ``elementName`` and ``elementText`` have been renamed to ``name`` resp. ``text``. flickrapi-2.1.2/LICENSE.txt0000664000175000017500000000247412524326321016142 0ustar sybrensybren00000000000000Copyright (c) 2012, Sybren A. Stüvel This code is subject to the Python licence, as can be read on http://www.python.org/download/releases/3.3.0/license/ For those without an internet connection, here is a summary. When this summary clashes with the Python licence, the latter will be applied. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. flickrapi-2.1.2/flickrapi/0000775000175000017500000000000012613632407016260 5ustar sybrensybren00000000000000flickrapi-2.1.2/flickrapi/__init__.py0000664000175000017500000000561312613632310020367 0ustar sybrensybren00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """A FlickrAPI interface. The main functionality can be found in the `flickrapi.FlickrAPI` class. See `the FlickrAPI homepage`_ for more info. .. _`the FlickrAPI homepage`: http://stuvel.eu/projects/flickrapi """ from __future__ import unicode_literals __version__ = '2.1.2' __all__ = ('FlickrAPI', 'IllegalArgumentException', 'FlickrError', 'CancelUpload', 'LockingError', 'XMLNode', 'set_log_level', '__version__', 'SimpleCache', 'TokenCache', 'SimpleTokenCache', 'LockingTokenCache') __author__ = 'Sybren Stüvel' # Copyright (c) 2007 by the respective coders, see # http://www.stuvel.eu/projects/flickrapi # # This code is subject to the Python licence, as can be read on # http://www.python.org/download/releases/2.5.2/license/ # # For those without an internet connection, here is a summary. When this # summary clashes with the Python licence, the latter will be applied. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import logging # Import the core functionality into the flickrapi namespace from flickrapi.core import FlickrAPI from flickrapi.xmlnode import XMLNode from flickrapi.exceptions import IllegalArgumentException, \ FlickrError, CancelUpload, LockingError from flickrapi.cache import SimpleCache from flickrapi.tokencache import OAuthTokenCache, TokenCache, SimpleTokenCache, \ LockingTokenCache def set_log_level(level): """Sets the log level of the logger used by the FlickrAPI module. >>> import flickrapi >>> import logging >>> flickrapi.set_log_level(logging.INFO) """ import flickrapi.core import flickrapi.tokencache import flickrapi.contrib flickrapi.core.LOG.setLevel(level) flickrapi.tokencache.LOG.setLevel(level) flickrapi.contrib.LOG.setLevel(level) if __name__ == "__main__": print("Running doctests") import doctest doctest.testmod() print("Tests OK") flickrapi-2.1.2/flickrapi/html.py0000664000175000017500000000367312613631741017607 0ustar sybrensybren00000000000000# -*- coding: utf-8 -*- """HTML code.""" auth_okay_html = """ Python FlickrAPI authorization page

Flickr Authorization

Authorization of the application with Flickr was successful. You can now close this browser window, and return to the application.

Powered by Python FlickrAPI, by Sybren A. Stüvel.

""" import six if six.PY3: auth_okay_html = auth_okay_html.encode('utf-8') flickrapi-2.1.2/flickrapi/tokencache.py0000664000175000017500000002737612524326321020751 0ustar sybrensybren00000000000000 '''Persistent token cache management for the Flickr API''' import os.path import logging import time import sqlite3 from flickrapi.exceptions import LockingError, CacheDatabaseError from flickrapi.auth import FlickrAccessToken LOG = logging.getLogger(__name__) __all__ = ('SimpleTokenCache', 'OAuthTokenCache') class SimpleTokenCache(object): '''In-memory token cache.''' def __init__(self): self._token = None @property def token(self): return self._token @token.setter def token(self, token): self._token = token @token.deleter def token(self): self._token = None def forget(self): '''Removes the cached token''' del self.token class TokenCache(object): '''On-disk persistent token cache for a single application. The application is identified by the API key used. Per application multiple users are supported, with a single token per user. ''' def __init__(self, api_key, username=None): '''Creates a new token cache instance''' self.api_key = api_key self.username = username self.memory = {} self.path = os.path.join("~", ".flickr") def get_cached_token_path(self): """Return the directory holding the app data.""" return os.path.expanduser(os.path.join(self.path, self.api_key)) def get_cached_token_filename(self): """Return the full pathname of the cached token file.""" if self.username: filename = 'auth-%s.token' % self.username else: filename = 'auth.token' return os.path.join(self.get_cached_token_path(), filename) def get_cached_token(self): """Read and return a cached token, or None if not found. The token is read from the cached token file. """ # Only read the token once if self.username in self.memory: return self.memory[self.username] try: f = open(self.get_cached_token_filename(), "r") token = f.read() f.close() return token.strip() except IOError: return None def set_cached_token(self, token): """Cache a token for later use.""" # Remember for later use self.memory[self.username] = token path = self.get_cached_token_path() if not os.path.exists(path): os.makedirs(path) f = open(self.get_cached_token_filename(), "w") f.write(token) f.close() def forget(self): '''Removes the cached token''' if self.username in self.memory: del self.memory[self.username] filename = self.get_cached_token_filename() if os.path.exists(filename): os.unlink(filename) token = property(get_cached_token, set_cached_token, forget, "The cached token") class OAuthTokenCache(object): '''TokenCache for OAuth tokens; stores them in a SQLite database.''' DB_VERSION = 1 # Mapping from (api_key, lookup_key) to FlickrAccessToken object. RAM_CACHE = {} def __init__(self, api_key, lookup_key=''): '''Creates a new token cache instance''' assert lookup_key is not None self.api_key = api_key self.lookup_key = lookup_key self.path = os.path.expanduser(os.path.join("~", ".flickr")) self.filename = os.path.join(self.path, 'oauth-tokens.sqlite') if not os.path.exists(self.path): os.makedirs(self.path) self.create_table() def create_table(self): '''Creates the DB table, if it doesn't exist already.''' db = sqlite3.connect(self.filename) curs = db.cursor() # Check DB version curs.execute('CREATE TABLE IF NOT EXISTS oauth_cache_db_version (version int not null)') curs.execute('select version from oauth_cache_db_version') oauth_cache_db_version = curs.fetchone() if not oauth_cache_db_version: curs.execute('INSERT INTO oauth_cache_db_version (version) values (?)', str(self.DB_VERSION)) elif int(oauth_cache_db_version[0]) != self.DB_VERSION: raise CacheDatabaseError('Unsupported database version %s' % oauth_cache_db_version[0]) # Create cache table if it doesn't exist already curs.execute('''CREATE TABLE IF NOT EXISTS oauth_tokens ( api_key varchar(64) not null, lookup_key varchar(64) not null default '', oauth_token varchar(64) not null, oauth_token_secret varchar(64) not null, access_level varchar(6) not null, fullname varchar(255) not null, username varchar(255) not null, user_nsid varchar(64) not null, PRIMARY KEY(api_key, lookup_key))''') @property def token(self): '''Return the cached token for this API key, or None if not found.''' # Only read the token once if (self.api_key, self.lookup_key) in self.RAM_CACHE: return self.RAM_CACHE[self.api_key, self.lookup_key] db = sqlite3.connect(self.filename) curs = db.cursor() curs.execute('''SELECT oauth_token, oauth_token_secret, access_level, fullname, username, user_nsid FROM oauth_tokens WHERE api_key=? and lookup_key=?''', (self.api_key, self.lookup_key)) token_data = curs.fetchone() if token_data is None: return None return FlickrAccessToken(*token_data) @token.setter def token(self, token): """Cache a token for later use.""" assert isinstance(token, FlickrAccessToken) # Remember for later use self.RAM_CACHE[self.api_key, self.lookup_key] = token db = sqlite3.connect(self.filename) curs = db.cursor() curs.execute('''INSERT OR REPLACE INTO oauth_tokens (api_key, lookup_key, oauth_token, oauth_token_secret, access_level, fullname, username, user_nsid) values (?, ?, ?, ?, ?, ?, ?, ?)''', (self.api_key, self.lookup_key, token.token, token.token_secret, token.access_level, token.fullname, token.username, token.user_nsid) ) db.commit() @token.deleter def token(self): '''Removes the cached token''' # Delete from ram cache if (self.api_key, self.lookup_key) in self.RAM_CACHE: del self.RAM_CACHE[self.api_key, self.lookup_key] db = sqlite3.connect(self.filename) curs = db.cursor() curs.execute('''DELETE FROM oauth_tokens WHERE api_key=? and lookup_key=?''', (self.api_key, self.lookup_key)) db.commit() def forget(self): '''Removes the cached token''' del self.token class LockingTokenCache(TokenCache): '''Locks the token cache when reading or updating it, so that multiple processes can safely use the same API key. ''' def get_lock_name(self): '''Returns the filename of the lock.''' token_name = self.get_cached_token_filename() return '%s-lock' % token_name lock = property(get_lock_name) def get_pidfile_name(self): '''Returns the name of the pidfile in the lock directory.''' return os.path.join(self.lock, 'pid') pidfile_name = property(get_pidfile_name) def get_lock_pid(self): '''Returns the PID that is stored in the lock directory, or None if there is no such file. ''' filename = self.pidfile_name if not os.path.exists(filename): return None pidfile = open(filename) try: pid = pidfile.read() if pid: return int(pid) finally: pidfile.close() return None def acquire(self, timeout=60): '''Locks the token cache for this key and username. If the token cache is already locked, waits until it is released. Throws an exception when the lock cannot be acquired after ``timeout`` seconds. ''' # Check whether there is a PID file already with our PID in # it. lockpid = self.get_lock_pid() if lockpid == os.getpid(): LOG.debug('The lock is ours, continuing') return # Figure out the lock filename lock = self.get_lock_name() LOG.debug('Acquiring lock %s' % lock) # Try to obtain the lock start_time = time.time() while True: try: os.makedirs(lock) break except OSError: # If the path doesn't exist, the error isn't that it # can't be created because someone else has got the # lock. Just bail out then. if not os.path.exists(lock): LOG.error('Unable to acquire lock %s, aborting' % lock) raise if time.time() - start_time >= timeout: # Timeout has passed, bail out raise LockingError('Unable to acquire lock ' + '%s, aborting' % lock) # Wait for a bit, then try again LOG.debug('Unable to acquire lock, waiting') time.sleep(0.1) # Write the PID file LOG.debug('Lock acquired, writing our PID') pidfile = open(self.pidfile_name, 'w') try: pidfile.write('%s' % os.getpid()) finally: pidfile.close() def release(self): '''Unlocks the token cache for this key.''' # Figure out the lock filename lock = self.get_lock_name() if not os.path.exists(lock): LOG.warn('Trying to release non-existing lock %s' % lock) return # If the PID file isn't ours, abort. lockpid = self.get_lock_pid() if lockpid and lockpid != os.getpid(): raise LockingError(('Lock %s is NOT ours, but belongs ' + 'to PID %i, unable to release.') % (lock, lockpid)) LOG.debug('Releasing lock %s' % lock) # Remove the PID file and the lock directory pidfile = self.pidfile_name if os.path.exists(pidfile): os.remove(pidfile) os.removedirs(lock) def __del__(self): '''Cleans up any existing lock.''' # Figure out the lock filename lock = self.get_lock_name() if not os.path.exists(lock): return # If the PID file isn't ours, we're done lockpid = self.get_lock_pid() if lockpid and lockpid != os.getpid(): return # Release the lock self.release() def locked(method): '''Decorator, ensures the method runs in a locked cache.''' def locker(self, *args, **kwargs): self.acquire() try: return method(self, *args, **kwargs) finally: self.release() return locker @locked def get_cached_token(self): """Read and return a cached token, or None if not found. The token is read from the cached token file. """ return TokenCache.get_cached_token(self) @locked def set_cached_token(self, token): """Cache a token for later use.""" TokenCache.set_cached_token(self, token) @locked def forget(self): '''Removes the cached token''' TokenCache.forget(self) token = property(get_cached_token, set_cached_token, forget, "The cached token") flickrapi-2.1.2/flickrapi/xmlnode.py0000664000175000017500000000454712524326321020306 0ustar sybrensybren00000000000000 '''FlickrAPI uses its own in-memory XML representation, to be able to easily use the info returned from Flickr. There is no need to use this module directly, you'll get XMLNode instances from the FlickrAPI method calls. ''' import xml.dom.minidom __all__ = ('XMLNode', ) class XMLNode: """XMLNode -- generic class for holding an XML node""" def __init__(self): """Construct an empty XML node.""" self.name = "" self.text = "" self.attrib = {} self.xml = None def __setitem__(self, key, item): """Store a node's attribute in the attrib hash.""" self.attrib[key] = item def __getitem__(self, key): """Retrieve a node's attribute from the attrib hash.""" return self.attrib[key] @classmethod def __parse_element(cls, element, this_node): """Recursive call to process this XMLNode.""" this_node.name = element.nodeName # add element attributes as attributes to this node for i in range(element.attributes.length): an = element.attributes.item(i) this_node[an.name] = an.nodeValue for a in element.childNodes: if a.nodeType == xml.dom.Node.ELEMENT_NODE: child = XMLNode() # Ugly fix for an ugly bug. If an XML element # exists, it now overwrites the 'name' attribute # storing the XML element name. if not hasattr(this_node, a.nodeName) or a.nodeName == 'name': setattr(this_node, a.nodeName, []) # add the child node as an attrib to this node children = getattr(this_node, a.nodeName) children.append(child) cls.__parse_element(a, child) elif a.nodeType == xml.dom.Node.TEXT_NODE: this_node.text += a.nodeValue return this_node @classmethod def parse(cls, xml_str, store_xml=False): """Convert an XML string into a nice instance tree of XMLNodes. xml_str -- the XML to parse store_xml -- if True, stores the XML string in the root XMLNode.xml """ dom = xml.dom.minidom.parseString(xml_str) # get the root root_node = XMLNode() if store_xml: root_node.xml = xml_str return cls.__parse_element(dom.firstChild, root_node) flickrapi-2.1.2/flickrapi/cache.py0000664000175000017500000000606612613631741017705 0ustar sybrensybren00000000000000# -*- encoding: utf-8 -*- """Call result cache. Designed to have the same interface as the `Django low-level cache API`_. Heavily inspired (read: mostly copied-and-pasted) from the Django framework - thanks to those guys for designing a simple and effective cache! .. _`Django low-level cache API`: http://www.djangoproject.com/documentation/cache/#the-low-level-cache-api """ import threading import time class SimpleCache(object): """Simple response cache for FlickrAPI calls. This stores max 50 entries, timing them out after 120 seconds: >>> cache = SimpleCache(timeout=120, max_entries=50) """ def __init__(self, timeout=300, max_entries=200): self.storage = {} self.expire_info = {} self.lock = threading.RLock() self.default_timeout = timeout self.max_entries = max_entries self.cull_frequency = 3 def locking(method): """Method decorator, ensures the method call is locked""" def locked(self, *args, **kwargs): self.lock.acquire() try: return method(self, *args, **kwargs) finally: self.lock.release() return locked @locking def get(self, key, default=None): """Fetch a given key from the cache. If the key does not exist, return default, which itself defaults to None. """ now = time.time() exp = self.expire_info.get(repr(key)) if exp is None: return default elif exp < now: self.delete(repr(key)) return default return self.storage[repr(key)] @locking def set(self, key, value, timeout=None): """Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. """ if len(self.storage) >= self.max_entries: self.cull() if timeout is None: timeout = self.default_timeout self.storage[repr(key)] = value self.expire_info[repr(key)] = time.time() + timeout @locking def delete(self, key): """Deletes a key from the cache, failing silently if it doesn't exist.""" if key in self.storage: del self.storage[key] if key in self.expire_info: del self.expire_info[key] @locking def has_key(self, key): """Returns True if the key is in the cache and has not expired.""" return self.get(repr(key)) is not None @locking def __contains__(self, key): """Returns True if the key is in the cache and has not expired.""" return self.has_key(repr(key)) @locking def cull(self): """Reduces the number of cached items""" doomed = [k for (i, k) in enumerate(self.storage) if i % self.cull_frequency == 0] for k in doomed: self.delete(k) @locking def __len__(self): """Returns the number of cached items -- they might be expired though. """ return len(self.storage) flickrapi-2.1.2/flickrapi/call_builder.py0000664000175000017500000000300312613631741021247 0ustar sybrensybren00000000000000 class CallBuilder(object): """Builds a method name for FlickrAPI calls. >>> class Faker(object): ... def do_flickr_call(self, method_name, **kwargs): ... print('%s(%s)' % (method_name, kwargs)) ... >>> c = CallBuilder(Faker()) >>> c.photos CallBuilder('flickr.photos') >>> c.photos.getInfo CallBuilder('flickr.photos.getInfo') >>> c.photos.getInfo(photo_id='1234') flickr.photos.getInfo({'photo_id': '1234'}) """ def __init__(self, flickrapi_object, method_name='flickr'): self.flickrapi_object = flickrapi_object self.method_name = method_name def __getattr__(self, name): """Returns a CallBuilder for the given name.""" # Refuse to act as a proxy for unimplemented special methods if name.startswith('_'): raise AttributeError("No such attribute '%s'" % name) return self.__class__(self.flickrapi_object, self.method_name + '.' + name) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.method_name) def __call__(self, **kwargs): return self.flickrapi_object.do_flickr_call(self.method_name, **kwargs) if __name__ == '__main__': import doctest doctest.testmod() class Faker(object): def do_flickr_call(self, method_name, **kwargs): print('%s(%s)' % (method_name, kwargs)) c = CallBuilder(Faker()) c.photos.getInfo(photo_id='1234') c.je.moeder.heeft.een.moeder(photo_id='1234') flickrapi-2.1.2/flickrapi/shorturl.py0000664000175000017500000000316612524326321020516 0ustar sybrensybren00000000000000# -*- coding: utf-8 -*- '''Helper functions for the short http://fli.kr/p/... URL notation. Photo IDs can be converted to and from Base58 short IDs, and a short URL can be generated from a photo ID. The implementation of the encoding and decoding functions is based on the posts by stevefaeembra and Kohichi on http://www.flickr.com/groups/api/discuss/72157616713786392/ ''' import six __all__ = ['encode', 'decode', 'url', 'SHORT_URL'] ALPHABET = u'123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' ALPHALEN = len(ALPHABET) SHORT_URL = u'http://flic.kr/p/%s' def encode(photo_id): '''encode(photo_id) -> short id >>> encode(u'4325695128') '7Afjsu' >>> encode(u'2811466321') '5hruZg' ''' photo_id = int(photo_id) encoded = u'' while photo_id >= ALPHALEN: div, mod = divmod(photo_id, ALPHALEN) encoded = ALPHABET[mod] + encoded photo_id = int(div) encoded = ALPHABET[photo_id] + encoded return encoded def decode(short_id): '''decode(short id) -> photo id >>> decode(u'7Afjsu') '4325695128' >>> decode(u'5hruZg') '2811466321' ''' decoded = 0 multi = 1 for i in six.moves.range(len(short_id)-1, -1, -1): char = short_id[i] index = ALPHABET.index(char) decoded += multi * index multi *= len(ALPHABET) return six.text_type(decoded) def url(photo_id): '''url(photo id) -> short url >>> url(u'4325695128') 'http://flic.kr/p/7Afjsu' >>> url(u'2811466321') 'http://flic.kr/p/5hruZg' ''' short_id = encode(photo_id) return SHORT_URL % short_id flickrapi-2.1.2/flickrapi/core.py0000664000175000017500000007045512613632236017575 0ustar sybrensybren00000000000000"""The core Python FlickrAPI module. This module contains most of the FlickrAPI code. It is well tested and documented. """ from __future__ import print_function import logging import six import functools from . import tokencache, auth from flickrapi.xmlnode import XMLNode from flickrapi.exceptions import * from flickrapi.cache import SimpleCache from flickrapi.call_builder import CallBuilder LOG = logging.getLogger(__name__) def make_bytes(dictionary): """Encodes all Unicode strings in the dictionary to UTF-8 bytes. Converts all other objects to regular bytes. Returns a copy of the dictionary, doesn't touch the original. """ result = {} for (key, value) in six.iteritems(dictionary): # Keep binary data as-is. if isinstance(value, six.binary_type): result[key] = value continue # If it's not a string, convert it to one. if not isinstance(value, six.text_type): value = six.text_type(value) result[key] = value.encode('utf-8') return result def debug(method): """Method decorator for debugging method calls. Using this automatically sets the log level to DEBUG. """ def debugged(*args, **kwargs): LOG.debug("Call: %s(%s, %s)" % (method.__name__, args, kwargs)) result = method(*args, **kwargs) LOG.debug("\tResult: %s" % result) return result return debugged # REST parsers, {format: (parser_method, request format), ...}. Fill by using the # @rest_parser(format) function decorator rest_parsers = {} def rest_parser(parsed_format, request_format='rest'): """Method decorator, use this to mark a function as the parser for REST as returned by Flickr. """ def decorate_parser(method): rest_parsers[parsed_format] = (method, request_format) return method return decorate_parser def require_format(required_format): """Method decorator, raises a ValueError when the decorated method is called if the default format is not set to ``required_format``. """ def decorator(method): @functools.wraps(method) def decorated(self, *args, **kwargs): # If everything is okay, call the method if self.default_format == required_format: return method(self, *args, **kwargs) # Otherwise raise an exception msg = 'Function %s requires that you use ' \ 'ElementTree ("etree") as the communication format, ' \ 'while the current format is set to "%s".' raise ValueError(msg % (method.func_name, self.default_format)) return decorated return decorator def authenticator(method): """Method wrapper, assumed the wrapped method has a 'perms' parameter. Only calls the wrapped method if the token cache doesn't contain a valid token. """ @functools.wraps(method) def decorated(self, *args, **kwargs): assert isinstance(self, FlickrAPI) if 'perms' in kwargs: perms = kwargs['perms'] elif len(args): perms = args[0] else: perms = 'read' if self.token_valid(perms=perms): # Token is valid, and for the expected permissions, so no # need to continue authentication. return method(self, *args, **kwargs) return decorated 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): """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) """ 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 '') self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache) 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, **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'], **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, **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) # 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, **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. 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, **kwargs) def replace(self, filename, photo_id, fileobj=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. """ 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, **kwargs) def _upload_to_form(self, form_url, filename, fileobj=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) def token_valid(self, perms='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='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='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 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='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.func_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) flickrapi-2.1.2/flickrapi/contrib.py0000664000175000017500000000353212613631741020275 0ustar sybrensybren00000000000000"""Contributed FlickrAPI extensions. These FlickrAPI extensions have been contributed by other developers. They may not be as thoroughly tested as the core Python FlickrAPI modules. """ import logging import threading import six http_client = six.moves.http_client from flickrapi import core LOG = logging.getLogger(__name__) class PersistentFlickrAPI(core.FlickrAPI): """FlickrAPI that uses persistent HTTP connections via httplib. The HTTP connection is persisted in a thread-local way. Note that it may be possible that the connection was closed for some reason, in which case a Flickr call will fail. The next call will try to re-establish the connection. Re-trying the call in such a case is the responsibility of the caller. """ def __init__(self, *args, **kwargs): core.FlickrAPI.__init__(self, *args, **kwargs) # Thread-local HTTPConnection, see _http_post self.thr = threading.local() def _http_post(self, post_data): """Performs a HTTP POST call to the Flickr REST URL. Raises a httplib.ImproperConnectionState exception when the connection was closed unexpectedly. """ # Thread-local persistent connection try: if 'conn' not in self.thr.__dict__: self.thr.conn = http_client.HTTPConnection(self.flickr_host) LOG.info("connection opened to %s" % self.flickr_host, 3) self.thr.conn.request("POST", self.flickr_rest_form, post_data, {"Content-Type": "application/x-www-form-urlencoded"}) reply = self.thr.conn.getresponse().read() except http_client.ImproperConnectionState as e: LOG.error("connection error: %s" % e, 3) self.thr.conn.close() del self.thr.conn raise return reply flickrapi-2.1.2/flickrapi/sockutil.py0000664000175000017500000000360012524326321020462 0ustar sybrensybren00000000000000# -*- coding: utf-8 -*- '''Utility functions for working with network sockets. Created by Sybren A. Stüvel for Chess IX, Haarlem, The Netherlands. Licensed under the Apache 2 license. ''' import socket import os import logging LOG = logging.getLogger(__name__) def is_bindable(address): '''Tries to bind a listening socket to the given address. Returns True if this works, False otherwise. In any case the socket is closed before returning. ''' sock = None try: sock = socket.socket() if os.name == 'posix': sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(address) sock.close() except IOError as ex: LOG.debug('is_bindable(%s): %s', address, ex) if sock: sock.close() return False return True def is_reachable(address): '''Tries to connect to the given address using a TCP socket. Returns True iff this is possible. Always closes the connection before returning. ''' try: sock = socket.create_connection(address, 1.0) sock.close() except IOError: return False return True def find_free_port(start_address): '''Incrementally searches for a TCP port that can be bound to. :param start_address: (hostname, portnr) tuple defining the host to bind and the portnumber to start the search :type start_address: tuple :return: the address containing the first port number that was found to be free. :rtype: tuple of (hostname, port_nr) ''' (hostname, port_nr) = start_address LOG.debug('find_free_port(%s)', start_address) while not is_bindable((hostname, port_nr)): LOG.debug('find_free_port: %i is not bindable, trying next port', port_nr) port_nr += 1 return hostname, port_nr flickrapi-2.1.2/flickrapi/auth.py0000664000175000017500000004245612613631741017606 0ustar sybrensybren00000000000000"""OAuth support functionality """ from __future__ import unicode_literals # Try importing the Python 3 packages first, falling back to 2.x packages when it fails. try: from http import server as http_server except ImportError: import BaseHTTPServer as http_server try: from urllib import parse as urllib_parse except ImportError: import urlparse as urllib_parse import logging import random import os.path import sys import webbrowser import six from requests_toolbelt import MultipartEncoder import requests from requests_oauthlib import OAuth1 from . import sockutil, exceptions, html from .exceptions import FlickrError class OAuthTokenHTTPHandler(http_server.BaseHTTPRequestHandler): def do_GET(self): # /?oauth_token=72157630789362986-5405f8542b549e95&oauth_verifier=fe4eac402339100e qs = urllib_parse.urlsplit(self.path).query url_vars = urllib_parse.parse_qs(qs) oauth_token = url_vars['oauth_token'][0] oauth_verifier = url_vars['oauth_verifier'][0] if six.PY2: self.server.oauth_token = oauth_token.decode('utf-8') self.server.oauth_verifier = oauth_verifier.decode('utf-8') else: self.server.oauth_token = oauth_token self.server.oauth_verifier = oauth_verifier assert(isinstance(self.server.oauth_token, six.string_types)) assert(isinstance(self.server.oauth_verifier, six.string_types)) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.auth_okay_html) class OAuthTokenHTTPServer(http_server.HTTPServer): """HTTP server on a random port, which will receive the OAuth verifier.""" def __init__(self): self.log = logging.getLogger('%s.%s' % (self.__class__.__module__, self.__class__.__name__)) self.local_addr = self.listen_port() self.log.info('Creating HTTP server at %s', self.local_addr) http_server.HTTPServer.__init__(self, self.local_addr, OAuthTokenHTTPHandler) self.oauth_verifier = None def listen_port(self): """Returns the hostname and TCP/IP port number to listen on. By default finds a random free port between 1100 and 20000. """ # Find a random free port local_addr = ('localhost', int(random.uniform(1100, 20000))) self.log.debug('Finding free port starting at %s', local_addr) # return local_addr return sockutil.find_free_port(local_addr) def wait_for_oauth_verifier(self, timeout=None): """Starts the HTTP server, waits for the OAuth verifier.""" if self.oauth_verifier is None: self.timeout = timeout self.handle_request() if self.oauth_verifier: self.log.info('OAuth verifier: %s' % self.oauth_verifier) return self.oauth_verifier @property def oauth_callback_url(self): return 'http://localhost:%i/' % (self.local_addr[1], ) class FlickrAccessToken(object): """Flickr access token. Contains the token, token secret, and the user's full name, username and NSID. """ levels = ('read', 'write', 'delete') def __init__(self, token, token_secret, access_level, fullname=u'', username=u'', user_nsid=u''): assert isinstance(token, six.text_type), 'token should be unicode text' assert isinstance(token_secret, six.text_type), 'token_secret should be unicode text' assert isinstance(access_level, six.text_type), 'access_level should be unicode text, is %r' % type(access_level) assert isinstance(fullname, six.text_type), 'fullname should be unicode text' assert isinstance(username, six.text_type), 'username should be unicode text' assert isinstance(user_nsid, six.text_type), 'user_nsid should be unicode text' access_level = access_level.lower() assert access_level in self.levels, 'access_level should be one of %r' % (self.levels, ) self.token = token self.token_secret = token_secret self.access_level = access_level self.fullname = fullname self.username = username self.user_nsid = user_nsid def __str__(self): return six.text_type(self).encode('utf-8') def __unicode__(self): return 'FlickrAccessToken(token=%s, fullname=%s, username=%s, user_nsid=%s)' % ( self.token, self.fullname, self.username, self.user_nsid) def __repr__(self): return str(self) def has_level(self, access_level): """Returns True iff the token's access level implies the given access level.""" my_idx = self.levels.index(self.access_level) q_idx = self.levels.index(access_level) return q_idx <= my_idx class OAuthFlickrInterface(object): """Interface object for handling OAuth-authenticated calls to Flickr.""" REQUEST_TOKEN_URL = "https://www.flickr.com/services/oauth/request_token" AUTHORIZE_URL = "https://www.flickr.com/services/oauth/authorize" ACCESS_TOKEN_URL = "https://www.flickr.com/services/oauth/access_token" def __init__(self, api_key, api_secret, oauth_token=None): self.log = logging.getLogger('%s.%s' % (self.__class__.__module__, self.__class__.__name__)) assert isinstance(api_key, six.text_type), 'api_key must be unicode string' assert isinstance(api_secret, six.text_type), 'api_secret must be unicode string' token = None secret = None if oauth_token.token: token = oauth_token.token.token secret = oauth_token.token.token_secret self.oauth = OAuth1(api_key, api_secret, token, secret, signature_type='auth_header') self.oauth_token = oauth_token self.auth_http_server = None self.requested_permissions = None @property def key(self): """Returns the OAuth key""" return self.oauth.client.client_key @property def resource_owner_key(self): """Returns the OAuth resource owner key""" return self.oauth.client.resource_owner_key @resource_owner_key.setter def resource_owner_key(self, new_key): """Stores the OAuth resource owner key""" self.oauth.client.resource_owner_key = new_key @property def resource_owner_secret(self): """Returns the OAuth resource owner secret""" return self.oauth.client.resource_owner_secret @resource_owner_secret.setter def resource_owner_secret(self, new_secret): """Stores the OAuth resource owner secret""" self.oauth.client.resource_owner_secret = new_secret @property def verifier(self): """Returns the OAuth verifier.""" return self.oauth.client.verifier @verifier.setter def verifier(self, new_verifier): """Sets the OAuth verifier""" assert isinstance(new_verifier, six.text_type), 'verifier must be unicode text type' self.oauth.client.verifier = new_verifier @property def token(self): return self.oauth_token @token.setter def token(self, new_token): if new_token is None: self.oauth_token = None self.oauth.client.resource_owner_key = None self.oauth.client.resource_owner_secret = None self.oauth.client.verifier = None self.requested_permissions = None return assert isinstance(new_token, FlickrAccessToken), new_token self.oauth_token = new_token self.oauth.client.resource_owner_key = new_token.token self.oauth.client.resource_owner_secret = new_token.token_secret self.oauth.client.verifier = None self.requested_permissions = new_token.access_level def _find_cache_dir(self): """Returns the appropriate directory for the HTTP cache.""" if sys.platform.startswith('win'): return os.path.expandvars('%APPDATA%/flickrapi/cache') return os.path.expanduser('~/.flickrapi/cache') def do_request(self, url, params=None): """Performs the HTTP request, signed with OAuth. @return: the response content """ req = requests.post(url, params=params, auth=self.oauth, headers={'Connection': 'close'}) # check the response headers / status code. if req.status_code != 200: self.log.error('do_request: Status code %i received, content:', req.status_code) for part in req.text.split('&'): self.log.error(' %s', urllib_parse.unquote(part)) raise exceptions.FlickrError('do_request: Status code %s received' % req.status_code) return req.content def do_upload(self, filename, url, params=None, fileobj=None): """Performs a file upload to the given URL with the given parameters, signed with OAuth. @return: the response content """ # work-around to allow non-ascii characters in file name # Flickr doesn't store the name but does use it as a default title if 'title' not in params: params['title'] = os.path.basename(filename) # work-around for Flickr expecting 'photo' to be excluded # from the oauth signature: # 1. create a dummy request without 'photo' # 2. create real request and use auth headers from the dummy one dummy_req = requests.Request('POST', url, data=params, auth=self.oauth, headers={'Connection': 'close'}) prepared = dummy_req.prepare() headers = prepared.headers self.log.debug('do_upload: prepared headers = %s', headers) if not fileobj: fileobj = open(filename, 'rb') params['photo'] = ('dummy name', fileobj) m = MultipartEncoder(fields=params) auth = {'Authorization': headers.get('Authorization'), 'Content-Type' : m.content_type, 'Connection' : 'close'} self.log.debug('POST %s', auth) req = requests.post(url, data=m, headers=auth) # check the response headers / status code. if req.status_code != 200: self.log.error('do_upload: Status code %i received, content:', req.status_code) for part in req.text.split('&'): self.log.error(' %s', urllib_parse.unquote(part)) raise exceptions.FlickrError('do_upload: Status code %s received' % req.status_code) return req.content @staticmethod def parse_oauth_response(data): """Parses the data string as OAuth response, returning it as a dict. The keys and values of the dictionary will be text strings (i.e. not binary strings). """ if isinstance(data, six.binary_type): data = data.decode('utf-8') qsl = urllib_parse.parse_qsl(data) resp = {} for key, value in qsl: resp[key] = value return resp def _start_http_server(self): """Starts the HTTP server, if it wasn't started already.""" if self.auth_http_server is not None: return self.auth_http_server = OAuthTokenHTTPServer() def _stop_http_server(self): """Stops the HTTP server, if one was started.""" if self.auth_http_server is None: return self.auth_http_server = None 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.log.debug('get_request_token(oauth_callback=%s):', oauth_callback) if oauth_callback is None: self._start_http_server() oauth_callback = self.auth_http_server.oauth_callback_url params = { 'oauth_callback': oauth_callback, } token_data = self.do_request(self.REQUEST_TOKEN_URL, params) self.log.debug('Token data: %s', token_data) # Parse the token data request_token = self.parse_oauth_response(token_data) self.log.debug('Request token: %s', request_token) self.oauth.client.resource_owner_key = request_token['oauth_token'] self.oauth.client.resource_owner_secret = request_token['oauth_token_secret'] def auth_url(self, perms='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, use open_browser_for_authentication instead. """ if self.oauth.client.resource_owner_key is None: raise FlickrError('No resource owner key set, you probably forgot to call get_request_token(...)') if perms not in ('read', 'write', 'delete'): raise ValueError('Invalid parameter perms=%r' % perms) self.requested_permissions = perms return "%s?oauth_token=%s&perms=%s" % (self.AUTHORIZE_URL, self.oauth.client.resource_owner_key, perms) def auth_via_browser(self, perms='read'): """Opens the webbrowser to authenticate the given request request_token, sets the verifier. Use this method in stand-alone apps. In webapps, use auth_url(...) instead, and redirect the user to the returned URL. Updates the given request_token by setting the OAuth verifier. """ # The HTTP server may have been started already, but we're not sure. Just start # it if it needs to be started. self._start_http_server() url = self.auth_url(perms) if not webbrowser.open_new_tab(url): raise exceptions.FlickrError('Unable to open a browser to visit %s' % url) self.verifier = self.auth_http_server.wait_for_oauth_verifier() # We're now done with the HTTP server, so close it down again. self._stop_http_server() def auth_via_console(self, perms='read'): """Waits for the user to authenticate the app, sets the verifier. Use this method in stand-alone apps. In webapps, use auth_url(...) instead, and redirect the user to the returned URL. Updates the given request_token by setting the OAuth verifier. """ # The HTTP server may have been started already, but we're not sure. Just start # it if it needs to be started. self._start_http_server() auth_url = self.auth_url(perms=perms) print("Go to the following link in your browser to authorize this application:") print(auth_url) print() self.verifier = self.auth_http_server.wait_for_oauth_verifier() # We're now done with the HTTP server, so close it down again. self._stop_http_server() def get_access_token(self): """Exchanges the request token for an access token. Also stores the access token in 'self' for easy authentication of subsequent calls. @return: Access token, a FlickrAccessToken object. """ if self.oauth.client.resource_owner_key is None: raise FlickrError('No resource owner key set, you probably forgot to call get_request_token(...)') if self.oauth.client.verifier is None: raise FlickrError('No token verifier set, you probably forgot to set %s.verifier' % self) if self.requested_permissions is None: raise FlickrError('Requested permissions are unknown.') content = self.do_request(self.ACCESS_TOKEN_URL) #parse the response access_token_resp = self.parse_oauth_response(content) self.oauth_token = FlickrAccessToken(access_token_resp['oauth_token'], access_token_resp['oauth_token_secret'], self.requested_permissions, access_token_resp.get('fullname', ''), access_token_resp['username'], access_token_resp['user_nsid']) self.oauth.client.resource_owner_key = access_token_resp['oauth_token'] self.oauth.client.resource_owner_secret = access_token_resp['oauth_token_secret'] self.oauth.client.verifier = None return self.oauth_token flickrapi-2.1.2/flickrapi/exceptions.py0000664000175000017500000000212212613631741021010 0ustar sybrensybren00000000000000"""Exceptions used by the FlickrAPI module.""" class IllegalArgumentException(ValueError): """Raised when a method is passed an illegal argument. More specific details will be included in the exception message when thrown. """ class FlickrError(Exception): """Raised when a Flickr method fails. More specific details will be included in the exception message when thrown. """ def __init__(self, message, code=None): Exception.__init__(self, message) if code is None: self.code = None else: self.code = int(code) class CancelUpload(Exception): """Raise this exception in an upload/replace callback function to abort the upload. """ class LockingError(Exception): """Raised when TokenCache cannot acquire a lock within the timeout period, or when a lock release is attempted when the lock does not belong to this process. """ class CacheDatabaseError(FlickrError): """Raised when the OAuth token cache database is corrupted or otherwise unusable. """ flickrapi-2.1.2/setup.cfg0000664000175000017500000000007312613632407016135 0ustar sybrensybren00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 flickrapi-2.1.2/README.txt0000664000175000017500000000142012524326321016003 0ustar sybrensybren00000000000000====================================================================== Python FlickrAPI ====================================================================== Most of the info can be found in the 'doc' directory or on http://stuvel.eu/flickrapi To install the Python Flickr API module from source, run:: python setup.py install To install the latest version from PyPi: pip install flickrapi For development, install extra dependencies using:: pip install -r requirements.txt then run ``nosetest`` in the top-level directory. Generating the documentation -------------------------------------------------- The documentation is written in Sphynx. Execute 'make html' in the doc directory to generate the HTML pages. They can then be found in doc/_build/html. flickrapi-2.1.2/setup.py0000664000175000017500000000414412613632260016026 0ustar sybrensybren00000000000000#!/usr/bin/env python '''Python distutils install script. Run with "python setup.py install" to install FlickrAPI ''' from __future__ import print_function __author__ = 'Sybren A. Stuvel' __version__ = '2.1.2' # Check the Python version import sys (major, minor) = sys.version_info[:2] if (major, minor) < (2, 7) or (major == 3 and minor < 3): raise SystemExit("Sorry, Python 2.7, or 3.3 or newer required") from setuptools import setup data = { 'name': 'flickrapi', 'version': __version__, 'author': 'Sybren A. Stuvel', 'author_email': 'sybren@stuvel.eu', 'maintainer': 'Sybren A. Stuvel', 'maintainer_email': 'sybren@stuvel.eu', 'url': 'http://stuvel.eu/projects/flickrapi', 'description': 'The Python interface to the Flickr API', 'long_description': 'The easiest to use, most complete, and ' 'most actively developed Python interface to the Flickr API.' 'It includes support for authorized and non-authorized ' 'access, uploading and replacing photos, and all Flickr API ' 'functions.', 'packages': ['flickrapi'], 'package_data': {'flickrapi': ['../LICENSE.txt', '../README.txt', '../UPGRADING.txt']}, 'license': 'Python', 'classifiers': [ 'Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: Python License (CNRI Python License)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries :: Python Modules', ], 'install_requires': [ 'six>=1.5.2', 'requests>=2.2.1', 'requests_oauthlib>=0.4.0', 'requests_toolbelt>=0.3.1', ], 'extras_require': { 'ElementTree': ["elementtree>=1.2.6"], 'Sphinx': ["sphinx>=1.1.3"], }, 'zip_safe': True, 'test_suite': 'tests', } setup(**data) flickrapi-2.1.2/PKG-INFO0000664000175000017500000000202312613632407015406 0ustar sybrensybren00000000000000Metadata-Version: 1.1 Name: flickrapi Version: 2.1.2 Summary: The Python interface to the Flickr API Home-page: http://stuvel.eu/projects/flickrapi Author: Sybren A. Stuvel Author-email: sybren@stuvel.eu License: Python Description: The easiest to use, most complete, and most actively developed Python interface to the Flickr API.It includes support for authorized and non-authorized access, uploading and replacing photos, and all Flickr API functions. Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Python License (CNRI Python License) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries :: Python Modules flickrapi-2.1.2/flickrapi.egg-info/0000775000175000017500000000000012613632407017752 5ustar sybrensybren00000000000000flickrapi-2.1.2/flickrapi.egg-info/requires.txt0000664000175000017500000000020712613632406022350 0ustar sybrensybren00000000000000six>=1.5.2 requests>=2.2.1 requests_oauthlib>=0.4.0 requests_toolbelt>=0.3.1 [ElementTree] elementtree>=1.2.6 [Sphinx] sphinx>=1.1.3 flickrapi-2.1.2/flickrapi.egg-info/zip-safe0000664000175000017500000000000112524326470021403 0ustar sybrensybren00000000000000 flickrapi-2.1.2/flickrapi.egg-info/SOURCES.txt0000664000175000017500000000103612613632407021636 0ustar sybrensybren00000000000000README.txt setup.py flickrapi/__init__.py flickrapi/auth.py flickrapi/cache.py flickrapi/call_builder.py flickrapi/contrib.py flickrapi/core.py flickrapi/exceptions.py flickrapi/html.py flickrapi/shorturl.py flickrapi/sockutil.py flickrapi/tokencache.py flickrapi/xmlnode.py flickrapi.egg-info/PKG-INFO flickrapi.egg-info/SOURCES.txt flickrapi.egg-info/dependency_links.txt flickrapi.egg-info/requires.txt flickrapi.egg-info/top_level.txt flickrapi.egg-info/zip-safe flickrapi/../LICENSE.txt flickrapi/../README.txt flickrapi/../UPGRADING.txtflickrapi-2.1.2/flickrapi.egg-info/PKG-INFO0000664000175000017500000000202312613632406021043 0ustar sybrensybren00000000000000Metadata-Version: 1.1 Name: flickrapi Version: 2.1.2 Summary: The Python interface to the Flickr API Home-page: http://stuvel.eu/projects/flickrapi Author: Sybren A. Stuvel Author-email: sybren@stuvel.eu License: Python Description: The easiest to use, most complete, and most actively developed Python interface to the Flickr API.It includes support for authorized and non-authorized access, uploading and replacing photos, and all Flickr API functions. Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Python License (CNRI Python License) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries :: Python Modules flickrapi-2.1.2/flickrapi.egg-info/top_level.txt0000664000175000017500000000001212613632406022474 0ustar sybrensybren00000000000000flickrapi flickrapi-2.1.2/flickrapi.egg-info/dependency_links.txt0000664000175000017500000000000112613632406024017 0ustar sybrensybren00000000000000