pax_global_header00006660000000000000000000000064132356253260014521gustar00rootroot0000000000000052 comment=d293ab41099bfe1707bd40b47419915979f68584 flickrapi-version-2.4/000077500000000000000000000000001323562532600150155ustar00rootroot00000000000000flickrapi-version-2.4/.coveragerc000066400000000000000000000001471323562532600171400ustar00rootroot00000000000000# .coveragerc to control coverage.py [run] source = flickrapi omit = setup.py .tox/* doc/* flickrapi-version-2.4/.gitignore000066400000000000000000000002021323562532600167770ustar00rootroot00000000000000*.pyc *.pyo .*.swp /.cache/ /build/ /dist/ /doc/index.html /doc/apidoc/ /.coverage /flickrapi.egg-info/ /doc/_build /.idea/ /.tox flickrapi-version-2.4/.travis.yml000066400000000000000000000015321323562532600171270ustar00rootroot00000000000000sudo: false language: python cache: pip # Python versions specified to make tox environment 'py35' and 'py36' work. # See: https://github.com/travis-ci/travis-ci/issues/4794 # Environment changes have to be manually synced with 'tox.ini'. # See: https://github.com/travis-ci/travis-ci/issues/3024 matrix: include: - python: '2.7' env: TOXENV=py27 - python: '3.4' env: TOXENV=py34 - python: '3.5' env: TOXENV=py35 - python: '3.6' env: TOXENV=py36 - python: 'pypy' env: TOXENV=pypy - python: 3.5 env: TOXENV=qa,doc install: - pip install --upgrade setuptools pip tox-travis coveralls script: - tox -v after_success: # Coveralls submission only for py36 environment, because of being the only # one that executes doctest-modules testing, according to tox.ini. - if [ ${TOXENV} = "py36" ]; then coveralls; fi flickrapi-version-2.4/CHANGELOG.md000066400000000000000000000322461323562532600166350ustar00rootroot00000000000000Python FlickrAPI Changelog ========================== This is the changelog of the Python FlickrAPI package. All changes were made by Sybren A. Stüvel, unless noted otherwise. Version 2.4: released 2018-02-04 -------------------------------- - Fix parameter naming conflict: 'method_name' parameter from `do_flickr_call()` clashed with 'method_name' parameter required by the Flickr API method `flickr.reflection.getMethodInfo` (since [952c133](https://github.com/sybrenstuvel/flickrapi/commit/952c133) - Fixed typos [[ Pull request #86 ]](https://github.com/sybrenstuvel/flickrapi/pull/86) (Daniel Hohard) - Fixed infinite recursion calling `FlickrAccessToken.str()` - Drop support for EOL Python 3.3 (by hugovk) - Use latest pip and enable cache on travis [[ Pull request #100 ]](https://github.com/sybrenstuvel/flickrapi/pull/100) (Thijs Triemstra) - Properly include text files in `setup.py` [[ Pull request #101 ]](https://github.com/sybrenstuvel/flickrapi/pull/101) (Thijs Triemstra) Version 2.3: released 2017-05-08 -------------------------------- - Include README.md and CHANGELOG.md in the package data. - Removed flickrapi/contrib.py, as a persistent connection is now managed (much better) by requests (since [cdeb6ffea26](https://github.com/sybrenstuvel/flickrapi/commit/cdeb6ffea26)). - Late-import module `webbrowser`, only when needed (Thijs Triemstra) [[ Pull request #78 ]](https://github.com/sybrenstuvel/flickrapi/pull/78) - Perform explicit commit after inserting auth token (Joshua Hunter) [[ Bug report #75 ]](https://github.com/sybrenstuvel/flickrapi/pull/75) [[ Pull request #76 ]](https://github.com/sybrenstuvel/flickrapi/pull/76) - Configured Tox & Travis to test on Python 3.6 as well. - Timeout for API calls. [[ Feature Request #27 ]](https://github.com/sybrenstuvel/flickrapi/issues/27) - Late-import SQLite3 to allow running on Heroku (and other systems). [[ Feature Request #81 ]](https://github.com/sybrenstuvel/flickrapi/issues/81) - Fixed using obsolete `func_name` attribute. [[ Bug report #80 ]](https://github.com/sybrenstuvel/flickrapi/issues/80) Version 2.2.1: released 2017-01-15 ---------------------------------- - Converted the old changelog (on [stuvel.eu](https://stuvel.eu/flickrapi/changelog)) from HTML to MarkDown, and added that to this file. - Added some missing changes to the version 2.2 section. Version 2.2: released 2017-01-15 -------------------------------- - Added this changelog file. - Added explicit support for Python 3.5. - Moved from a Mercurial repository at BitBucket to a [Git repository at GitHub](https://github.com/sybrenstuvel/flickrapi/). - Mocking some calls to Flickr, so that unit tests can run without requiring the user to authenticate via the browser. This also prevents the upload of the test photo. - More serious testing, using py.test and Tox to test on all support versions of Python. - Automated builds are performed with Travis-CI. - Make flickrapi token storage directory configurable. [[ Feature request #68 ]](https://github.com/sybrenstuvel/flickrapi/issues/68) - Put requests in a session to benefit from connection reuse (Alexandre L). - When uploading a photo, send the title as UTF8 - Sort many photosets with 'flickr.photosets.orderSets' failed [[ Bug report #74]](https://github.com/sybrenstuvel/flickrapi/issues/74) - Converted requested_permissions to unicode and ported authentication example to Python 3 (Michael Klich). Version 2.1.2: released 2015-10-27 ---------------------------------- - Added error checking (and raising of `FlickrError`) from the parsed-json handler Version 2.1.1: released 2015-08-03 ---------------------------------- - Bumped version to 2.1.1 due to some mistake with pypi Version 2.1: released 2015-08-03 -------------------------------- - solved issue with `autenticate_console()` [[ Bug report #58 ]](https://github.com/sybrenstuvel/flickrapi/issues/58) - Using POST instead of GET [[ Bug report #59 ]](https://github.com/sybrenstuvel/flickrapi/issues/59) - Fix: use req.text for text-based interpretation of HTTP response. Version 2.0.1: released 2014-12-18 ---------------------------------- - Lowered dependency versions to those shipped with Ubuntu 14.04 LTS - Added Python 3.3 to the list of supported versions, rejecting Python <=2.6 and <=3.2. - Allow non-ascii characters in file names (Jim Easterbrook) Version 2.0: released 2014-11-06 -------------------------------- - Major revision; now uses OAuth to interface with Flickr. Combines work of Sybren A. Stüvel, Jim Easterbrook, Thai Nguyen, Nick Loadholtes and Bengt. Version 1.4.4: released 2014-06-18 -------------------------------- - Changed default API URL to use HTTPS (Joseph Hui). - PEP 8 compliance (Luar Roji). - Replaced Distribute with Setuptools. Versions 1.4.1 .. 1.4.3: unreleased ----------------------------------- - Lots of messy changes to smoothen release procedure. Version 1.4: released 2010-02-03 -------------------------------- - Using `auth_callback=False` when authentication is actually required now raises a `FlickrError` exception. - The implementation uses `self.flickr_host` that subclasses can override the API URLs. - Support for short URLs was added. Version 1.3: released 2009-10-03 -------------------------------- - Added functions to easily walk through sets and search results, querying Flickr no more than needed. - Uses the hashlib module, falling back to the md5 module when hashlib is unavailable. - Added locking token cache, in case a Flickr API key is used by multiple processes at the same time on the same machine (or shared filesystem) - Removed the deprecated `fail_on_error` parameter. - Implemented the `auth_callback` functionality. Version 1.2: released 2008-11-15 -------------------------------- - Added ElementTree support for Python 2.4. [[ Patch 2007996 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=2007996&group_id=203043&atid=984008) - Made ElementTree the default response parser. - The upload and replace methods now take a `format` parameter. [[ Feature request 1912606 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1912606&group_id=203043&atid=984009) - Removed deprecated methods. - `upload` and `replace` methods no longer report progress on their callback regarding the HTTP headers. Version 1.1: released 2008-04-15 -------------------------------- - Tab completion of all Flickr API functions in IPython. - Fix for a bug where a crash occurred when XML containing a `` element was parsed with XMLNode. - Deprecated a number of methods for old-style error handling, including the `fail_on_error` constructor parameter. Just handle the `FlickrError` exceptions instead of explicitly testing all method calls. The deprecated methods will be completely removed in release 1.2. - Implemented a response parser system, which still parses to XMLNode objects by default. It now also includes parsing to the Python standard ElementTree, which will soon replace XMLNode as the default response parser. See the [documentation](http://flickrapi.sf.net/flickrapi.html) on how to use the new parsers. - Added `format` constructor parameter to set the default response format for all method calls. Overriding per call is still possible. - Added `store_token` constructor parameter that's `True` by default. Set to `False` to ensure the on-disk token cache isn't used. [[ Feature request 1923728 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1923728&group_id=203043&atid=984009) - Added caching framework. Pass `cache=True` to the constructor to use it. [[ Feature request 1911450 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1911450&group_id=203043&atid=984009) Version 1.0: released 2008-03-12 -------------------------------- - Added [API documentation](http://flickrapi.sf.net/apidoc/). [[ Feature request 1834460 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1834460&group_id=203043&atid=984009) - Lot of code improvements, most of them renaming of the public interface to the Python coding standard described in PEP 8. The exact publicly visible changes are described in the UPGRADING file. [[ Feature request 1773473 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1773473&group_id=203043&atid=984009) - Added multi-user support to the authentication token cache. [[ Feature request 1874067 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1874067&group_id=203043&atid=984009) - Support for web applications. [[ Feature request 1896701 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1896701&group_id=203043&atid=984009) - Passing `format='rest'` results in unparsed XML from the REST request. [[ Feature request 1834459 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1834459&group_id=203043&atid=984009) - Removed `uuid` module and replaced it with our own random MIME boundary generator. - Improved code and package documentation. Version 0.15: released 2007-11-07 -------------------------------- - Support for unauthenticated, read-only access to the Flickr API. [[ Feature request 1802210 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1802210&group_id=203043&atid=984009) - Deprecated the `getToken(...)` method. Use `getTokenPartOne(...)` and `getTokenPartTwo(...)` instead. - Better Windows compatibility by opening JPEG files in binary mode. Version 0.14: released 2007-09-19 -------------------------------- - `upload` method allows for the application to receive progress information regarding running uploads. [[ Feature request 1793276 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1793276&group_id=203043&atid=984009) - Preliminary support for different response formats. If a format other than the default REST is chosen, the raw response will be returned to the caller without parsing. - Python 2.4 compatibility. - Uses the `webbrowser` Python module to start a webbrowser, instead of doing it ourselves. This will greatly improve the platform independence of applications. [[ Feature request 1793350 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1793350&group_id=203043&atid=984009) Please note that this changes the API itself - you can no longer pass the 'browser' and 'fork' parameters to the `getTokenPartOne(...)` method call. The `webbrowser` module figures out what to do by itself. Version 0.13: released 2007-09-12 -------------------------------- - No longer allow passing `jpegData` to the `upload` or `replace` methods. The code handling it was buggy. If you're actually using the `jpegData` parameter of those functions, let us know and we might restore the functionality in a proper way. - Full Unicode compatibility. Pass strings as `unicode` objects, and they'll be automatically encoded as UTF-8 before sending to Flickr. [[ Bug report 1779463 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1779463&group_id=203043&atid=984006) - Introduced unittests. Not everything is covered yet, but we've got a good start. [[ Feature request 1773475 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1773475&group_id=203043&atid=984009) - Refactored the upload/replace methods. [[ Feature request 1778905 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1778905&group_id=203043&atid=984009) - Gracefully handle corrupt or otherwise invalid data in the token cache. [[ Feature request 1781236 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1781236&group_id=203043&atid=984009) Version 0.12: released 2007-08-21 -------------------------------- - First SourceForge based release. - Added [replace functionality](http://flickrapi.sourceforge.net/flickrapi.html#flickr-replace). - Added compatibility with graphical browsers. - Added functionality for GUI application authentication. - A `FlickrError` exception is now thrown when a Flickr API call returns an error status. [[ Feature request 1772031 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1772031&group_id=203043&atid=984009) - Reorganized the package source to a Python package (it was a module). - Instead of writing to stderr the `logging` module is now used for logging. - No longer required to pass the API key and the authentication token to Flickr API calls. [[ Feature request 1772023 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1772023&group_id=203043&atid=984009) - Written proper documentation [[ Feature request 1772026 ]](http://sourceforge.net/tracker/index.php?func=detail&aid=1772026&group_id=203043&atid=984009) Historical versions ------------------- Before we moved to SourceForge, there was a simple numbering scheme from version 1 to 11. Version 11 was our first version in Subversion, and was tagged as 0.11. From there on, a *major.minor* numbering scheme is used. This is the pre-Subversion changelog: 1. initial release 2. added upload functionality 3. code cleanup, convert to doc strings 4. better permission support 5. converted into fuller-featured "flickrapi" 6. fix upload sig bug (thanks Deepak Jois), encode test output 7. fix path construction, Manish Rai Jain's improvements, exceptions 8. change API endpoint to "api.flickr.com" 9. change to MIT license 10. fix horrid `\r\n` bug on final boundary 11. break out validateFrob() for subclassing flickrapi-version-2.4/LICENSE.txt000066400000000000000000000025011323562532600166360ustar00rootroot00000000000000Copyright (c) 2012-2018, 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-version-2.4/MANIFEST.in000066400000000000000000000004261323562532600165550ustar00rootroot00000000000000include README.md CHANGELOG.md LICENSE.txt UPGRADING.txt tox.ini setup.cfg .coveragerc recursive-include flickrapi *.py recursive-include doc * prune doc/_build recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude * *~ recursive-exclude * .coverage flickrapi-version-2.4/README.md000066400000000000000000000033741323562532600163030ustar00rootroot00000000000000Python FlickrAPI ================ [![Build Status](https://travis-ci.org/sybrenstuvel/flickrapi.svg?branch=master)](https://travis-ci.org/sybrenstuvel/flickrapi) [![Coverage Status](https://coveralls.io/repos/github/sybrenstuvel/flickrapi/badge.svg?branch=master)](https://coveralls.io/github/sybrenstuvel/flickrapi?branch=master) Most of the info can be found in the `doc` directory, or on https://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 the extra dependencies using: pip install tox .[docs] .[qa] .[test] then run `py.test` in the top-level directory. Supported Python versions ------------------------- The minimum Python versions that are supported are 2.7 and 3.4. [Python 2.7 will be discontinued in 2020](http://www.i-programmer.info/news/216-python/7179-python-27-to-be-maintained-until-2020.html), which also marks the end of the support for Python 2.x for this library. We might discontinue supporting Python 2.x earlier than that, but only if there is a very compelling reason to do so. As of September 2017, [Python 3.3 stopped receiving security updates](https://www.python.org/dev/peps/pep-0398/#lifespan), at which time this library also stopped supporting it. Generating the documentation ---------------------------- The documentation is written using [Sphinx](http://www.sphinx-doc.org). Execute `make html` in the `doc` directory to generate the HTML pages. They can then be found in the `doc/_build/html` directory. Tests ----- Run the tests on all supported Python versions with tox (`pip install tox`):: tox To run the tests for a specific Python version, e.g. 2.7:: tox -e py27 flickrapi-version-2.4/UPGRADING.txt000066400000000000000000000072571323562532600171110ustar00rootroot00000000000000Upgrading 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-version-2.4/demo-flask/000077500000000000000000000000001323562532600170375ustar00rootroot00000000000000flickrapi-version-2.4/demo-flask/demo.py000077500000000000000000000034421323562532600203430ustar00rootroot00000000000000#!/usr/bin/env python import logging logging.basicConfig(level=logging.INFO) import flickrapi from flask import Flask, render_template, redirect, url_for, request, session app = Flask(__name__) api_key = u'ecd01ab8f00faf13e1f8801586e126fd' api_secret = u'2ee3f558fd79f292' @app.route("/") def index(): return render_template('index.html') @app.route('/login') def login(): flickr = flickrapi.FlickrAPI(api_key, api_secret) if flickr.token_valid(perms='read'): # Already logged in print('Already logged in, redirecting to index.') return redirect(url_for('index')) # Get the request token callback = url_for('auth_ok', _external=True) print('Getting request token with callback URL %s' % callback) flickr.get_request_token(oauth_callback=callback) authorize_url = flickr.auth_url(perms='read') # Store it in the session, to use in auth_ok() session['request_token'] = flickr.flickr_oauth.resource_owner_key session['request_token_secret'] = flickr.flickr_oauth.resource_owner_secret session['requested_permissions'] = flickr.flickr_oauth.requested_permissions print(session) print('Redirecting to %s.' % authorize_url) return redirect(authorize_url) @app.route('/auth_ok') def auth_ok(): flickr = flickrapi.FlickrAPI(api_key, api_secret) flickr.flickr_oauth.resource_owner_key = session['request_token'] flickr.flickr_oauth.resource_owner_secret = session['request_token_secret'] flickr.flickr_oauth.requested_permissions = session['requested_permissions'] verifier = request.args['oauth_verifier'] print('Getting resource key') flickr.get_access_token(verifier) return 'Verifier is %s' % verifier if __name__ == "__main__": app.debug = True app.secret_key = 'je moeder' app.run() flickrapi-version-2.4/demo-flask/hello.py000066400000000000000000000002271323562532600205150ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!' if __name__ == '__main__': app.run() flickrapi-version-2.4/demo-flask/templates/000077500000000000000000000000001323562532600210355ustar00rootroot00000000000000flickrapi-version-2.4/demo-flask/templates/auth_ok.html000066400000000000000000000001221323562532600233500ustar00rootroot00000000000000{% extends "master.html" %} {% block body %} Welcome from Flickr! {% endblock %} flickrapi-version-2.4/demo-flask/templates/index.html000066400000000000000000000001311323562532600230250ustar00rootroot00000000000000{% extends "master.html" %} {% block body %} Log in {% endblock %} flickrapi-version-2.4/demo-flask/templates/master.html000066400000000000000000000002031323562532600232110ustar00rootroot00000000000000 FlickrAPI demo app {% block body %} {% endblock %} flickrapi-version-2.4/doc/000077500000000000000000000000001323562532600155625ustar00rootroot00000000000000flickrapi-version-2.4/doc/1-intro.rst000066400000000000000000000137621323562532600176160ustar00rootroot00000000000000 Introduction ====================================================================== This documentation does not specify what each Flickr API function does, nor what it returns. The `Flickr API documentation`_ is the source for that information, and will most likely be more up-to-date than this document could be. Since the Python Flickr API uses dynamic methods and introspection, you can call new Flickr methods as soon as they become available. .. _`Flickr API documentation`: http://www.flickr.com/services/api/ .. _`Flickr`: http://www.flickr.com/ .. _`Python Flickr API interface`: http://stuvel.eu/flickrapi Concepts ---------------------------------------------------------------------- To keep things simple, we do not write "he/she" or "(s)he". We know that men and women can all be fine programmers and end users. Some people will be addressed as male, others as female. To be able to easily talk about Flickr, its users, programmers and applications, here is an explanation of some concepts we use. you The reader of this document. We assume you are a programmer and that you are using this Python Flickr API to create an application. In this document we shall address you as male. application The Python application you are creating, that has to interface with Flickr. user The user of the application, and thus (either directly or indirectly via your application) a Flickr user. In this document we shall address the user as female. Installation ---------------------------------------------------------------------- You can install Python FlickrAPI from the Python Package Index using:: pip install flickrapi Installation from source is done using:: python setup.py install Developers of the Python Flickr API code (that is, people working on the library code, rather than using the library) can install development dependencies using:: pip install -r requirements.txt Requirements and compatibility ---------------------------------------------------------------------- The Python Flickr API uses two external libraries, Requests_ and Six_, and is compatible with Python 2.7 and 3.4+. Rendering the documentation requires `Sphinx `_. .. _Requests: http://docs.python-requests.org/ .. _Six: http://packages.python.org/six/ Upgrading ---------------------------------------------------------------------- This section describes how to upgrade from older versions. From 1.x 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-version-2.4/doc/2-calling.rst000066400000000000000000000201111323562532600200570ustar00rootroot00000000000000 Calling API functions ====================================================================== You start by creating a FlickrAPI object with your API key and secret. These can be obtained at `Flickr Services`_. Once you have that key, the cool stuff can begin. Calling a Flickr function is very easy. Here are some examples:: import flickrapi api_key = u'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' api_secret = u'YYYYYYYYYYYYYYYYYYYYYYY' flickr = flickrapi.FlickrAPI(api_key, api_secret) photos = flickr.photos.search(user_id='73509078@N00', per_page='10') sets = flickr.photosets.getList(user_id='73509078@N00') .. _`Flickr Services`: http://www.flickr.com/services/api/keys/apply/ The API key and secret MUST be Unicode strings. All parameters you pass to Flickr MUST be passed as keyword arguments. .. NOTE:: Contrary to Flickr's API documentation, you should **not** pass the ``api_key`` and ``api_secret`` parameters to each call. Passing them to the ``FlickrAPI`` constructor is enough. Parsing the return value ---------------------------------------------------------------------- Flickr sends back XML when you call a function. This XML is parsed and returned to the application. There are two parsers available: ElementTree and XMLNode. ElementTree was introduced in Python Flickr API version 1.1, and replaced XMLNode as the default parser as of version 1.2. If you want another format, such as JSON, you can use that too - see `Unparsed response formats`_. In the following sections, we'll use a ``sets = flickr.photosets.getList(...)`` call and assume this was the response XML: .. code-block:: xml Test foo My Set bar Response format: JSON ---------------------------------------------------------------------- When using JSON as the response format, there are a few possibilities. The most common use case is to use Python's built-in ``json`` module to parse the response, and return it as a ``dict``:: flickr = flickrapi.FlickrAPI(api_key, api_secret, format='parsed-json') sets = flickr.photosets.getList(user_id='73509078@N00') title = sets['photosets']['photoset'][0]['title']['_content'] print('First set title: %s' % title) To get the raw JSON response, use ``format='json'``. This will be directly parseable, as the Python FlickrAPI will pass ``nojsoncallback=1`` by default:: flickr = flickrapi.FlickrAPI(api_key, api_secret, format='json') raw_json = flickr.photosets.getList(user_id='73509078@N00') # raw_json -> '{...}' import json parsed = json.loads(raw_json.decode('utf-8')) To get a JavaScript callback function, use ``format=json`` and ``jsoncallback='foobar'``:: flickr = flickrapi.FlickrAPI(api_key, api_secret, format='json') raw_json = flickr.photosets.getList(user_id='73509078@N00', jsoncallback='foobar') # raw_json -> 'foobar({...})' Response parser: ElementTree ---------------------------------------------------------------------- ElementTree_ is an XML parser library that's part of Python's standard library. It is the default response parser, so if you create the ``FlickrAPI`` instance like this, you'll use ElementTree:: flickr = flickrapi.FlickrAPI(api_key, api_secret) or explicitly:: flickr = flickrapi.FlickrAPI(api_key, api_secret, format='etree') The ElementTree_ documentation is quite clear, but to make things even easier, here are some examples using the call and response XML as described above:: sets = flickr.photosets.getList(user_id='73509078@N00') sets.attrib['stat'] => 'ok' sets.find('photosets').attrib['cancreate'] => '1' set0 = sets.find('photosets').findall('photoset')[0] +-------------------------------+-----------+ | variable | value | +-------------------------------+-----------+ | set0.attrib['id'] | u'5' | | set0.attrib['primary'] | u'2483' | | set0.attrib['secret'] | u'abcdef' | | set0.attrib['server'] | u'8' | | set0.attrib['photos'] | u'4' | | set0.title[0].text | u'Test' | | set0.description[0].text | u'foo' | | set0.find('title').text | 'Test' | | set0.find('description').text | 'foo' | +-------------------------------+-----------+ ... and similar for set1 ... .. _ElementTree: http://effbot.org/zone/element.htm Response parser: XMLNode ---------------------------------------------------------------------- The XMLNode objects are quite simple. Attributes in the XML are converted to dictionary keys with unicode values. Subelements are stored in properties. We assume you did ``sets = flickr.photosets.getList(...)``. The ``sets`` variable will be structured as such:: sets['stat'] = 'ok' sets.photosets[0]['cancreate'] = u'1' sets.photosets[0].photoset = < a list of XMLNode objects > set0 = sets.photosets[0].photoset[0] set1 = sets.photosets[0].photoset[1] +--------------------------+-----------+ | variable | value | +--------------------------+-----------+ | set0['id'] | u'5' | | set0['primary'] | u'2483' | | set0['secret'] | u'abcdef' | | set0['server'] | u'8' | | set0['photos'] | u'4' | | set0.title[0].text | u'Test' | | set0.description[0].text | u'foo' | +--------------------------+-----------+ | set1['id'] | u'4' | | set1['primary'] | u'1234' | | set1['secret'] | u'832659' | | set1['server'] | u'3' | | set1['photos'] | u'12' | | set1.title[0].text | u'My Set' | | set1.description[0].text | u'bar' | +--------------------------+-----------+ Every ``XMLNode`` also has a ``name`` property. The content of this property is left as an exercise for the reader. As of version 1.2 of the Python Flickr API this XMLNode parser is no longer the default parser, in favour of the ElementTree parser. XMLNode is still supported, though. Erroneous calls ---------------------------------------------------------------------- When something has gone wrong Flickr will return an error code and a description of the error. In this case, a ``FlickrError`` exception will be thrown. Unparsed response formats ---------------------------------------------------------------------- Flickr supports different response formats, such as JSON and SOAP. If you want, you can use such a different response format. Just add a parameter like ``format="json"`` to the Flickr call. The Python Flickr API won't parse that format for you, and you just get the raw response:: >>> f = flickrapi.FlickrAPI(api_key, api_secret) >>> f.test_echo(boo='baah', format='json') 'jsonFlickrApi({"format":{"_content":"json"}, "auth_token":{"_content":"xxxxx"}, "boo":{"_content":"baah"}, "api_sig":{"_content":"xxx"}, "api_key":{"_content":"xxx"}, "method":{"_content":"flickr.test.echo"}, "stat":"ok"})' If you want all your calls in a certain format, you can also use the ``format`` constructor parameter:: >>> f = flickrapi.FlickrAPI(api_key, api_secret, format='json') >>> f.test.echo(boo='baah') 'jsonFlickrApi({"format":{"_content":"json"}, "auth_token":{"_content":"xxxxx"}, "boo":{"_content":"baah"}, "api_sig":{"_content":"xxx"}, "api_key":{"_content":"xxx"}, "method":{"_content":"flickr.test.echo"}, "stat":"ok"})' If you use an unparsed format, FlickrAPI won't check for errors. Any format supported by Flickr but not described in the "Response parser" sections is considered to be unparsed. flickrapi-version-2.4/doc/3-auth.rst000066400000000000000000000235061323562532600174230ustar00rootroot00000000000000 Authentication and Authorization ====================================================================== For authentication and authorization Flickr uses `OAuth 1.0a `_. This ensures that in one flow, the user is authenticated via the Flickr website, and the application is authorized by the user to act in its name. The user's photos may be private. Access to her account is private for sure. A lot of Flickr API calls require the application to be authorized. This means that the user has to tell Flickr that the application is allowed to do whatever it needs to do. The Flickr document `User Authentication`_ explains the authentication process; it's good to know what's in there before you go on. The Python Flickr API takes care of most of the OAuth details, but still it is important to know the authentication flow. In short: 1. Get a request token from Flickr. 2. Send the user's web browser to Flickr to log in and authorize the application. 3. The browser is redirected to the application's callback URL to pass the application a verification code. 4. Use the verification code to trade the request token for an access token. 5. The access token can be stored by the application, so that future calls don't have to follow these steps. Here is a simple example that does all the above in one simple call to ``authenticate_via_browser``:: import flickrapi api_key = u'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' api_secret = u'YYYYYYYYYYYYYYYY' flickr = flickrapi.FlickrAPI(api_key, api_secret) flickr.authenticate_via_browser(perms='read') The ``api_key`` and ``api_secret`` can be obtained from http://www.flickr.com/services/api/keys/. Every application should use its own key and secret. The call to ``flickr.authenticate_via_browser(...)`` does a lot of things. First, it checks the on-disk token cache. After all, the application may be authenticated already. If a token is found, it is checked with Flickr for validity. If it is valid, it is used for all following calls and the authentication process is complete. If the application isn't authenticated yet, a browser opens the Flickr page, on which the user can grant the application the appropriate access. When the user presses the "OK, I'LL AUTHORIZE IT" button, the browser will be redirected to a callback URL or display a verification code. The code is passed then to the application. When this code has been received, the token is stored in the token cache and the authentication process is complete. .. _`User Authentication`: http://www.flickr.com/services/api/auth.oauth.html Non-web applications -------------------------------------------------- OAuth was designed for web-based applications. After authorizing the application the browser is sent to an application-specific callback URL with a verification code appended. When your application is not web-based, you have two options: 1. Use "oob" as the callback URL. Flickr displays the verification code, which the user has to copy-and-paste to your application. This is described in `Authenticating without local web server`_. 2. Use Python Flickr API's local webserver. It is only available on the machine the application is running on, and listens on a random port. This is described in the rest of this section. Python Flickr API uses a local web server by default, and this is by far the easiest way to authorize desktop applications. .. todo:: more explanation; include timeout and GUI examples. Authenticating without local web server ---------------------------------------------------------------------- By default a webbrowser is started to let the user perform the authentication. A local web server is then started to receive the OAuth verifier code. Upon authorizing the application the browser is sent to this web server, where ``FlickrAPI`` obtains the verifier code. However, this may not be appropriate or even possible in your application. When a local web server is not used, you can use "out of band" passing of the verifier code:: import flickrapi import webbrowser api_key = u'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' api_secret = u'YYYYYYYYYYYYYYYY' flickr = flickrapi.FlickrAPI(api_key, api_secret) print('Step 1: authenticate') # Only do this if we don't have a valid token already if not flickr.token_valid(perms='read'): # Get a request token flickr.get_request_token(oauth_callback='oob') # Open a browser at the authentication URL. Do this however # you want, as long as the user visits that URL. authorize_url = flickr.auth_url(perms='read') webbrowser.open_new_tab(authorize_url) # Get the verifier code from the user. Do this however you # want, as long as the user gives the application the code. verifier = str(input('Verifier code: ')) # Trade the request token for an access token flickr.get_access_token(verifier) print('Step 2: use Flickr') resp = flickr.photos.getInfo(photo_id='7658567128') Authenticating web applications ---------------------------------------------------------------------- When working with web applications, things are a bit different. The user using the application (through a browser) is likely to be different from the user running the server-side software. You can pass a username to the ``FlickrAPI`` constructor, so that access tokens from different users won't be mixed up. .. todo:: web flow Token handling in web applications ---------------------------------------------------------------------- Web applications have two kinds of users: identified and anonymous users. If your users are identified, you can pass their name (or other means of identification) as the ``username`` parameter to the ``FlickrAPI`` constructor, and get a FlickrAPI instance that's bound to that user. It will keep track of the authentication token for that user, and there's nothing special you'll have to do. When working with anonymous users, you'll have to store their access token in a cookie. .. todo:: concrete examples like this:: flickr = flickrapi.FlickrAPI(api_key, api_secret, token=token) It won't be stored in the on-disk token cache - which is a good thing, since A. you don't know who the user is, so you wouldn't be able to retrieve the appropriate tokens for visiting users. B. the tokens are stored in cookies, so there is no need to store them in another place. Preventing usage of on-disk token cache ---------------------------------------------------------------------- If for any reason you want to make sure the access token is not stored, pass ``store_token=False`` as constructor parameter. Use this if you want to be absolutely sure that the FlickrAPI instance doesn't use any previously stored tokens, nor that it will store new tokens. Configuring location of on-disk token cache ---------------------------------------------------------------------- By default the authentication tokens are stored in ``~/.flickr``. If you want to change this, just pass ``token_cache_location='/path/to/token/cache/dir'`` as constructor parameter. Multiple processes using the same key ---------------------------------------------------------------------- The token database uses SQLite3, so it should be safe to access using multiple processes at the same time. Example using Django ---------------------------------------------------------------------- .. todo:: Update this example. Here is a simple example in `Django `_:: import flickrapi from django.conf import settings from django.http import HttpResponseRedirect, HttpResponse import logging logging.basicConfig() log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) def require_flickr_auth(view): '''View decorator, redirects users to Flickr when no valid authentication token is available. ''' def protected_view(request, *args, **kwargs): if 'token' in request.session: token = request.session['token'] log.info('Getting token from session: %s' % token) else: token = None log.info('No token in session') f = flickrapi.FlickrAPI(settings.FLICKR_API_KEY, settings.FLICKR_API_SECRET, token=token, store_token=False) if token: # We have a token, but it might not be valid log.info('Verifying token') try: f.auth_checkToken() except flickrapi.FlickrError: token = None del request.session['token'] if not token: # No valid token, so redirect to Flickr log.info('Redirecting user to Flickr to get frob') url = f.auth_url(perms='read') return HttpResponseRedirect(url) # If the token is valid, we can call the decorated view. log.info('Token is valid') return view(request, *args, **kwargs) return protected_view def callback(request): log.info('We got a callback from Flickr, store the token') f = flickrapi.FlickrAPI(settings.FLICKR_API_KEY, settings.FLICKR_API_SECRET, store_token=False) frob = request.GET['frob'] token = f.get_token(frob) request.session['token'] = token return HttpResponseRedirect('/content') @require_flickr_auth def content(request): return HttpResponse('Welcome, oh authenticated user!') Every view that calls an authenticated Flickr method should be decorated with ``@require_flickr_auth``. For more information on function decorators, see `PEP 318 `_. The ``callback`` view should be called when the user is sent to the callback URL as defined in your Flickr API key. The key and secret should be configured in your settings.py, in the properties ``FLICKR_API_KEY`` and ``FLICKR_API_SECRET``. flickrapi-version-2.4/doc/4-uploading.rst000066400000000000000000000054671323562532600204530ustar00rootroot00000000000000 Uploading or replacing images ====================================================================== Transferring images requires special attention since they have to send a lot of data. Therefore they also are a bit different than advertised in the Flickr API documentation. flickr.upload(...) ---------------------------------------------------------------------- The ``flickr.upload(...)`` method has the following parameters: ``filename`` The filename of the image. The image data is read from this file or from ``fileobj``. ``fileobj`` An optional file-like object from which the image data can be read. ``title`` The title of the photo ``description`` The description of the photo ``tags`` Space-delimited list of tags. Tags that contain spaces need to be quoted. For example:: tags='''Amsterdam "central station"''' Those are two tags, "Amsterdam" and "central station". ``is_public`` "1" if the photo is public, "0" if it is private. The default is public. ``is_family`` "1" if the private photo is visible for family, "0" if not. The default is not. ``is_friend`` "1" if the private photo is visible for friends, "0" if not. The default is not. ``format`` The response format. This *must* be either ``rest`` or one of the parsed formats ``etree`` / ``xmlnode``. The ``fileobj`` parameter can be used to monitor progress via a callback method. For example:: import os.path 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(filename) 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) params['fileobj'] = FileWithCallback(params['filename'], callback) rsp = flickr.upload(params) The callback method takes one parameter:: def callback(progress): print(progress) ``progress`` is a number between 0 and 100. flickr.replace(...) ---------------------------------------------------------------------- The ``flickr.replace(...)`` method has the following parameters: ``filename`` The filename of the image. ``photo_id`` The identifier of the photo that is to be replaced. Do not use this when uploading a new photo. ``fileobj`` An optional file-like object from which the image data can be read. ``format`` The response format. This *must* be either ``rest`` or one of the parsed formats ``etree`` / ``xmlnode``. Only the image itself is replaced, not the other data (title, tags, comments, etc.). flickrapi-version-2.4/doc/5-unicode.rst000066400000000000000000000013751323562532600201120ustar00rootroot00000000000000 Unicode and UTF-8 ====================================================================== Flickr expects every text to be encoded in UTF-8. The Python Flickr API can help you in a limited way. If you pass a ``unicode`` string, it will automatically be encoded to UTF-8 before it's sent to Flickr. This is the preferred way of working, and is also forward-compatible with the upcoming Python 3. If you do not use ``unicode`` strings, you're on your own, and you're expected to perform the UTF-8 encoding yourself. Here is an example:: flickr.photos.setMeta(photo_id='12345', title=u'Money', description=u'Around \u20ac30,-') This sets the photo's title to "Money" and the description to "Around €30,-". flickrapi-version-2.4/doc/6-caching.rst000066400000000000000000000031471323562532600200600ustar00rootroot00000000000000 Caching of Flickr API calls ====================================================================== There are situations where you call the same Flickr API methods over and over again. An example is a web page that shows your latest ten sets. In those cases caching can significantly improve performance. The FlickrAPI module comes with its own in-memory caching framework. By default it caches at most 200 entries, which time out after 5 minutes. These defaults are probably fine for average use. To use the cache, just pass ``cache=True`` to the constructor:: flickr = flickrapi.FlickrAPI(api_key, cache=True) To tweak the cache, instantiate your own instance and pass it some constructor arguments:: flickr = flickrapi.FlickrAPI(api_key, cache=True) flickr.cache = flickrapi.SimpleCache(timeout=300, max_entries=200) ``timeout`` is in seconds, ``max_entries`` in number of cached entries. Using the Django caching framework ---------------------------------------------------------------------- The caching framework was designed to have the same interface as the `Django low-level cache API`_ - thanks to those guys for designing a simple and effective cache. The result is that you can simply plug the Django caching framework into FlickrAPI, like this:: from django.core.cache import cache flickr = flickrapi.FlickrAPI(api_key, cache=True) flickr.cache = cache That's all you need to enable a wealth of caching options, from database-backed cache to multi-node in-memory cache farms. .. _`Django low-level cache API`: https://docs.djangoproject.com/en/dev/topics/cache/#the-low-level-cache-api flickrapi-version-2.4/doc/7-util.rst000066400000000000000000000053751323562532600174470ustar00rootroot00000000000000 Utility methods ====================================================================== There are a couple of useful methods for handling photos. *All utility methods require ElementTree to be available, so either use Python 2.5 or newer, or install it as described above.* Walking through all photos in a set ---------------------------------------------------------------------- It may be useful to be able to easily perform an operation on every photo in a set. This is what the ``walk_set`` function does. It accepts a photoset ID and returns a generator:: flickr = flickrapi.FlickrAPI(api_key) for photo in flickr.walk_set('2b640a3efc262f03567ee93cfd544e14'): print photo.get('title') The function uses the Flickr API call flickr.photosets.getPhotos_ and accepts the same parameters. The resulting "photo" objects are ElementTree objects for the ```` XML elements. .. _flickr.photosets.getPhotos: http://www.flickr.com/services/api/flickr.photosets.getPhotos.html Walking through a search result ---------------------------------------------------------------------- Walking through a search result is done in much the same way as walking through all photos in a set:: flickr = flickrapi.FlickrAPI(api_key) for photo in flickr.walk(tag_mode='all', tags='sybren,365,threesixtyfive', min_taken_date='2008-08-20', max_taken_date='2008-08-30'): print photo.get('title') The function uses the Flickr API call flickr.photos.search_ and accepts the same parameters. The resulting "photo" objects are ElementTree objects for the ```` XML elements. .. _flickr.photos.search: http://www.flickr.com/services/api/flickr.photos.search.html Influencing the number of calls to Flickr ---------------------------------------------------------------------- The walking functions described above only call Flickr when they have to. When they do, they fetch ``per_page`` (default 50) photos simultaneously. The ``per_page`` parameter can be used to tweak the number of calls. The following will perform two calls two Flickr:: flickr = flickrapi.FlickrAPI(api_key) set = flickr.walk_set('', per_page=15) for photo in set[:25]: print photo.get('title') The first call will get photos 0-14, the next call will get 15-29, even though only the first 25 photo titles will be shown. Another example, if you only want to show the titles of photos 5-20:: flickr = flickrapi.FlickrAPI(api_key) set = flickr.walk_set('' per_page=20) for photo in set[5:21]: print photo.get('title') The photos will always be fetched from the first page onwards. In the above example, the first twenty photos will all be fetched, even though the title of the first five will be skipped. flickrapi-version-2.4/doc/8-shorturl.rst000066400000000000000000000011341323562532600203420ustar00rootroot00000000000000 Short Flickr URLs ====================================================================== Flickr supports linking to a photo page using a short url such as http://flic.kr/p/6BTTT6. The ``flickrapi.shorturl`` module contains functionality for working with those short URLs. ``flickrapi.shorturl.encode(photo ID)``: Returns the short ID for this photo ID ``flickrapi.shorturl.encode(short ID)``: Returns the photo ID for this short ID ``flickrapi.shorturl.url(photo ID)``: Returns the short URL for the given photo ID. The photo ID, the short ID and the short URL are all unicode strings. flickrapi-version-2.4/doc/9-links.rst000066400000000000000000000005021323562532600175770ustar00rootroot00000000000000 Links ====================================================================== - `Python Flickr API interface`_ - `Flickr`_ - `Flickr API documentation`_ .. _`Python Flickr API interface`: http://stuvel.eu/flickrapi .. _`Flickr`: http://www.flickr.com/ .. _`Flickr API documentation`: http://www.flickr.com/services/api/ flickrapi-version-2.4/doc/Makefile000066400000000000000000000131121323562532600172200ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonFlickrAPI.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonFlickrAPI.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonFlickrAPI" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonFlickrAPI" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." upload: html rsync -va --delete $(BUILDDIR)/html/ stuvel@stuvel.eu:site-stuvel.eu/htdocs/flickrapi-doc/ flickrapi-version-2.4/doc/_static/000077500000000000000000000000001323562532600172105ustar00rootroot00000000000000flickrapi-version-2.4/doc/_static/.keep000066400000000000000000000000001323562532600201230ustar00rootroot00000000000000flickrapi-version-2.4/doc/conf.py000066400000000000000000000175241323562532600170720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Python Flickr API documentation build configuration file, created by # sphinx-quickstart on Mon Nov 19 05:27:43 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys from datetime import datetime sys.path.insert(0, os.path.abspath('..')) from flickrapi import __version__, __author__ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] todo_include_todos = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Python Flickr API' copyright = u'2012-{}, {}'.format(datetime.now().year, __author__) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PythonFlickrAPIdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'PythonFlickrAPI.tex', u'Python Flickr API Documentation', __author__, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pythonflickrapi', u'Python Flickr API Documentation', [__author__], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'PythonFlickrAPI', u'Python Flickr API Documentation', u'Sybren A. Stüvel', 'PythonFlickrAPI', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' flickrapi-version-2.4/doc/index.rst000066400000000000000000000013511323562532600174230ustar00rootroot00000000000000Welcome to Python Flickr API's documentation! ============================================= `Flickr`_ is one of the most popular photo sharing websites. Their public API makes it very easy to write applications that use Flickr some way or another. The possibilities are limitless. This document describes how to use the Flickr API in your Python programs using the `Python Flickr API interface`_. .. _`Flickr`: http://www.flickr.com/ .. _`Python Flickr API interface`: http://stuvel.eu/flickrapi Contents: .. toctree:: :maxdepth: 2 1-intro 2-calling 3-auth 4-uploading 5-unicode 6-caching 7-util 8-shorturl 9-links Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` flickrapi-version-2.4/flickrapi/000077500000000000000000000000001323562532600167615ustar00rootroot00000000000000flickrapi-version-2.4/flickrapi/__init__.py000066400000000000000000000054711323562532600211010ustar00rootroot00000000000000#!/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 # Copyright (c) 2007-2018 by the respective coders, see # http://stuvel.eu/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 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, # noqa: F401 LockingTokenCache) __version__ = '2.4.0' __all__ = ('FlickrAPI', 'IllegalArgumentException', 'FlickrError', 'CancelUpload', 'LockingError', 'XMLNode', 'set_log_level', '__version__', 'SimpleCache', 'TokenCache', 'SimpleTokenCache', 'LockingTokenCache') __author__ = 'Sybren Stüvel' 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 flickrapi.core.LOG.setLevel(level) flickrapi.tokencache.LOG.setLevel(level) if __name__ == "__main__": print("Running doctests") import doctest doctest.testmod() print("Tests OK") flickrapi-version-2.4/flickrapi/auth.py000066400000000000000000000426641323562532600203100ustar00rootroot00000000000000"""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 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 = (u'read', u'write', u'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): fmt = 'FlickrAccessToken(token=%s, fullname=%s, username=%s, user_nsid=%s)' return six.text_type(fmt % (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.""" session = requests.Session() 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, default_timeout=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 self.default_timeout = default_timeout @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, timeout=None): """Performs the HTTP request, signed with OAuth. :param timeout: optional request timeout, in seconds. :type timeout: float @return: the response content """ req = self.session.post(url, data=params, auth=self.oauth, timeout=timeout or self.default_timeout) # 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, timeout=None): """Performs a file upload to the given URL with the given parameters, signed with OAuth. :param timeout: optional request timeout, in seconds. :type timeout: float @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).encode('utf8') # 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) 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} self.log.debug('POST %s', auth) req = self.session.post(url, data=m, headers=auth, timeout=timeout or self.default_timeout) # 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=u'read'): """Returns the URL the user should visit to authenticate the given oauth Token. Use this method in webapps, where you can redirect the user to the returned URL. After authorization by the user, the browser is redirected to the callback URL, which will contain the OAuth verifier. Set the 'verifier' property on this object in order to use it. In stand-alone apps, 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 (u'read', u'write', u'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=u'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. """ import webbrowser # 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=u'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 auth_for_test(self, perms=u'read'): """Doesn't wait for anything, sets the verifier to something silly. Only use this in unit tests. """ auth_url = self.auth_url(perms=perms) # noqa: F841 # Normally we would direct the user to this URL. Now we don't. self.verifier = u'test' 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-version-2.4/flickrapi/cache.py000066400000000000000000000060521323562532600204010ustar00rootroot00000000000000# -*- 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-version-2.4/flickrapi/call_builder.py000066400000000000000000000030661323562532600217610ustar00rootroot00000000000000 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 self.__name__ = method_name.split('.')[-1] 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-version-2.4/flickrapi/core.py000066400000000000000000000722461323562532600202760ustar00rootroot00000000000000"""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.__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, token_cache_location=None, timeout=None): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. username Used to identify the appropriate authentication token for a certain user. token If you already have an authentication token, you can give it here. It won't be stored on disk by the FlickrAPI instance. format The response format. Use either "xmlnode" or "etree" to get a parsed response, or use any response format supported by Flickr to get an unparsed response from method calls. It's also possible to pass the ``format`` parameter on individual calls. store_token Disables the on-disk token cache if set to False (default is True). Use this to ensure that tokens aren't read nor written to disk, for example in web applications that store tokens in cookies. cache Enables in-memory caching of FlickrAPI calls - set to ``True`` to use. If you don't want to use the default settings, you can instantiate a cache yourself too: >>> f = FlickrAPI(u'123', u'123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) token_cache_location If not None, determines where the authentication tokens are stored. timeout Optional request timeout as float in seconds. """ self.default_format = format self._handler_cache = {} if isinstance(api_key, six.binary_type): api_key = api_key.decode('ascii') if isinstance(secret, six.binary_type): secret = secret.decode('ascii') if token: assert isinstance(token, auth.FlickrAccessToken) # Use a memory-only token cache self.token_cache = tokencache.SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = tokencache.SimpleTokenCache() else: # Use a real token cache self.token_cache = tokencache.OAuthTokenCache(api_key, username or '', path=token_cache_location) self.flickr_oauth = auth.OAuthFlickrInterface(api_key, secret, self.token_cache, default_timeout=timeout) if cache: self.cache = SimpleCache() else: self.cache = None def __repr__(self): """Returns a string representation of this object.""" return '[FlickrAPI for key "%s"]' % self.flickr_oauth.key __str__ = __repr__ def trait_names(self): """Returns a list of method names as supported by the Flickr API. Used for tab completion in IPython. """ try: rsp = self.reflection_getMethods(format='etree') except FlickrError: return None return [m.text[7:] for m in rsp.getiterator('method')] @rest_parser('xmlnode') def parse_xmlnode(self, rest_xml): """Parses a REST XML response from Flickr into an XMLNode object.""" rsp = XMLNode.parse(rest_xml, store_xml=True) if rsp['stat'] == 'ok': return rsp err = rsp.err[0] raise FlickrError(six.u('Error: %(code)s: %(msg)s') % err, code=err['code']) @rest_parser('parsed-json', 'json') def parse_json(self, json_string): """Parses a JSON response from Flickr.""" if isinstance(json_string, six.binary_type): json_string = json_string.decode('utf-8') import json parsed = json.loads(json_string) if parsed.get('stat', '') == 'fail': raise FlickrError(six.u('Error: %(code)s: %(message)s') % parsed, code=parsed['code']) return parsed @rest_parser('etree') def parse_etree(self, rest_xml): """Parses a REST XML response from Flickr into an ElementTree object.""" try: from lxml import etree as ElementTree LOG.info('REST Parser: using lxml.etree') except ImportError: try: import xml.etree.cElementTree as ElementTree LOG.info('REST Parser: using xml.etree.cElementTree') except ImportError: try: import xml.etree.ElementTree as ElementTree LOG.info('REST Parser: using xml.etree.ElementTree') except ImportError: try: import elementtree.cElementTree as ElementTree LOG.info('REST Parser: elementtree.cElementTree') except ImportError: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError("You need to install " "ElementTree to use the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok': return rsp err = rsp.find('err') code = err.attrib.get('code', None) raise FlickrError(six.u('Error: %(code)s: %(msg)s') % err.attrib, code=code) def __getattr__(self, method_name): """Returns a CallBuilder for the given method name.""" # Refuse to do anything with special methods if method_name.startswith('_'): raise AttributeError(method_name) # Compatibility with old way of calling, i.e. flickrobj.photos_getInfo(...) if '_' in method_name: method_name = method_name.replace('_', '.') return CallBuilder(self, method_name='flickr.' + method_name) def do_flickr_call(self, _method_name, timeout=None, **kwargs): """Handle all the regular Flickr API calls. Example:: etree = flickr.photos.getInfo(photo_id='1234') etree = flickr.photos.getInfo(photo_id='1234', format='etree') xmlnode = flickr.photos.getInfo(photo_id='1234', format='xmlnode') json = flickr.photos.getInfo(photo_id='1234', format='json') """ params = kwargs.copy() # Set some defaults defaults = {'method': _method_name, 'format': self.default_format} if 'jsoncallback' not in kwargs: defaults['nojsoncallback'] = 1 params = self._supply_defaults(params, defaults) LOG.info('Calling %s', defaults) return self._wrap_in_parser(self._flickr_call, parse_format=params['format'], timeout=timeout, **params) def _supply_defaults(self, args, defaults): """Returns a new dictionary containing ``args``, augmented with defaults from ``defaults``. Defaults can be overridden, or completely removed by setting the appropriate value in ``args`` to ``None``. """ result = args.copy() for key, default_value in six.iteritems(defaults): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in six.iteritems(result.copy()): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if value is None: del result[key] return result def _flickr_call(self, timeout=None, **kwargs): """Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self._flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') """ LOG.debug("Calling %s" % kwargs) # Return value from cache if available if self.cache and self.cache.get(kwargs): return self.cache.get(kwargs) reply = self.flickr_oauth.do_request(self.REST_URL, kwargs, timeout=timeout) # Store in cache, if we have one if self.cache is not None: self.cache.set(kwargs, reply) return reply def _wrap_in_parser(self, wrapped_method, parse_format, *args, **kwargs): """Wraps a method call in a parser. The parser will be looked up by the ``parse_format`` specifier. If there is a parser and ``kwargs['format']`` is set, it's set to ``rest``, and the response of the method is parsed before it's returned. """ # Find the parser, and set the format to rest if we're supposed to # parse it. if parse_format in rest_parsers and 'format' in kwargs: kwargs['format'] = rest_parsers[parse_format][1] LOG.debug('Wrapping call %s(self, %s, %s)' % (wrapped_method, args, kwargs)) data = wrapped_method(*args, **kwargs) # Just return if we have no parser if parse_format not in rest_parsers: return data # Return the parsed data parser = rest_parsers[parse_format][0] return parser(self, data) def _extract_upload_response_format(self, kwargs): """Returns the response format given in kwargs['format'], or the default format if there is no such key. If kwargs contains 'format', it is removed from kwargs. If the format isn't compatible with Flickr's upload response type, a FlickrError exception is raised. """ # Figure out the response format response_format = kwargs.get('format', self.default_format) if response_format not in rest_parsers and response_format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % response_format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return response_format def upload(self, filename, fileobj=None, timeout=None, **kwargs): """Upload a file to flickr. Be extra careful you spell the parameters correctly, or you will get a rather cryptic "Invalid Signature" error on the upload! Supported parameters: filename name of a file to upload fileobj an optional file-like object from which the data can be read title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private content_type Set to "1" for Photo, "2" for Screenshot, or "3" for Other. hidden Set to "1" to keep the photo in global search results, "2" to hide from public searches. format The response format. You can only choose between the parsed responses or 'rest' for plain REST. timeout Optional timeout for the HTTP request, as float in seconds. The ``fileobj`` parameter can be used to monitor progress via a callback method. For example:: class FileWithCallback(object): def __init__(self, filename, callback): self.file = open(filename, 'rb') self.callback = callback # the following attributes and methods are required self.len = os.path.getsize(path) self.fileno = self.file.fileno self.tell = self.file.tell def read(self, size): if self.callback: self.callback(self.tell() * 100 // self.len) return self.file.read(size) fileobj = FileWithCallback(filename, callback) rsp = flickr.upload(filename, fileobj, parameters) The callback method takes one parameter: ``def callback(progress)`` Progress is a number between 0 and 100. """ return self._upload_to_form(self.UPLOAD_URL, filename, fileobj, timeout=timeout, **kwargs) def replace(self, filename, photo_id, fileobj=None, timeout=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload fileobj an optional file-like object from which the data can be read photo_id the ID of the photo to replace format The response format. You can only choose between the parsed responses or 'rest' for plain REST. Defaults to the format passed to the constructor. timeout Optional timeout for the HTTP request, as float in seconds. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self._upload_to_form(self.REPLACE_URL, filename, fileobj, timeout=timeout, **kwargs) def _upload_to_form(self, form_url, filename, fileobj=None, timeout=None, **kwargs): """Uploads a photo - can be used to either upload a new photo or replace an existing one. form_url must be either ``FlickrAPI.flickr_replace_form`` or ``FlickrAPI.flickr_upload_form``. """ if not filename: raise IllegalArgumentException("filename must be specified") if not self.token_cache.token: raise IllegalArgumentException("Authentication is required") kwargs['api_key'] = self.flickr_oauth.key # Figure out the response format response_format = self._extract_upload_response_format(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_bytes(kwargs) return self._wrap_in_parser(self.flickr_oauth.do_upload, response_format, filename, form_url, kwargs, fileobj, timeout=timeout) def token_valid(self, perms=u'read'): """Verifies the cached token with Flickr. If the token turns out to be invalid, or with permissions lower than required, the token is erased from the token cache. @return: True if the token is valid for the requested parameters, False otherwise. """ token = self.token_cache.token if not token: return False # Check token for validity self.flickr_oauth.token = token try: resp = self.auth.oauth.checkToken(format='etree') token_perms = resp.findtext('oauth/perms') if token_perms == token.access_level and token.has_level(perms): # Token is valid, and for the expected permissions. return True except FlickrError: # There was an error talking to Flickr, we assume this is due # to an invalid token. pass # Token was for other permissions, so erase it as it is # not usable for this request. self.flickr_oauth.token = None del self.token_cache.token return False @authenticator def authenticate_console(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Shows the URL the user should visit on stdout, then waits for the user to authorize the program. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_console(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_via_browser(self, perms=u'read'): """Performs the authentication/authorization, assuming a console program. Starts the browser and waits for the user to authorize the app before continuing. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_via_browser(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token @authenticator def authenticate_for_test(self, perms=u'read'): """Skips a bit of the authentication/authorization, for unit tests. """ if isinstance(perms, six.binary_type): perms = six.u(perms) self.flickr_oauth.get_request_token() self.flickr_oauth.auth_for_test(perms=perms) token = self.flickr_oauth.get_access_token() self.token_cache.token = token def get_request_token(self, oauth_callback=None): """Requests a new request token. Updates this OAuthFlickrInterface object to use the request token on the following authentication calls. @param oauth_callback: the URL the user is sent to after granting the token access. If the callback is None, a local web server is started on a random port, and the callback will be http://localhost:randomport/ If you do not have a web-app and you also do not want to start a local web server, pass oauth_callback='oob' and have your application accept the verifier from the user instead. """ self.flickr_oauth.get_request_token(oauth_callback=oauth_callback) def auth_url(self, perms=u'read'): """Returns the URL the user should visit to authenticate the given oauth Token. Use this method in webapps, where you can redirect the user to the returned URL. After authorization by the user, the browser is redirected to the callback URL, which will contain the OAuth verifier. Set the 'verifier' property on this object in order to use it. In stand-alone apps, authenticate_via_browser(...) may be easier instead. """ return self.flickr_oauth.auth_url(perms=perms) def get_access_token(self, verifier=None): """Exchanges the request token for an access token. Also stores the access token for easy authentication of subsequent calls. @param verifier: the verifier code, in case you used out-of-band communication of the verifier code. """ if verifier is not None: self.flickr_oauth.verifier = verifier self.token_cache.token = self.flickr_oauth.get_access_token() @require_format('etree') def data_walker(self, method, searchstring='*/photo', **params): """Calls 'method' with page=0, page=1 etc. until the total number of pages has been visited. Yields the photos returned. Assumes that ``method(page=n, **params).findall(searchstring)`` results in a list of interesting elements (defaulting to photos), and that the toplevel element of the result contains a 'pages' attribute with the total number of pages. """ page = 1 total = 1 # We don't know that yet, update when needed while page <= total: # Fetch a single page of photos LOG.debug('Calling %s(page=%i of %i, %s)' % (method.__name__, page, total, params)) rsp = method(page=page, **params) photoset = rsp.getchildren()[0] total = int(photoset.get('pages')) photos = rsp.findall(searchstring) # Yield each photo for photo in photos: yield photo # Ready to get the next page page += 1 @require_format('etree') def walk_contacts(self, per_page=50, **kwargs): """walk_contacts(self, per_page=50, ...) -> \ generator, yields each contact of the calling user. :Parameters: per_page the number of contacts that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.contacts.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.contacts.getList: http://www.flickr.com/services/api/flickr.contacts.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.contacts_getList, searchstring='*/contact', per_page=per_page, **kwargs) @require_format('etree') def walk_photosets(self, per_page=50, **kwargs): """walk_photosets(self, per_page=50, ...) -> \ generator, yields each photoset belonging to a user. :Parameters: per_page the number of photosets that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getList_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getList: http://www.flickr.com/services/api/flickr.photosets.getList.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getList, searchstring='*/photoset', per_page=per_page, **kwargs) @require_format('etree') def walk_set(self, photoset_id, per_page=50, **kwargs): """walk_set(self, photoset_id, per_page=50, ...) -> \ generator, yields each photo in a single set. :Parameters: photoset_id the photoset ID per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photosets.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photosets.getPhotos: http://www.flickr.com/services/api/flickr.photosets.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photosets_getPhotos, photoset_id=photoset_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user(self, user_id='me', per_page=50, **kwargs): """walk_user(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream. :Parameters: user_id the user ID, or 'me' per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.people.getPhotos_ API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.people.getPhotos: http://www.flickr.com/services/api/flickr.people.getPhotos.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.people_getPhotos, user_id=user_id, per_page=per_page, **kwargs) @require_format('etree') def walk_user_updates(self, min_date, per_page=50, **kwargs): """walk_user_updates(self, user_id, per_page=50, ...) -> \ generator, yields each photo in a user's photostream updated \ after ``min_date`` :Parameters: min_date per_page the number of photos that are fetched in one call to Flickr. Other arguments can be passed, as documented in the flickr.photos.recentlyUpdated API call in the Flickr API documentation, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.recentlyUpdated: http://www.flickr.com/services/api/flickr.photos.recentlyUpdated.html Uses the ElementTree format, incompatible with other formats. """ return self.data_walker(self.photos_recentlyUpdated, min_date=min_date, per_page=per_page, **kwargs) @require_format('etree') def walk(self, per_page=50, **kwargs): """walk(self, user_id=..., tags=..., ...) -> generator, \ yields each photo in a search query result Accepts the same parameters as flickr.photos.search_ API call, except for ``page`` because all pages will be returned eventually. .. _flickr.photos.search: http://www.flickr.com/services/api/flickr.photos.search.html Also see `walk_set`. """ return self.data_walker(self.photos.search, per_page=per_page, **kwargs) flickrapi-version-2.4/flickrapi/exceptions.py000066400000000000000000000020771323562532600215220ustar00rootroot00000000000000"""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-version-2.4/flickrapi/html.py000066400000000000000000000037101323562532600203000ustar00rootroot00000000000000# -*- 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.

""" # noqa: W293 import six if six.PY3: auth_okay_html = auth_okay_html.encode('utf-8') flickrapi-version-2.4/flickrapi/shorturl.py000066400000000000000000000031531323562532600212170ustar00rootroot00000000000000# -*- 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-version-2.4/flickrapi/sockutil.py000066400000000000000000000035211323562532600211710ustar00rootroot00000000000000# -*- 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-version-2.4/flickrapi/tokencache.py000066400000000000000000000277671323562532600214620ustar00rootroot00000000000000"""Persistent token cache management for the Flickr API""" import os.path import logging import time 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, path=''): """Creates a new token cache instance""" self.api_key = api_key self.username = username self.memory = {} self.path = path or os.path.expanduser(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(), 'rb') token = f.read().decode('utf8') 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) # Open in binary mode for backward compatibility with Python 2.7, # as that version's open() doesn't have the 'encoding' parameter. f = open(self.get_cached_token_filename(), 'wb') f.write(token.encode('utf8')) 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='', path=''): """Creates a new token cache instance""" assert lookup_key is not None self.api_key = api_key self.lookup_key = lookup_key self.path = path or 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.""" import sqlite3 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))''') db.commit() @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] import sqlite3 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) import sqlite3 # 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""" import sqlite3 # 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-version-2.4/flickrapi/xmlnode.py000066400000000000000000000045531323562532600210100ustar00rootroot00000000000000"""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-version-2.4/oauth_test_1.py000077500000000000000000000100751323562532600177740ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function import requests from requests_oauthlib import OAuth1 import six from urlparse import parse_qsl if six.PY3: from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlsplit, parse_qs else: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer from urlparse import urlsplit, parse_qs, unquote url = "http://www.flickr.com/services/oauth/request_token" class keys: apikey = u'a233c66549c9fb5e40a68c1ae156b370' apisecret = u'03fbb3ea705fe096' class OAuthTokenHTTPHandler(BaseHTTPRequestHandler): def do_GET(self): # /?oauth_token=72157630789362986-5405f8542b549e95&oauth_verifier=fe4eac402339100e qs = urlsplit(self.path).query url_vars = parse_qs(qs) self.server.oauth_token = url_vars['oauth_token'][0] self.server.oauth_verifier = url_vars['oauth_verifier'][0] self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write('OK') #self.server.server_close() #self.server.shutdown() http_server = HTTPServer(('', 8000), OAuthTokenHTTPHandler) def wait_for_http_request(): http_server.oauth_verifier = None while http_server.oauth_verifier is None: http_server.handle_request() print('OAuth verifier: %s' % http_server.oauth_verifier) return http_server.oauth_verifier print(120 * '=') print("Part 1: Obtain request token") params = { 'oauth_callback': 'oob', # "http://localhost:8000/", } import sys queryoauth = OAuth1(keys.apikey, keys.apisecret, signature_type='query') r = requests.get(url, params=params, auth=queryoauth, config={'verbose': sys.stderr}) assert isinstance(r, requests.Response) parts = r.text.split('&') print('Status code:', r.status_code) for part in parts: print(unquote(part)) print(50 * '-') ############################################################ # Part 2 ############################################################ print(120 * '=') print("Part 2: Authorize the request token") authorize_url = "http://www.flickr.com/services/oauth/authorize" #parse the content request_token = dict(parse_qsl(r.text)) print("Request Token:") print(" - oauth_token = %s" % request_token['oauth_token']) print(" - oauth_token_secret = %s" % request_token['oauth_token_secret']) print() # Create the token object with returned oauth_token and oauth_token_secret # You need to authorize this app via your browser. print("Go to the following link in your browser:") print("%s?oauth_token=%s&perms=read" % (authorize_url, request_token['oauth_token'])) print() oauth_verifier = wait_for_http_request() oauth_verifier = oauth_verifier.decode('ascii') if http_server.oauth_token != request_token['oauth_token']: print("ERROR: received verifier for different OAuth token") print(" Expected token: %r" % request_token['oauth_token']) print(" Received token: %r" % http_server.oauth_token) ############################################################ # Part 3 ############################################################ print(120 * '=') print("Part 3: Exchange request token for an access token") # url to get access token access_token_url = "http://www.flickr.com/services/oauth/access_token" # Now you need to exchange your Request Token for an Access Token queryoauth = OAuth1(keys.apikey, keys.apisecret, request_token['oauth_token'], request_token['oauth_token_secret'], signature_type='query', verifier=oauth_verifier) r = requests.get(access_token_url, auth=queryoauth, config={'verbose': sys.stderr}) #parse the response access_token_resp = {k: v.decode('utf-8') for (k, v) in parse_qsl(r.content)} #write out a file with the oauth_token and oauth_token_secret with open('token', 'w') as f: for key, value in access_token_resp.items(): keyvalue = key.encode('utf-8') + '=' + value.encode('utf-8') + '\n' f.write(keyvalue) sys.stdout.write(keyvalue) flickrapi-version-2.4/oauth_test_2.py000077500000000000000000000021371323562532600177750ustar00rootroot00000000000000#!/usr/bin/env python import logging logging.basicConfig(level=logging.DEBUG) from flickrapi import auth class keys: apikey = u'a233c66549c9fb5e40a68c1ae156b370' apisecret = u'03fbb3ea705fe096' print('Creating OAuth interface') flickr_oauth = auth.OAuthFlickrInterface(keys.apikey, keys.apisecret) # ------------------------------------------------------------------------------ print('Step 1: obtain a request token') flickr_oauth.get_request_token() # ------------------------------------------------------------------------------ print('Step 2: let the user authenticate the token') flickr_oauth.auth_via_browser('read') # ------------------------------------------------------------------------------ print('Step 3: exchange for an access token') flickr_oauth.get_access_token() # ------------------------------------------------------------------------------ print('Step 4: use Flickr!') params = { 'method': 'flickr.photos.getInfo', 'format': 'json', 'nojsoncallback': 1, 'photo_id': '7658567128', } print(flickr_oauth.do_request('http://api.flickr.com/services/rest/', params)) flickrapi-version-2.4/oauth_test_3-oob.py000077500000000000000000000023531323562532600205530ustar00rootroot00000000000000#!/usr/bin/env python from xml.etree import ElementTree as ET import six import logging import webbrowser logging.basicConfig(level=logging.INFO) from flickrapi import FlickrAPI class keys: apikey = u'a233c66549c9fb5e40a68c1ae156b370' apisecret = u'03fbb3ea705fe096' print('Creating FlickrAPI object') flickr = FlickrAPI(keys.apikey, keys.apisecret) # ------------------------------------------------------------------------------ print('Step 1: authenticate') if not flickr.token_valid(perms='read'): # Get a request token flickr.get_request_token(oauth_callback='oob') # Open a browser at the authentication URL. Do this however # you want, as long as the user visits that URL. authorize_url = flickr.auth_url(perms='read') webbrowser.open_new_tab(authorize_url) # Get the verifier code from the user. Do this however you # want, as long as the user gives the application the code. verifier = six.text_type(six.input('Verifier code: ')) # Trade the request token for an access token flickr.get_access_token(verifier) # ------------------------------------------------------------------------------ print('Step 2: use Flickr') resp = flickr.photos.getInfo(photo_id='7658567128') ET.dump(resp) flickrapi-version-2.4/oauth_test_3.py000077500000000000000000000016331323562532600177760ustar00rootroot00000000000000#!/usr/bin/env python from xml.etree import ElementTree as ET import logging logging.basicConfig(level=logging.INFO) from flickrapi import FlickrAPI class keys: apikey = u'a233c66549c9fb5e40a68c1ae156b370' apisecret = u'03fbb3ea705fe096' print('Creating FlickrAPI object') flickr = FlickrAPI(keys.apikey, keys.apisecret) # #token = flickr.token_cache.token #flickr.flickr_oauth.token = token #print('Step 0: check token %r' % token.token) # #resp = flickr.auth.oauth.checkToken(format='etree') #ET.dump(resp) #raise SystemExit() # ------------------------------------------------------------------------------ print('Step 1: authenticate') # flickr.authenticate_via_browser(perms='read') flickr.authenticate_console(perms='read') # ------------------------------------------------------------------------------ print('Step 2: user Flickr') resp = flickr.photos.getInfo(photo_id='7658567128') ET.dump(resp) flickrapi-version-2.4/oauth_test_4.py000077500000000000000000000022151323562532600177740ustar00rootroot00000000000000#!/usr/bin/env python import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger('requests_oauthlib').setLevel(logging.INFO) logging.getLogger('oauthlib').setLevel(logging.INFO) from flickrapi import FlickrAPI class keys: apikey = u'a233c66549c9fb5e40a68c1ae156b370' apisecret = u'03fbb3ea705fe096' print('Creating FlickrAPI object') flickr = FlickrAPI(keys.apikey, keys.apisecret) # ------------------------------------------------------------------------------ print('Step 1: authenticate') flickr.authenticate_via_browser(perms='delete') # ------------------------------------------------------------------------------ print('Step 2: Upload photo') resp = flickr.upload('tests/photo.jpg', is_public=0, is_friend=0, is_family=0) from xml.etree import ElementTree as ET ET.dump(resp) photo_id = resp.findtext('photoid') # ------------------------------------------------------------------------------ print('Step 3: Replace photo') flickr.replace('jaguar.jpg', photo_id=photo_id) # ------------------------------------------------------------------------------ print('Step 4: Delete photo') flickr.photos.delete(photo_id=photo_id) flickrapi-version-2.4/requirements.txt000066400000000000000000000006641323562532600203070ustar00rootroot00000000000000# Primary requirements requests==2.2.1 requests-oauthlib==0.4.0 requests-toolbelt==0.3.1 six==1.9.0 # Development requirements tox==2.3.1 responses==0.5.1 pytest==2.9.1 pytest-cov==2.2.1 pytest-xdist==1.14 Sphinx==1.2.2 mock==2.0.0 # for Python 2.7 compatibility # Secondary requirements apipkg==1.4 cookies==2.2.1 coverage==4.0.3 docutils==0.12 execnet==1.4.1 Jinja2==2.8 MarkupSafe==0.23 oauthlib==1.0.3 py==1.4.31 Pygments==2.1.3 flickrapi-version-2.4/setup.cfg000066400000000000000000000005461323562532600166430ustar00rootroot00000000000000[bdist_wheel] universal = 1 [flake8] ignore = E121, E122, E127, E128, E241, E265, E305, E402, E501, E731, F403, F405, W601 exclude = .AppleDouble, .ropeproject, .tox, .eggs, # No need to traverse our git directory .git, # There's no value in checking cache directories __pycache__, build, dist [aliases] test=pytest flickrapi-version-2.4/setup.py000066400000000000000000000062541323562532600165360ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Python setuptools install script. Run with "python setup.py install" to install FlickrAPI """ from __future__ import print_function __author__ = 'Sybren A. Stuvel' # Check the Python version import re import io import os import sys (major, minor) = sys.version_info[:2] if (major, minor) < (2, 7) or (major == 3 and minor < 4): raise SystemExit("Sorry, Python 2.7, or 3.4 or newer required") from setuptools import setup def read(*names, **kwargs): with io.open( os.path.join(os.path.dirname(__file__), *names), encoding=kwargs.get("encoding", "utf8") ) as fp: return fp.read() def find_version(*file_paths): version_file = read(*file_paths) version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) pytest_runner = ['pytest-runner'] if needs_pytest else [] test_deps = [ 'mock;python_version<"3.3"', "pytest>=2.9.1", "pytest-cov>=2.2.1", "responses>=0.5.1" ] data = { 'name': 'flickrapi', 'version': find_version("flickrapi", "__init__.py"), 'author': __author__, 'author_email': 'sybren@stuvel.eu', 'maintainer': __author__, 'maintainer_email': 'sybren@stuvel.eu', 'url': 'https://stuvel.eu/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'], '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', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries :: Python Modules', ], 'setup_requires': pytest_runner, 'tests_require': test_deps, 'install_requires': [ 'six>=1.5.2', 'requests>=2.2.1', 'requests_oauthlib>=0.4.0', 'requests_toolbelt>=0.3.1', ], 'extras_require': { 'docs': [ 'sphinx >= 1.5.1' ], 'qa': [ 'flake8' ], 'test': test_deps }, 'zip_safe': True, 'test_suite': 'tests', } setup(**data) flickrapi-version-2.4/tests/000077500000000000000000000000001323562532600161575ustar00rootroot00000000000000flickrapi-version-2.4/tests/common_responses.py000066400000000000000000000502441323562532600221270ustar00rootroot00000000000000# -*- encoding: utf-8 -*- PHOTO_XML = u''' Drowned trees Near our camping in Belavići, Croatia. 3 6x6 hasselblad500cm Belavici Duga Resa Karlovac Croatia https://www.flickr.com/photos/sybrenstuvel/7955646798/ ''' KITTEN_SEARCH_XML = u''' ''' UPLOAD_XML = u''' 1234 ''' WALK_PAGE_1_XML = u''' ''' WALK_PAGE_2_XML = u''' ''' WALK_PAGE_3_XML = u''' ''' flickrapi-version-2.4/tests/photo.jpg000066400000000000000000000567771323562532600200400ustar00rootroot00000000000000JFIFHH ExifMM* z(1 2iRCanonCanon EOS 350D DIGITALHHGIMP 2.4.52008:08:04 11:13:05₝"'"*  2:BJ   a@2008:01:24 17:15:332008:01:24 17:15:33>=l5p JFIFC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222h" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?NZjƲK.g0E#K(,2/2$!YI>?*C+u, TSKs3ե T9#9gCP DjEʨco(Rq~Sf'+ȍş1JSs 5&SH)¥!qF)M!!Rc4i7 f=4rX\~FI44m4[i52Cո 2jeRù'eT3AU=+$ܵgSi-T (F|9a]g%%ԓY#$@T+!~c++Nah ݢ Fy(338!fynnb- @p{\~( Eo'Y)aTYnc@;xIkZ?X:W|IՙX0.>^+5-hiZ,W1"Fc>Tx-uw~8Ӯm=my"oKH;4-EQ<2xXIV1"DrOj>-k7+9 "Z3lqӷTݚ(|UɲsܗM?)e$ wkڤnQTrI9$552⡳jpKT8.h+qOQJAR7pЇMaLlCjhiyJz$ 楎2s1c{HHوSu#s'k#<9V95tiFQ;6Kđ *S=(24u}_`d2}l4XY@t"CM&{I$Ve dV@?##+΁HwwgxY!#V)(Xe9h\{-k]BCA80*[-Ey( '69c;}b}YYMׂj71ZFkRZ8NcS+By U*(С*C-+A-51X`v=S#%Ĭ:g\):T""IWm$V6RO#֊[DoSW3[dk(ONin&&:s l^濳al'Ϲ=.ӁޙzWџ!r4k94ƋP)Y,H$o=GzM-U6)EboΏ1ߝ.Ro<1sdoΏ2Cm)W]R)\N{$Ʊ-=&b0*Rqp~t~tѬB3xc%O0yvg?vY8/qK}꯹̢uTxS/T;7(~wSg.0N${TX}QLY4QZ((sEP1A4QHbKz(b&梊.HIPI h^4QTAaxHc4QPl8 8#QJŦ'0 "~2EKf(fT ?֊(:?Photoshop 3.08BIMcPython FlickrAPI test imagex7Python FlickrAPI test image, http://stuvel.eu/flickrapi XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)KmC    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222 A!1AQaq"2R#Br3b$'+!1Q"Aa2#B3C ?8#j9@5"QB7AAJE9PP(ҥ"`YijQAQD! Q0ztCGP! T„L.(I 5(FpF TjEJȨ0qZvDUI@EБ@(ԋr4keF+m5]d,Z9Pn[@UZ-R@4 E(E4 jbQT< iAFA@(*jEԇU0 ,V7 j"3D ~%t"i@삀HDL*ʡEꁄkCԌȒLcOB4D"(:|ֱakR4 YӞZ=/NS,kM\C QeV㕩|UM@ aT5PhUPTj-F.LNdʠ)EuAE5!SD8a$hآE"uCH&rI*iL* ZH'ͭȰ-nFdu6m=/NmC,k,o:-+X:JqZs)+4) hSTUB0rhфAhҧA@).((.(S4!P( $4*x =#uTx#:d tE)Ft(ajȠ5DAaJ ,ԋ +6Zk?;^xXh `mfr8"d-DX9Cח˅|]6V7x-,mk]\ԜhS/{INE{CZ>-mz1\D H}:+pn?ox][ e/)U ָsA0 :džpk'-g~S;G/Й2Dh#PuAwgu>ڵ#=*$F1&ŷ_r舍e VWu..֩mmYe,'A;L ߆`Ǿ{o>HdeRkjsr+> òN˓Vըs6.m2e3;4Qceu^ֽ+j-󪲙,sk9Yi߈G{W[2g 0KqODSRJSQ()E9ET >*#tM)*I ])CL]PM\hx,Ql V۝AVp'dŁMjFhkY:vՔw\F4kƉ9gF6mK |]^WQ/OtTx{8 p;VXHw{]Zq %2|]^m1,c¸®0ڮ}H4SpXbOmK ?찌&uVͩ\K4va Desm{>.k]YԢȌ7/-B5x^ʆ#Ÿ5>e 5Y$fc4#Q;(>"KLU!ZɜǎSTf]YFvtвW0KD6i>\W.svL~68MqR5{L 7Lgt=PrWN=4.8sV+jV5`3mY7xŽcZҎ.ʭ#UՋ*SxaL\[_gux['{4/+ ? "v ~%>v \c8_Ūm l4[ER{MVss؟`8e+0*xfVF0:Y vW̷t(UR+Xۺc6us\%"4~y'~G ۇ"DaU!"Ub`Y@Q0:)[[bYFmu;@N2%W etŁ)bMjrэe~9ns霮@ 5Ԍ^Ԏ]V.$:T)HVmt熀Bκf]|>IH]99y1b3Nf<0O|WĸY~}y9g͗7]gԌxaGѰg.ҹ}hNL3+`Pz+ѩoTYUM@25 ա&֟a|T`mx7\#)$0ϟxx6[{˱ UN hD"ٵC1:/cUz7):uFkۛ7=O.&?(grDtY0#7qyk<3EϽyvB܁^ګkS$L9 kvַ8͕k{sg -$:+8bE 's'xݏ"73Ykܻ|UĘY~}y9g͗7]gԎK~/tx)>͵Ņغ}'[ASVswc{60 >޾э(I IOYS[e]^91yRJ,u1'sxQq WyW,*+z7dHplj-> 8K*]F^^ \$.?Dռng|Z*Ԩg/0CsL $ 5~ai x1}Ódz>lߘ?f.#|w^gھndrO.L!"F(tE}>wpM+wSlq;[-|-[8I:Xg'ɜ\Z/a6Xe<>6~?\|_ew^K+XUjng=*#Y:N;߾%+.pB,)#phQ; L'V3bgEDPQ,^cA( 6eiϢ %5<PLhг]9aRjYI ȖkR9ڃV)A+Z(2T;^ V$Zf+Nz0ƈPpD 4SPD0QMEX܊*-%Yj((Q7숽Tj0sb邢Δ FUB*pr pfdNW*PBa 8sz[s0;ZןBpıD N>`{Ru僞NEGdA+Nt-k 2^4 a@Ģ"hEÏ0lJ&U4TJ@h.N@EG(т$iΨiKeѰX*4QJ4UBkX!4-*b@*b5,) LR*RL*IV (( -CXZEPN$j6zvrլ%srHYrմoMύbfzt4o!6jrl_TI ;-e4 WSa쮦#"k_|PiFe*L/jImhp6ۮavE+^@@2t:c<7 8ݝTmGѨڌ;+]릱=%L[ܟ}mg:ioކ.;b?C- t&cA. O?o?fx7gsB sIn\&eD u3rexԸ qK S uzL{/(GU2xqYW]ZNݎsrq ]k:͎j`:S%Fi1 q`LVo|`w|EԷlf* -|evWiYgُp;p`isP[%9hf6L=M Ѻ>閮ihL I?oU8سWR\I͗gB';>Ѹʔ p^0ka9T*v;/Ycx]z8yrQ)S-a :>/kq[W ysYwhq q'c,0՟eR+z5 #XS#Աiop/aZڥFdFs]%lvL<8L a{ۆ縥:z29`K.\Lyŕ*Rn _{Mh07.bOv&U'?Wokp[5H#ȩ~g_q{Fp=2"Ώ+O;I:=Uk8c,F[S.!V-KCR^M=z2fսuza|Qd]I՟q U:*9mDn/]xpx:K (.k>nx[|o -kѿsYFyqtfN~No>}_;+aJ״-km}˜4lBq|]^8_xpjo^_lڡ'XXmKE f-k^(=1lTq 9w#yoK7Ęv_ªӵ2L9HvR`뷊^:l^י~ x.zDGEfB0bd VL Pjւ܆DtQ$ Q@ )ml$Ƌ⮪t .=vqm ߟhzvռ.!@<#{mw!MO )4F;u<ϗJΉAˬmGV I8n@ց%Sh.> qjW.~/Hb:5pfOsIF~喾.mp (WMB>&OjUOS!nuL˾9lvͯ彜RoroyQIdVY%סa}^;ո͞N)9NO2G3 oql:ua1Ou5m- Cg,;dGWl;qWßU^RV)Qv3u0nN1\5}m B5\O^o0 {$Kj65,6ѷ `9HUޣxcpg͵ů*7[.7#IcmB =b[l~QmqJMl :G$zƼMxe\~+h+h$I\S 4ү]+Nmq Ap929XB̺g|E]|CmiJ\u7>$1ԿQ+&ư^e|\BW[e4i}}Z[{Qk(W0Z\iq$Mr}(`>n)^5*2雞?0~ovLNVkVdВ4{wO/RY?~pMwb b Ӱw7/CON~/ǡVp,!fS πhO^/q4z|i N ][\XWs[WWLsݘ7+={۷ܰ:cA/PeөTkz%}zgUWq[<{}|EmӿmyM dRsw엛ڍ: ZS m>C.ȧyA)5}K{W¸6T;ܲ\f cF)OoJum\aa7\:n.A$?u/ܲ~ggGB޶e-qN,zhj2є K?Y[;ϯ?V78++U{}ohe.od,X^}}/)pVV:q+QZU&VI! HYj*gjzHXqƺP$.v|q=|r5k$,Nxhʸy'}LsvHRЧմ 짶մǶ5vS\:-zb zg6׸Og{}ԧ?`љFE3"z)CDЉ@FPF.DÍwEBa|tnpV5"eerRNZ4Btkxբ-u4 .(B-EC r9vq2G$H]gT 1f"hN nMkMӷ뺍0IeU罽~Z SZy :!́+2V)x)jjvbׇC-OhXזOKmu|!=m TZ i'lF槥z]~3!~ʦ4J!@0SU` \P kxupEe \8EÄAq (PG * .ԨܫVkZs,ZTh YIrKn'k\9$WS!V,b:$mNq>w=z6Đay{貋Xq L ?n8yF8]S/˵@']KegYg: W:|icLH.Wk,b7maOLAf"s=3 p {]i^? :Qq w)mq'1ms?_~@J* @J SE0aV. 5\=SM<L8&/C2aч)Uu@QtEF`֥[uYHѪ:FYu,W^kf@+R] Q%dh.=|9x$d: R|:K' p铚lC{+yIiM<;' Ht]A#E}ǧL6Fɑ/y'YZ'Ч_G:DI]'Xyz[PHqN{T>n$"zY9z9ՃS05Ӗ\H2#:_|JT&lT2tJt闽5אDu_~d5@ QEP* ҁ 5(֜eD@(aSWpEPEQ@QwcS6:Nڵ͎~XM:Ζ ë 'u\ll`&VoMsá.vխ 5iˑbfr^Yj|##{JzOo@E6ן:)P$>]||/B,^n{95vZ $Μ-hHg[6& L%Zl/-7X\QCdN/y{VX}K[*4 R19qqq뾰Ldžw:q߉JҤ \:wҶ33(@Nt5`\ABv` GY%?,uQiP9$^x;]le"enp#Mh~}*_0[+>,?+֎'PV_I +某}Ea{+c~@E9E>)QpNTUjP֎0"#״(ʍ,4ySVr%Ge5Lu#ITdĢ]곎ѵ!gˤ6 >["H=R2Y:dȐcsslmkS(2\|>ƺ|}^F۩pY:Un{-LuϧG'.j9z~v/|vc *c/ qc@ ?o=ީjǸܶ|y_#aY}˚ 5L6_oߋ>?<=Z ~671?G?|s7^{^t"N˝FSi?/k`!йo7̭k)}2 D5YcmG ;Hp-q1.fې-EϿnR hVs+],^yr֮qE:Eq6Cs 2zƣ|_wGwJ'bTҦ)2۔!:$u2~[VN+mpJڛoRBHbCлeյ+AVTak"D0u wsݼSWP_:NvWO52B@ %JP9E0P9E0T]R.)좳}`4H_<=ST *b0dJץ{]?D]>?%@_J *ƛ0/čN@%I>ңPCD${'`N1ehّ,7+f1i>.llW>zx7@8 cp']e$hSӤ2ӻ]61!@Mw/K̋PS͑//PeR4c=S+ٖȟwr{6?zyc8ѹ/[r&?E?2>OJ1ŕЂb4+ NPI-:KHWs{e~Xw_^}?'~?Gj;'{-} HI#_Ws{Gg_|mZC/zzTS\}3NnlKm^Ti$uuFҤmUq3 uY*qN׬jJ:qڨn #lAQdDC+3W 콖Ψ 5E:MIU5y&IDPќCFs \=Saqs ܆wwCiwtFwwL4gwt_T9SC0G1Ts]0G1Ts]0G5ts_0G5C釡t>sL=u1}xa(E<e)S?/CS?/GϨ?~^TI'䣟Sȿ9yͶӲ'驸ZAq T俭d U;)d?F.*)jʞ9?/_ߌ_~^r~>lB$Ʌ?~^ 둵g\KzQ~.tOܯ4t&GYgݯOk*8]٘G/1WSÌ2Xl,[s˷:=!xgǺ??q/n1O 'Rc |So^.˯2-U'mi^Aq1:-s?w酳3>oS#w`<s?H7W_2^̸%T'I:@ 7@Rms?0fev'B:F PP PT! ! .>D(@ h0*B&&BdǙ%({> g9'bgL*)t7ڝ'Tkܰ<Ek, S dmh9\[4D7*)0Sw?#Xk"f2ǎ~_~n$Ҏl[0k\6L$$j g]_/vX`ZFwWzsv W/s7~mIy~J4 zDD) B pB@ ޕZs4cjSV86KIhp ~D&y!$N"e R)={I OI(X |]&$ < $QZ!AtdASs~:i=u7i=H@jH g=B#$QvЮڴ 2$D: ztE\Sgg~ꌳ -$d {#upK{}SO(5|T9 ?TU4pٿ~8=ٿꞗOGバT|wo 5u<SO#i,*,쮧+WIU\ʘ,]<9Vޘp$]8]~uӉ7M<)B*k^KN~TL>[&Ud}⦯)3@?⚾)k?dA@{SZ65tSWʍ&Hrt >Ke մY jH57MlWʀ#@"vjnO]^mhPІqNS0 4@B: 4B>i%O0(:@.%1tNS=1NI:h 8T 8LRG0\5|4!  G0# bjI>J᠑OFmqUQen8Gfc<vߧӍuZV+]2 kyX]Oh"t$Uc1驟皆FG}4H&5YH&.0$Ԃ>\_.*b79Aĵ$K:6lb!U9hP,d7H犸PFdH?Ή0&j'] hd%%*)PPY@O|PE@z(IPPJ FӠ` ghD8E(R*2Qs,1LUGE\IVztLy:`"DĢ4!,A(P@ tC$ ɀ:Uej@#榮PIPK;":ւ4 ?ɥm'I2L4 DJ@Dm}%ē!SW#@[@~x"LFAkɠ$~Ш8SA;-Z 'äDؑprё0nM2N '>,17Y_DϨih&` ؓPeh7?b~֒]`;S<XUl6SP'"7 rsT3c}R& u@>AдO׸h@&vO#RՀL7S~'mD4FC@HԐڪ[\j4\Ŝx$oM楴jP#O].A'Cz)5⩴KDI (Et0~J*)wUS #H! Q扄FL,y0\Dzf1y4<[sMNQ0'tOպv<Q^l5 enwIhOPÚH:@@G+a|Znly c^~k8 \Τ'Nk|COIkHTY+?D 5dd`%I q yA3CLSRf~QL&4iOUVۚ:7 0L-ym7C[ 4Xu=t/)3TahFfH&|Ig^-t-@B( @@@G@QO%@w#q (H=/U4:(h CLܟTMKpBq1:D^I2wZ}!T&%ՠw5aB~.E>Om*r:uk}HPSCA5ye#_gALGBНXbh>D\L29FtsD:SATͰ0Sx ~cRs$:?"p5Ėk#5g g0ڌng?EYp-QaQBޣKpH w(M.Y~>c[q}P%!D(P t aI#D_@ @kТwD TSOCzdxzQ!$@ >pԠD0 ˙ە:J2B *WNk^h#O泍FcOxLjv;TiQÆ-kLSÙ$=d vY\[Ep\z`,vߺ2AI+6nRgAv"~IosMI؟D=֔-&>_R?#QRAv2?%$5Sp[<e;w<ћk8 | aOD sJ '|$@ 4@HQM@@ 0aObQ@ < "J)w( NPo2T0 SA$$ǚ2ۭ3D"E$F@' Vk@GH2GyXiEO\C ==BڍI>F#L'k7$G, $oS\NNng*ҐhˈxB[)g4C+L >J@KA:i@\k H;p#*mjʖHE֭}ܞ)Nݢx r ( @N@@P=TSE D$ET rSZ72Yqe=#pz 4$ B%dB4"u艊 4Y9G+5AO@ $xU]1PL??Z R߄q>JV t R"Gm7 qp|.oŔ|vt46W?* 2׻柯ŠѵPQi!L_XMs9if̟KfB疖 Lxo[kI.=!aOx'?ܴ9IVuu2'-X$)+7qy,?RZǚV PCe. ]P>@*S@w@P!v@'/PTI nވA$T@@6K5YnQQ H˵E^Pd",e$y*;k``k llmj*y89h*tTyqX1#3,CI%tpʱ/\4::K k~h/JΛ9Nxflickrapi-version-2.4/tests/test_cache.py000066400000000000000000000022221323562532600206310ustar00rootroot00000000000000# -*- encoding: utf-8 -*- '''Unittest for the flickrapi.cache module''' import unittest import sys import time import six # Make sure the flickrapi module from the source distribution is used sys.path.insert(0, '..') import flickrapi class TestCache(unittest.TestCase): def test_store_retrieve(self): cache = flickrapi.SimpleCache() key = 'abc' value = 'def' cache.set(key, value) self.assertEqual(value, cache.get(key)) def test_expire(self): cache = flickrapi.SimpleCache(timeout=1) key = 'abc' cache.set(key, 'def') time.sleep(1.1) self.assertFalse(key in cache) def test_delete(self): cache = flickrapi.SimpleCache() key = 'abc' cache.set(key, 'def') cache.delete(key) self.assertFalse(key in cache) def test_max_entries(self): max_entries = 90 cache = flickrapi.SimpleCache(max_entries=max_entries) for num in six.moves.range(100): cache.set('key-%03d' % num, 'value') removed = float(max_entries) / cache.cull_frequency self.assertEqual(100 - removed, len(cache)) flickrapi-version-2.4/tests/test_call_builder.py000066400000000000000000000015561323562532600222200ustar00rootroot00000000000000import unittest try: from unittest import mock except ImportError: import mock # noqa: F401 class CallBuilderTest(unittest.TestCase): def setUp(self): import flickrapi self.f = mock.MagicMock(spec=flickrapi.FlickrAPI) def test_building(self): from flickrapi.call_builder import CallBuilder cb = CallBuilder(self.f) three = cb.one.two.three self.assertEqual('flickr.one.two.three', three.method_name) def test_calling(self): from flickrapi.call_builder import CallBuilder cb = CallBuilder(self.f) cb.one.two.three(a='b') self.f.do_flickr_call.assert_called_with('flickr.one.two.three', a='b') def test_name(self): from flickrapi.call_builder import CallBuilder cb = CallBuilder(self.f) self.assertEqual('three', cb.one.two.three.__name__) flickrapi-version-2.4/tests/test_core.py000066400000000000000000000017351323562532600205260ustar00rootroot00000000000000# -*- coding: utf-8 -*- import unittest from flickrapi import core class TestFlickrapiCore(unittest.TestCase): def test_make_bytes(self): d = core.make_bytes({'ascii-key': 'ascii-value', 'nonascii-ključ': 'nonascii-вредност', 'integer value': 1431, 'float value': 4.32}) keys = sorted(d.keys()) self.assertEqual(keys, ['ascii-key', 'float value', 'integer value', 'nonascii-ključ']) self.assertEqual(d['ascii-key'], b'ascii-value') self.assertEqual(d['float value'], b'4.32') self.assertEqual(d['integer value'], b'1431') self.assertEqual(d['nonascii-ključ'], b'nonascii-\xd0\xb2\xd1\x80\xd0\xb5\xd0\xb4\xd0\xbd\xd0\xbe\xd1\x81\xd1\x82') if __name__ == '__main__': unittest.main() flickrapi-version-2.4/tests/test_flickrapi.py000066400000000000000000000432151323562532600215410ustar00rootroot00000000000000#!/usr/bin/env python # -*- encoding: utf-8 -*- '''Unittest for the FlickrAPI. Far from complete, but it's a start. ''' import json import logging import pkg_resources import sys import types import unittest import urllib import six import responses from six.moves.urllib.parse import quote_plus, parse_qs import flickrapi #flickrapi.set_log_level(logging.FATAL) flickrapi.set_log_level(logging.DEBUG) from common_responses import * print("Testing FlickrAPI version %s" % flickrapi.__version__) # Some useful constants EURO_UNICODE = u'\u20ac' EURO_UTF8 = EURO_UNICODE.encode('utf-8') U_UML_UNICODE = u'\u00fc' U_UML_UTF8 = U_UML_UNICODE.encode('utf-8') key = u'ecd01ab8f00faf13e1f8801586e126fd' secret = u'2ee3f558fd79f292' logging.basicConfig() LOG = logging.getLogger(__name__) 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") class SuperTest(unittest.TestCase): '''Superclass for unittests, provides useful methods.''' def setUp(self): super(SuperTest, self).setUp() # Reference to the class under test. Makes it easier to switch to one # of the contributed subclasses and run the entire suite. #self.clasz = flickrapi.contrib.PersistentFlickrAPI self.clasz = flickrapi.FlickrAPI self.f = self.clasz(key, secret) self.f_noauth = self.clasz(key, secret) # Remove/prevent any unwanted tokens del self.f.token_cache.token def print_auth_message(self, frob, perms): sys.stderr.write("Your browser starts, press ENTER after " "authentication") return self.f.validate_frob(frob, perms) def assertUrl(self, expected_protocol, expected_host, expected_path, expected_query_arguments, actual_url): '''Asserts that the 'actual_url' matches the given parts.''' # Test the URL part by part (urltype, rest) = urllib.splittype(actual_url) self.assertEqual(expected_protocol, urltype) (hostport, path) = urllib.splithost(rest) self.assertEqual(expected_host, hostport) (path, query) = urllib.splitquery(path) self.assertEqual(expected_path, path) attrvalues = query.split('&') attribs = dict(av.split('=') for av in attrvalues) self.assertEqual(expected_query_arguments, attribs) class MockedTest(SuperTest): """Flickr test in which all HTTP requests are mocked.""" def setUp(self): super(MockedTest, self).setUp() self.mock = responses.RequestsMock(assert_all_requests_are_fired=True) self.mock.start() def tearDown(self): self.mock.stop() self.mock.reset() super(MockedTest, self).tearDown() def expect(self, params=None, body='', status=200, content_type='text/xml', method='POST', match_querystring=True, urlbase=None): """Mocks an expected HTTP query with Responses.""" if urlbase is None: urlbase = self.f.REST_URL param_test_callback = None url = urlbase if params: params.setdefault('format', 'rest') params.setdefault('nojsoncallback', '1') if method == 'GET': # The parameters should be on the URL. qp = quote_plus qs = '&'.join('%s=%s' % (qp(key), qp(six.text_type(value).encode('utf-8'))) for key, value in sorted(params.items())) if qs: url = '%s?%s' % (urlbase, qs) self.mock.add(method=method, url=url, body=body, status=status, content_type=content_type, match_querystring=match_querystring) else: # The parameters should be in the request body, not on the URL. if params is not None: expect_params = {key.encode('utf8'): [value.encode('utf8')] for key, value in params.items()} def param_test_callback(request): # This callback can only handle x-www-form-urlencoded requests. self.assertEqual('application/x-www-form-urlencoded', request.headers['Content-Type'].decode('utf8')) actual_params = parse_qs(request.body) if params is None: self.assertFalse(actual_params) else: self.assertEqual(actual_params, expect_params) headers = {'Content-Type': 'text/xml'} return (status, headers, body) self.mock.add_callback(method=method, url=url, callback=param_test_callback, content_type=content_type, match_querystring=match_querystring) def expect_auth(self, perms): self.mock.add( method='POST', url=self.f.flickr_oauth.REQUEST_TOKEN_URL, body=b'oauth_callback_confirmed=true&' b'oauth_token=cafef00d089843641-e04b4114a40fe037&' b'oauth_token_secret=cafef00dc551b5d7', status=200, content_type='text/plain;charset=UTF-8', match_querystring=False) self.mock.add( method='POST', url=self.f.flickr_oauth.ACCESS_TOKEN_URL, body=u'fullname=एकाइ परीक्षकs&&' u'oauth_token=cafef00d089843641-e04b4114a40fe037&' u'oauth_token_secret=cafef00dc551b5d7&' u'username=unittester&' u'user_nsid=1234'.encode('utf-8'), status=200, content_type='text/plain;charset=UTF-8', match_querystring=False) class FlickrApiTest(MockedTest): def test_repr(self): '''Class name and API key should be in repr output''' r = repr(self.f) self.assertTrue('FlickrAPI' in r) self.assertTrue(key in r) def test_defaults(self): '''Tests _supply_defaults.''' data = self.f._supply_defaults({'foo': 'bar', 'baz': None, 'token': None}, {'baz': 'foobar', 'room': 'door'}) self.assertEqual({'foo': 'bar', 'room': 'door'}, data) def test_unauthenticated(self): '''Test we can access public photos without any authentication/authorization.''' # make sure this test is made without a valid token in the cache del self.f.token_cache.token self.expect({'method': 'flickr.photos.getInfo', 'photo_id': '7955646798'}, PHOTO_XML) self.f.photos.getInfo(photo_id='7955646798') def test_simple_search(self): '''Test simple Flickr search''' self.expect({'method': 'flickr.photos.search', 'tags': 'kitten'}, KITTEN_SEARCH_XML) # We expect to be able to find kittens result = self.f.photos.search(tags='kitten') total = int(result.find('photos').attrib['total']) self.assertTrue(total > 0) def test_token_constructor(self): '''Test passing a token to the constructor''' token = flickrapi.auth.FlickrAccessToken(u'123-abc-def', u'token_secret', u'read', u'fullname', u'username', u'user_nsid') # Pass the token flickr = self.clasz(key, secret, token=token) # It should be in the in-memory token cache now self.assertEqual(token, flickr.token_cache.token) # But not in the on-disk token cache self.assertNotEqual(token, flickrapi.OAuthTokenCache(key)) def test_upload_without_filename(self): '''Uploading a file without filename is impossible''' self.assertRaises(flickrapi.IllegalArgumentException, self.f.upload, '') self.assertRaises(flickrapi.IllegalArgumentException, self.f.upload, None) def test_upload(self): photo = pkg_resources.resource_filename(__name__, 'photo.jpg') from requests_toolbelt.multipart.encoder import MultipartEncoder def upload_test_callback(request): ct = request.headers['Content-Type'] self.assertTrue(ct.startswith('multipart/form-data; boundary=')) self.assertIsInstance(request.body, MultipartEncoder) self.assertEqual(request.body.fields['is_public'], b'0') self.assertEqual(request.body.fields['is_friend'], b'0') self.assertEqual(request.body.fields['is_family'], b'0') self.assertEqual(request.body.fields['content_type'], b'2') self.assertEqual(request.body.fields['title'], b'photo.jpg') self.assertEqual(request.body.fields['api_key'], key.encode('utf8')) self.assertIn('photo', request.body.fields) headers = {'Content-Type': 'text/xml'} return (200, headers, UPLOAD_XML) self.expect_auth(perms='delete') self.mock.add_callback(method='POST', url=self.f.UPLOAD_URL, callback=upload_test_callback) self.f.authenticate_for_test(perms='delete') self.f.upload(photo, is_public=0, is_friend=0, is_family=0, content_type=2) def test_store_token(self): '''Tests that store_token=False FlickrAPI uses SimpleTokenCache''' flickr = self.clasz(key, secret, store_token=False) self.assertTrue(isinstance(flickr.token_cache, flickrapi.SimpleTokenCache), 'Token cache should be SimpleTokenCache, not %r' % flickr.token_cache) def test_wrap_in_parser(self): '''Tests wrap_in_parser''' test = {'wrapped': False} def to_wrap(format, test_param): self.assertEqual('rest', format) self.assertEqual('test_value', test_param) test['wrapped'] = True return '' rst = self.f._wrap_in_parser(to_wrap, parse_format='xmlnode', format='xmlnode', test_param='test_value') self.assertEqual('5', rst.element[0]['photo_id']) self.assertTrue(test['wrapped'], 'Expected wrapped function to be called') def test_wrap_in_parser_no_format(self): '''Tests wrap_in_parser without a format in the wrapped arguments''' test = {'wrapped': False} def to_wrap(test_param): self.assertEqual('test_value', test_param) test['wrapped'] = True return '' rst = self.f._wrap_in_parser(to_wrap, parse_format='xmlnode', test_param='test_value') self.assertEqual('5', rst.element[0]['photo_id']) self.assertTrue(test['wrapped'], 'Expected wrapped function to be called') class FormatsTest(SuperTest): '''Tests the different parsed formats. We have to test ElementTree in a bit of a strange way in order to support all current flavours of (c)ElementTree. ''' def test_default_format(self): '''Test that the default format is etree''' f = self.clasz(key, secret) etree = f.photos.getInfo(photo_id=u'2333478006') self.assertEqual(type(etree), type(ElementTree.Element(None))) def test_etree_format_happy(self): '''Test ETree format''' etree = self.f_noauth.photos.getInfo(photo_id=u'2333478006', format='etree') self.assertEqual(type(etree), type(ElementTree.Element(None))) def test_etree_format_error(self): '''Test ETree format in error conditions''' self.assertRaises(flickrapi.exceptions.FlickrError, self.f_noauth.photos_getInfo, format='etree') def test_etree_default_format(self): '''Test setting the default format to etree''' f = self.clasz(key, secret, format='etree') etree = f.photos_getInfo(photo_id=u'2333478006') self.assertEqual(type(etree), type(ElementTree.Element(None))) def test_xmlnode_format(self): '''Test XMLNode format''' node = self.f_noauth.photos_getInfo(photo_id=u'2333478006', format='xmlnode') self.assertNotEqual(None, node.photo[0]) def test_xmlnode_format_error(self): '''Test XMLNode format in error conditions''' self.assertRaises(flickrapi.exceptions.FlickrError, self.f_noauth.photos_getInfo, format='xmlnode') def test_explicit_format(self): '''Test explicitly requesting a certain unparsed format''' xml = self.f.photos_search(tags='kitten', format='rest') self.assertTrue(isinstance(xml, six.binary_type), 'XML is type %r, not %r' % (type(xml), six.binary_type)) # Try to parse it rst = flickrapi.XMLNode.parse(xml, False) self.assertTrue(int(rst.photos[0]['total']) > 0) def test_json_format(self): '''Test json format (no callback)''' data = self.f_noauth.photos.getInfo(photo_id='2333478006', format='json') photo = json.loads(data.decode('utf-8')) location = photo['photo']['location'] if 'locality' not in location: raise KeyError('locality not in %r' % location) locality = location['locality'] self.assertEqual(photo['photo']['id'], '2333478006') self.assertEqual(locality['_content'], 'Amsterdam') def test_parsed_json_format(self): '''Test parsed json format''' photo = self.f_noauth.photos.getInfo(photo_id='2333478006', format='parsed-json') location = photo['photo']['location'] if 'locality' not in location: raise KeyError('locality not in %r' % location) locality = location['locality'] self.assertEqual(photo['photo']['id'], '2333478006') self.assertEqual(locality['_content'], 'Amsterdam') def test_json_callback_format(self): '''Test json format (with callback)''' data = self.f_noauth.photos.getInfo(photo_id='2333478006', format='json', jsoncallback='foobar') decoded = data.decode('utf-8') self.assertEqual('foobar({', decoded[:8]) class RealWalkerTest(SuperTest): """Test walk* functions, on the real, live Flickr API.""" def test_walk_set(self): # Check that we get a generator, and not a list of results. gen = self.f.walk_set('72157611690250298', per_page=8) self.assertEqual(types.GeneratorType, type(gen)) # I happen to know that that set contains 24 photos, and it is # very unlikely that this will ever change (photos of a past # event) self.assertEqual(24, len(list(gen))) class MockedWalkerTest(MockedTest): """Tests walk* functions on a mocked API for data stability.""" def test_walk(self): # We expect the API to be called more than once, given that there are more results # than the per_page parameter allows to fetch in one request. self.expect({'method': 'flickr.photos.search', 'per_page': '4', 'page': '1'}, WALK_PAGE_1_XML) self.expect({'method': 'flickr.photos.search', 'per_page': '4', 'page': '2'}, WALK_PAGE_2_XML) self.expect({'method': 'flickr.photos.search', 'per_page': '4', 'page': '3'}, WALK_PAGE_3_XML) # Check that we get a generator, and not a list of results. gen = self.f.walk(per_page=4) self.assertEqual(types.GeneratorType, type(gen)) ids = [p.get('id') for p in gen] self.assertEqual(['11192308693', '11853287542', '11627471650', '11161255944', '21627488910', '21884772401', '21161270134', '21964432216', '32001923265', '31964437076', '32001922675'], ids) class TokenCachePathTest(MockedTest): def test_token_cache_path(self): """Test that the FlickrAPI actually uses the token cache location.""" import tempfile import shutil import os.path tmpdir = tempfile.mkdtemp() try: self.f = flickrapi.FlickrAPI(key, secret, token_cache_location=tmpdir) # We have to authenticate so that a token is actually written. self.expect_auth(perms=u'read') self.f.authenticate_for_test(perms=u'read') # Check the token cache exists on disk. cache_path = os.path.join(tmpdir, 'oauth-tokens.sqlite') self.assertTrue(os.path.exists(cache_path)) # Check the token is stored correctly. self.assertEqual('cafef00d089843641-e04b4114a40fe037', self.f.token_cache.token.token) finally: shutil.rmtree(tmpdir) if __name__ == '__main__': unittest.main() flickrapi-version-2.4/tests/test_shorturl.py000066400000000000000000000015201323562532600214500ustar00rootroot00000000000000#!/usr/bin/env python import unittest from flickrapi import shorturl class ShortUrlTest(unittest.TestCase): '''Tests the shorturl module.''' def test_encoding(self): '''Test ID to Base58 encoding.''' self.assertEqual(shorturl.encode(u'4325695128'), u'7Afjsu') self.assertEqual(shorturl.encode(u'2811466321'), u'5hruZg') def test_decoding(self): '''Test Base58 to ID decoding.''' self.assertEqual(shorturl.decode(u'7Afjsu'), u'4325695128') self.assertEqual(shorturl.decode(u'5hruZg'), u'2811466321') def test_short_url(self): '''Test photo ID to short URL conversion.''' self.assertEqual(shorturl.url(u'4325695128'), u'http://flic.kr/p/7Afjsu') self.assertEqual(shorturl.url(u'2811466321'), u'http://flic.kr/p/5hruZg') flickrapi-version-2.4/tests/test_tokencache.py000066400000000000000000000073011323562532600216750ustar00rootroot00000000000000# -*- encoding: utf-8 -*- import unittest class SimpleTokenCacheTest(unittest.TestCase): def setUp(self): from flickrapi.tokencache import SimpleTokenCache self.tc = SimpleTokenCache() def test_get_set_del(self): self.assertIsNone(self.tc.token) self.tc.token = 'nümbér' self.assertEquals(self.tc.token, 'nümbér') del self.tc.token self.assertIsNone(self.tc.token) self.tc.token = 'nümbér' self.tc.forget() self.assertIsNone(self.tc.token) class TokenCacheTest(unittest.TestCase): def setUp(self): import tempfile from flickrapi.tokencache import TokenCache # Use mkdtemp() for backward compatibility with Python 2.7 self.tc_path = tempfile.mkdtemp() self.tc = TokenCache('123-456', path=self.tc_path) def tearDown(self): tc_path = getattr(self, 'tc_path', None) if tc_path is not None: import shutil shutil.rmtree(self.tc_path) def test_get_set_del(self): self.assertIsNone(self.tc.token) # Check setting token, both in memory and on disk. self.tc.token = u'nümbér' self.assertEquals(self.tc.token, u'nümbér') on_disk = open(self.tc.get_cached_token_filename(), 'rb').read() self.assertEquals(on_disk.decode('utf8'), u'nümbér') # Erase from in-RAM cache and try again, to read from disk. self.tc.memory.clear() self.assertEquals(self.tc.token, u'nümbér') del self.tc.token self.assertIsNone(self.tc.token) self.tc.token = u'nümbér' self.tc.forget() self.assertIsNone(self.tc.token) def test_username(self): """Username should impact the location of the cache on disk.""" from flickrapi.tokencache import TokenCache user_tc = TokenCache(u'123-456', username=u'frøbel', path=self.tc_path) tc_path = self.tc.get_cached_token_filename() user_path = user_tc.get_cached_token_filename() self.assertNotEquals(tc_path, user_path) self.assertNotIn(u'frøbel', tc_path) self.assertIn(u'frøbel', user_path) class OAuthTokenCache(unittest.TestCase): def setUp(self): import tempfile from flickrapi.tokencache import OAuthTokenCache from flickrapi.auth import FlickrAccessToken # Use mkdtemp() for backward compatibility with Python 2.7 self.tc_path = tempfile.mkdtemp() self.tc = OAuthTokenCache('123-456', path=self.tc_path) self.token = FlickrAccessToken( u'nümbér', u'səcret-tøken', u'read', u'My Full Name™', u'üsernåme', u'user—nsid', ) def tearDown(self): tc_path = getattr(self, 'tc_path', None) if tc_path is not None: import shutil shutil.rmtree(self.tc_path) def test_get_set_del(self): self.assertIsNone(self.tc.token) # Check setting token self.tc.token = self.token self.assertEquals(self.tc.token.token, u'nümbér') # Erase from in-RAM cache and try again, to read from disk. self.tc.RAM_CACHE.clear() self.assertEquals(self.tc.token.token, u'nümbér') self.assertEquals(self.tc.token.token_secret, u'səcret-tøken') self.assertEquals(self.tc.token.access_level, u'read') self.assertEquals(self.tc.token.fullname, u'My Full Name™') self.assertEquals(self.tc.token.username, u'üsernåme') self.assertEquals(self.tc.token.user_nsid, u'user—nsid') del self.tc.token self.assertIsNone(self.tc.token) self.tc.token = self.token self.tc.forget() self.assertIsNone(self.tc.token) flickrapi-version-2.4/tests/test_xmlnode.py000066400000000000000000000112021323562532600212320ustar00rootroot00000000000000# -*- encoding: utf-8 -*- '''Unittest for the flickrapi.tokencache module''' import unittest import sys # Make sure the flickrapi module from the source distribution is used sys.path.insert(0, '..') from flickrapi.xmlnode import XMLNode # This XML is used in the tests xml = ''' threesixtyfive | day 115 An eye for an eye? 3 365 365days threesixtyfive me selfportrait sybren lens:type=1755mmf28isusm merge twin tongue amsterdam gimp thegimp http://www.flickr.com/photos/sybrenstuvel/2141453991/ ''' group_info_xml = ''' Flickr API A Flickr group for Flickr API projects. Driving awareness of the Flickr API, projects that use it and those incredible ideas that programmatically exposed systems produce. Think Google API + Amazon API + Flickr API with a bit of GMail thrown in. The developers of Flickr rightly pointed out they want to keep technical discussions directly related to the API on the mailing list. 5180 3 ''' class TestXMLNode(unittest.TestCase): def setUp(self): self.doc = XMLNode.parse(xml, True) def testXmlStorage(self): '''Tests that the XML stored in the parsed document is equal to the XML fed to it. ''' self.assertEqual(self.doc.xml, xml) def testParsing(self): '''Tests that parsing of XML works as expected.''' self.assertEqual(self.doc.photo[0]['id'], '2141453991') self.assertEqual(self.doc.photo[0].comments[0].text, '3') self.assertEqual(self.doc.photo[0].comments[0].name, u'comments') self.assertEqual(self.doc.photo[0].owner[0]['username'], u"Sybren Stüvel") def testGroupInfoXml(self): '''This XML exposed a bug in 1.0, should parse okay now.''' XMLNode.parse(group_info_xml) def testMoreParsing(self): '''Tests parsing of XML. This used to be a doctest, but it was very hard to make it compatible with both Python 2 and 3. ''' xml_str = ''' Name0 Name1 ''' f = XMLNode.parse(xml_str) self.assertEqual(f.name, 'xml') self.assertEqual(f['foo'], '32') self.assertEqual(f.taggy[0].name, 'taggy') self.assertEqual(f.taggy[0]["bar"], '10') self.assertEqual(f.taggy[0].text, 'Name0') self.assertEqual(f.taggy[1].name, 'taggy') self.assertEqual(f.taggy[1]["bar"], '11') self.assertEqual(f.taggy[1]["baz"], '12') flickrapi-version-2.4/tox.ini000066400000000000000000000010011323562532600163200ustar00rootroot00000000000000[tox] envlist = py{27,34,35,36},pypy,qa,doc skip_missing_interpreters = True [testenv] commands = python setup.py install coverage run -m py.test -v --cov flickrapi -r wsx deps= .[test] [testenv:py36] commands = python setup.py install coverage run -m py.test --doctest-modules flickrapi tests -v -r wsx deps= .[test] [testenv:qa] commands = flake8 deps = .[qa] [testenv:doc] commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html changedir = doc deps = .[docs] flickrapi-version-2.4/update_version.sh000077500000000000000000000005171323562532600204060ustar00rootroot00000000000000#!/bin/bash if [ -z "$1" ]; then echo "Usage: $0 new-version" >&2 exit 1 fi sed "s/__version__\s=\s'[^']*'/__version__ = '$1'/" -i flickrapi/__init__.py git diff echo echo "Don't forget to commit and tag:" echo git commit -m \'Bumped version to $1\' flickrapi/__init__.py echo git tag -a version-$1 -m \'Tagged version $1\'