flickrapi-1.2/0000755000175000017500000000000011107627065012713 5ustar sybrensybrenflickrapi-1.2/LICENSE0000644000175000017500000000251511065261043013714 0ustar sybrensybrenCopyright (c) 2007 by the respective coders, see http://flickrapi.sf.net/ 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. flickrapi-1.2/PKG-INFO0000644000175000017500000000152211107627065014010 0ustar sybrensybrenMetadata-Version: 1.0 Name: flickrapi Version: 1.2 Summary: The official Python interface to the Flickr API Home-page: http://flickrapi.sourceforge.net/ Author: Sybren A. Stuvel Author-email: sybren@stuvel.eu License: Python Description: The easiest to use, most complete, and most actively developed Python interface to the Flickr API.It includes support for authorized and non-authorized access, uploading and replacing photos, and all Flickr API functions. Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Python License (CNRI Python License) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries :: Python Modules flickrapi-1.2/flickrapi.egg-info/0000755000175000017500000000000011107627065016351 5ustar sybrensybrenflickrapi-1.2/flickrapi.egg-info/PKG-INFO0000644000175000017500000000152211107627065017446 0ustar sybrensybrenMetadata-Version: 1.0 Name: flickrapi Version: 1.2 Summary: The official Python interface to the Flickr API Home-page: http://flickrapi.sourceforge.net/ Author: Sybren A. Stuvel Author-email: sybren@stuvel.eu License: Python Description: The easiest to use, most complete, and most actively developed Python interface to the Flickr API.It includes support for authorized and non-authorized access, uploading and replacing photos, and all Flickr API functions. Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Python License (CNRI Python License) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Multimedia :: Graphics Classifier: Topic :: Software Development :: Libraries :: Python Modules flickrapi-1.2/flickrapi.egg-info/requires.txt0000644000175000017500000000004211107627065020745 0ustar sybrensybren [ElementTree] elementtree>=1.2.6flickrapi-1.2/flickrapi.egg-info/dependency_links.txt0000644000175000017500000000000111107627065022417 0ustar sybrensybren flickrapi-1.2/flickrapi.egg-info/top_level.txt0000644000175000017500000000001211107627065021074 0ustar sybrensybrenflickrapi flickrapi-1.2/flickrapi.egg-info/SOURCES.txt0000644000175000017500000000111211107627065020230 0ustar sybrensybrenLICENSE MANIFEST README UPGRADING _setup.py ez_setup.py runtests setup.py doc/GNUmakefile doc/documentation.css doc/flickrapi.rst doc/html4css1.css flickrapi/__init__.py flickrapi/cache.py flickrapi/exceptions.py flickrapi/multipart.py flickrapi/reportinghttp.py flickrapi/tokencache.py flickrapi/xmlnode.py flickrapi.egg-info/PKG-INFO flickrapi.egg-info/SOURCES.txt flickrapi.egg-info/dependency_links.txt flickrapi.egg-info/requires.txt flickrapi.egg-info/top_level.txt tests/test_cache.py tests/test_flicrkapi.py tests/test_multipart.py tests/test_tokencache.py tests/test_xmlnode.pyflickrapi-1.2/setup.py0000644000175000017500000000055111065261043014417 0ustar sybrensybren#!/usr/bin/env python '''Python distutils install script. Run with "python setup.py install" to install FlickrAPI ''' import ez_setup ez_setup.use_setuptools() import sys # Check the Python version (major, minor) = sys.version_info[:2] if major < 2 or (major == 2 and minor < 4): raise SystemExit("Sorry, Python 2.4 or newer required") import _setup flickrapi-1.2/_setup.py0000644000175000017500000000703711066250730014566 0ustar sybrensybren# -*- encoding: utf-8 -*- '''The setup code is split into two sections, one in setup.py which contains very simple Python code and checks the Python version, and this file, which contains code only parsable by 2.4+. ''' __author__ = 'Sybren A. Stuvel' from setuptools import setup, Distribution import os import sys try: import docutils.core except ImportError: docutils = None from flickrapi import __version__ # This will be set to True when either the documentation is already # there, or if we can build it. documentation_available = False class OurDistribution(Distribution): '''Distribution that also generates the flickrapi.html''' def run_command(self, command): '''Builds the documentation if needed, then passes control to the superclass' run_command(...) method. ''' if command == 'install_data' and docutils: print 'creating doc/flickrapi.html' docutils.core.publish_file(writer_name='html', source=open('doc/flickrapi.rst'), source_path='doc', destination=open('doc/flickrapi.html', 'w'), destination_path='doc', settings_overrides={'stylesheet_path': 'doc/documentation.css'} ) Distribution.run_command(self, command) data = { 'name': 'flickrapi', 'version': __version__, 'url': 'http://flickrapi.sourceforge.net/', 'author': 'Beej Jorgensen and Sybren A. Stuvel', 'author_email': 'beej@beej.us', 'maintainer': 'Sybren A. Stuvel', 'maintainer_email': 'sybren@stuvel.eu', 'description': 'The official 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'], 'data_files': [], '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', 'Topic :: Multimedia :: Graphics', 'Topic :: Software Development :: Libraries :: Python Modules', ], 'extras_require': { 'ElementTree': ["elementtree>=1.2.6"], }, 'distclass': OurDistribution } (major, minor) = sys.version_info[:2] if major == 2 and minor < 5: # We still want to use this function, but Python 2.4 doesn't have # it built-in. def all(iterator): for item in iterator: if not item: return False return True alldocs = ['doc/flickrapi.html', 'doc/documentation.css', 'doc/html4css1.css'] if docutils or all(os.path.exists(doc) for doc in alldocs): # Only include documentation if it can be built, or if it has been # built already data['data_files'].append(('share/doc/flickrapi-%s' % __version__, alldocs)) documentation_available = True else: print "=======================================================================" print "WARNING: Unable to import docutils, documentation will not be included" print "Documentation for the latest version can be found at" print "http://flickrapi.sourceforge.net/documentation" print "=======================================================================" print setup(**data) flickrapi-1.2/flickrapi/0000755000175000017500000000000011107627065014657 5ustar sybrensybrenflickrapi-1.2/flickrapi/multipart.py0000644000175000017500000000551511065261043017251 0ustar sybrensybren# -*- encoding: utf-8 -*- '''Module for encoding data as form-data/multipart''' import os import base64 class Part(object): '''A single part of the multipart data. >>> Part({'name': 'headline'}, 'Nice Photo') ... # doctest: +ELLIPSIS >>> image = file('tests/photo.jpg') >>> Part({'name': 'photo', 'filename': image}, image.read(), 'image/jpeg') ... # doctest: +ELLIPSIS ''' def __init__(self, parameters, payload, content_type=None): self.content_type = content_type self.parameters = parameters self.payload = payload def render(self): '''Renders this part -> List of Strings''' parameters = ['%s="%s"' % (k, v) for k, v in self.parameters.iteritems()] lines = ['Content-Disposition: form-data; %s' % '; '.join(parameters)] if self.content_type: lines.append("Content-Type: %s" % self.content_type) lines.append('') if isinstance(self.payload, unicode): lines.append(self.payload.encode('utf-8')) else: lines.append(self.payload) return lines class FilePart(Part): '''A single part with a file as the payload This example has the same semantics as the second Part example: >>> FilePart({'name': 'photo'}, 'tests/photo.jpg', 'image/jpeg') ... #doctest: +ELLIPSIS ''' def __init__(self, parameters, filename, content_type): parameters['filename'] = filename imagefile = open(filename, 'rb') payload = imagefile.read() imagefile.close() Part.__init__(self, parameters, payload, content_type) def boundary(): """Generate a random boundary, a bit like Python 2.5's uuid module.""" bytes = os.urandom(16) return base64.b64encode(bytes, 'ab').strip('=') class Multipart(object): '''Container for multipart data''' def __init__(self): '''Creates a new Multipart.''' self.parts = [] self.content_type = 'form-data/multipart' self.boundary = boundary() def attach(self, part): '''Attaches a part''' self.parts.append(part) def __str__(self): '''Renders the Multipart''' lines = [] for part in self.parts: lines += ['--' + self.boundary] lines += part.render() lines += ['--' + self.boundary + "--"] return '\r\n'.join(lines) def header(self): '''Returns the top-level HTTP header of this multipart''' return ("Content-Type", "multipart/form-data; boundary=%s" % self.boundary) flickrapi-1.2/flickrapi/xmlnode.py0000644000175000017500000000545411065261043016700 0ustar sybrensybren '''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 >>> xml_str = ''' ... Name0 ... Name1 ... ''' >>> f = XMLNode.parse(xml_str) >>> f.name u'xml' >>> f['foo'] u'32' >>> f.taggy[0].name u'taggy' >>> f.taggy[0]["bar"] u'10' >>> f.taggy[0].text u'Name0' >>> f.taggy[1].name u'taggy' >>> f.taggy[1]["bar"] u'11' >>> f.taggy[1]["baz"] u'12' """ 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-1.2/flickrapi/__init__.py0000755000175000017500000006310711065261043016773 0ustar sybrensybren#!/usr/bin/env python # -*- coding: utf-8 -*- '''A FlickrAPI interface. See `the FlickrAPI homepage`_ for more info. .. _`the FlickrAPI homepage`: http://flickrapi.sf.net/ ''' __version__ = '1.2' __all__ = ('FlickrAPI', 'IllegalArgumentException', 'FlickrError', 'CancelUpload', 'XMLNode', 'set_log_level', '__version__') __author__ = u'Sybren St\u00fcvel'.encode('utf-8') # Copyright (c) 2007 by the respective coders, see # http://flickrapi.sf.net/ # # 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 sys import md5 import urllib import urllib2 import mimetools import os.path import logging import copy import webbrowser from flickrapi.tokencache import TokenCache, SimpleTokenCache from flickrapi.xmlnode import XMLNode from flickrapi.multipart import Part, Multipart, FilePart from flickrapi.exceptions import * from flickrapi.cache import SimpleCache from flickrapi import reportinghttp logging.basicConfig() LOG = logging.getLogger(__name__) LOG.setLevel(logging.INFO) def make_utf8(dictionary): '''Encodes all Unicode strings in the dictionary to UTF-8. Converts all other objects to regular strings. Returns a copy of the dictionary, doesn't touch the original. ''' result = {} for (key, value) in dictionary.iteritems(): if isinstance(value, unicode): value = value.encode('utf-8') else: value = str(value) result[key] = value return result def debug(method): '''Method decorator for debugging method calls. Using this automatically sets the log level to DEBUG. ''' LOG.setLevel(logging.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, ...}. Fill by using the # @rest_parser(format) function decorator rest_parsers = {} def rest_parser(format): '''Function decorator, use this to mark a function as the parser for REST as returned by Flickr. ''' def decorate_parser(method): rest_parsers[format] = method return method return decorate_parser class FlickrAPI: """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') """ flickr_host = "api.flickr.com" flickr_rest_form = "/services/rest/" flickr_auth_form = "/services/auth/" flickr_upload_form = "/services/upload/" flickr_replace_form = "/services/replace/" def __init__(self, api_key, secret=None, fail_on_error=None, username=None, token=None, format='etree', store_token=True, cache=False): """Construct a new FlickrAPI instance for a given API key and secret. api_key The API key as obtained from Flickr. secret The secret belonging to the API key. fail_on_error If False, errors won't be checked by the FlickrAPI module. Deprecated, don't use this parameter, just handle the FlickrError exceptions. 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(api_key='123') >>> f.cache = SimpleCache(timeout=5, max_entries=100) """ if fail_on_error is not None: LOG.warn("fail_on_error has been deprecated. Remove this " "parameter and just handle the FlickrError exceptions.") else: fail_on_error = True self.api_key = api_key self.secret = secret self.fail_on_error = fail_on_error self.default_format = format self.__handler_cache = {} if token: # Use a memory-only token cache self.token_cache = SimpleTokenCache() self.token_cache.token = token elif not store_token: # Use an empty memory-only token cache self.token_cache = SimpleTokenCache() else: # Use a real token cache self.token_cache = TokenCache(api_key, username) 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.api_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 def tr(name): '''Translates Flickr names to something that can be called here. >>> tr(u'flickr.photos.getInfo') u'photos_getInfo' ''' return name[7:].replace('.', '_') return [tr(m.text) 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' or not self.fail_on_error: return rsp err = rsp.err[0] raise FlickrError(u'Error: %(code)s: %(msg)s' % err) @rest_parser('etree') def parse_etree(self, rest_xml): '''Parses a REST XML response from Flickr into an ElementTree object.''' try: import xml.etree.ElementTree as ElementTree except ImportError: # For Python 2.4 compatibility: try: import elementtree.ElementTree as ElementTree except ImportError: raise ImportError("You need to install " "ElementTree for using the etree format") rsp = ElementTree.fromstring(rest_xml) if rsp.attrib['stat'] == 'ok' or not self.fail_on_error: return rsp err = rsp.find('err') raise FlickrError(u'Error: %s: %s' % ( err.attrib['code'], err.attrib['msg'])) def sign(self, dictionary): """Calculate the flickr signature for a set of params. data a hash of all the params and values to be hashed, e.g. ``{"api_key":"AAAA", "auth_token":"TTTT", "key": u"value".encode('utf-8')}`` """ data = [self.secret] for key in sorted(dictionary.keys()): data.append(key) datum = dictionary[key] if isinstance(datum, unicode): raise IllegalArgumentException("No Unicode allowed, " "argument %s (%r) should have been UTF-8 by now" % (key, datum)) data.append(datum) md5_hash = md5.new() md5_hash.update(''.join(data)) return md5_hash.hexdigest() def encode_and_sign(self, dictionary): '''URL encodes the data in the dictionary, and signs it using the given secret, if a secret was given. ''' dictionary = make_utf8(dictionary) if self.secret: dictionary['api_sig'] = self.sign(dictionary) return urllib.urlencode(dictionary) def __getattr__(self, attrib): """Handle all the regular Flickr API calls. Example:: flickr.auth_getFrob(api_key="AAAAAA") 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') """ # Refuse to act as a proxy for unimplemented special methods if attrib.startswith('_'): raise AttributeError("No such attribute '%s'" % attrib) # Construct the method name and see if it's cached method = "flickr." + attrib.replace("_", ".") if method in self.__handler_cache: return self.__handler_cache[method] def handler(**args): '''Dynamically created handler for a Flickr API call''' if self.token_cache.token and not self.secret: raise ValueError("Auth tokens cannot be used without " "API secret") # Set some defaults defaults = {'method': method, 'auth_token': self.token_cache.token, 'api_key': self.api_key, 'format': self.default_format} args = self.__supply_defaults(args, defaults) return self.__wrap_in_parser(self.__flickr_call, parse_format=args['format'], **args) handler.method = method self.__handler_cache[method] = handler return handler 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``. >>> f = FlickrAPI('123') >>> f._FlickrAPI__supply_defaults( ... {'foo': 'bar', 'baz': None, 'token': None}, ... {'baz': 'foobar', 'room': 'door'}) {'foo': 'bar', 'room': 'door'} ''' result = args.copy() for key, default_value in defaults.iteritems(): # Set the default if the parameter wasn't passed if key not in args: result[key] = default_value for key, value in result.copy().iteritems(): # You are able to remove a default by assigning None, and we can't # pass None to Flickr anyway. if result[key] is None: del result[key] return result def __flickr_call(self, **kwargs): '''Performs a Flickr API call with the given arguments. The method name itself should be passed as the 'method' parameter. Returns the unparsed data from Flickr:: data = self.__flickr_call(method='flickr.photos.getInfo', photo_id='123', format='rest') ''' LOG.debug("Calling %s" % kwargs) post_data = self.encode_and_sign(kwargs) # Return value from cache if available if self.cache and self.cache.get(post_data): return self.cache.get(post_data) url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_rest_form flicksocket = urllib.urlopen(url, post_data) reply = flicksocket.read() flicksocket.close() # Store in cache, if we have one if self.cache is not None: self.cache.set(post_data, 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' 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] return parser(self, data) def auth_url(self, perms, frob): """Return the authorization URL to get a token. This is the URL the app will launch a browser toward if it needs a new token. perms "read", "write", or "delete" frob picked up from an earlier call to FlickrAPI.auth_getFrob() """ encoded = self.encode_and_sign({ "api_key": self.api_key, "frob": frob, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) def web_login_url(self, perms): '''Returns the web login URL to forward web users to. perms "read", "write", or "delete" ''' encoded = self.encode_and_sign({ "api_key": self.api_key, "perms": perms}) return "http://%s%s?%s" % (FlickrAPI.flickr_host, \ FlickrAPI.flickr_auth_form, encoded) 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 format = kwargs.get('format', self.default_format) if format not in rest_parsers and format != 'rest': raise FlickrError('Format %s not supported for uploading ' 'photos' % format) # The format shouldn't be used in the request to Flickr. if 'format' in kwargs: del kwargs['format'] return format def upload(self, filename, callback=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 callback method that gets progress reports title title of the photo description description a.k.a. caption of the photo tags space-delimited list of tags, ``'''tag1 tag2 "long tag"'''`` is_public "1" or "0" for a public resp. private photo is_friend "1" or "0" whether friends can see the photo while it's marked as private is_family "1" or "0" whether family can see the photo while it's marked as private content_type Set to "1" for Photo, "2" for Screenshot, or "3" for Other. hidden Set to "1" to keep the photo in global search results, "2" to hide from public searches. format The response format. You can only choose between the parsed responses or 'rest' for plain REST. The callback method should take two parameters: ``def callback(progress, done)`` Progress is a number between 0 and 100, and done is a boolean that's true only when the upload is done. """ return self.__upload_to_form(FlickrAPI.flickr_upload_form, filename, callback, **kwargs) def replace(self, filename, photo_id, callback=None, **kwargs): """Replace an existing photo. Supported parameters: filename name of a file to upload photo_id the ID of the photo to replace callback method that gets progress reports 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. The callback parameter has the same semantics as described in the ``upload`` function. """ if not photo_id: raise IllegalArgumentException("photo_id must be specified") kwargs['photo_id'] = photo_id return self.__upload_to_form( FlickrAPI.flickr_replace_form, filename, callback, **kwargs) def __upload_to_form(self, form_url, filename, callback, **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") # Figure out the response format format = self.__extract_upload_response_format(kwargs) # Update the arguments with the ones the user won't have to supply arguments = {'auth_token': self.token_cache.token, 'api_key': self.api_key} arguments.update(kwargs) # Convert to UTF-8 if an argument is an Unicode string kwargs = make_utf8(arguments) if self.secret: kwargs["api_sig"] = self.sign(kwargs) url = "http://%s%s" % (FlickrAPI.flickr_host, form_url) # construct POST data body = Multipart() for arg, value in kwargs.iteritems(): part = Part({'name': arg}, value) body.attach(part) filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg') body.attach(filepart) return self.__wrap_in_parser(self.__send_multipart, format, url, body, callback) def __send_multipart(self, url, body, progress_callback=None): '''Sends a Multipart object to an URL. Returns the resulting unparsed XML from Flickr. ''' LOG.debug("Uploading to %s" % url) request = urllib2.Request(url) request.add_data(str(body)) (header, value) = body.header() request.add_header(header, value) if not progress_callback: # Just use urllib2 if there is no progress callback # function response = urllib2.urlopen(request) return response.read() def __upload_callback(percentage, done, seen_header=[False]): '''Filters out the progress report on the HTTP header''' # Call the user's progress callback when we've filtered # out the HTTP header if seen_header[0]: return progress_callback(percentage, done) # Remember the first time we hit 'done'. if done: seen_header[0] = True response = reportinghttp.urlopen(request, __upload_callback) return response.read() def validate_frob(self, frob, perms): '''Lets the user validate the frob by launching a browser to the Flickr website. ''' auth_url = self.auth_url(perms, frob) webbrowser.open(auth_url, True, True) def get_token_part_one(self, perms="read"): """Get a token either from the cache, or make a new one from the frob. This first attempts to find a token in the user's token cache on disk. If that token is present and valid, it is returned by the method. If that fails (or if the token is no longer valid based on flickr.auth.checkToken) a new frob is acquired. The frob is validated by having the user log into flickr (with a browser). To get a proper token, follow these steps: - Store the result value of this method call - Give the user a way to signal the program that he/she has authorized it, for example show a button that can be pressed. - Wait for the user to signal the program that the authorization was performed, but only if there was no cached token. - Call flickrapi.get_token_part_two(...) and pass it the result value you stored. The newly minted token is then cached locally for the next run. perms "read", "write", or "delete" An example:: (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) Also take a look at ``authenticate_console(perms)``. """ # see if we have a saved token token = self.token_cache.token frob = None # see if it's valid if token: LOG.debug("Trying cached token '%s'" % token) try: rsp = self.auth_checkToken(auth_token=token, format='xmlnode') # see if we have enough permissions tokenPerms = rsp.auth[0].perms[0].text if tokenPerms == "read" and perms != "read": token = None elif tokenPerms == "write" and perms == "delete": token = None except FlickrError: LOG.debug("Cached token invalid") self.token_cache.forget() token = None # get a new token if we need one if not token: # get the frob LOG.debug("Getting frob for new token") rsp = self.auth_getFrob(auth_token=None, format='xmlnode') frob = rsp.frob[0].text # validate online self.validate_frob(frob, perms) return (token, frob) def get_token_part_two(self, (token, frob)): """Part two of getting a token, see ``get_token_part_one(...)`` for details.""" # If a valid token was obtained in the past, we're done if token: LOG.debug("get_token_part_two: no need, token already there") self.token_cache.token = token return token LOG.debug("get_token_part_two: getting a new token for frob '%s'" % frob) return self.get_token(frob) def get_token(self, frob): '''Gets the token given a certain frob. Used by ``get_token_part_two`` and by the web authentication method. ''' # get a token rsp = self.auth_getToken(frob=frob, auth_token=None, format='xmlnode') token = rsp.auth[0].token[0].text LOG.debug("get_token: new token '%s'" % token) # store the auth info for next time self.token_cache.token = token return token def authenticate_console(self, perms='read'): '''Performs the authentication, assuming a console program. Gets the token, if needed starts the browser and waits for the user to press ENTER before continuing. ''' (token, frob) = self.get_token_part_one(perms) if not token: raw_input("Press ENTER after you authorized this program") self.get_token_part_two((token, frob)) 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.tokencache LOG.setLevel(level) flickrapi.tokencache.LOG.setLevel(level) if __name__ == "__main__": print "Running doctests" import doctest doctest.testmod() print "Tests OK" flickrapi-1.2/flickrapi/cache.py0000644000175000017500000000601311065261043016265 0ustar sybrensybren# -*- 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(key) if exp is None: return default elif exp < now: self.delete(key) return default return self.storage[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[key] = value self.expire_info[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(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(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-1.2/flickrapi/reportinghttp.py0000644000175000017500000000521411065261043020135 0ustar sybrensybren# -*- encoding: utf-8 -*- '''HTTPHandler that supports a callback method for progress reports. ''' import urllib2 import httplib import logging __all__ = ['urlopen'] logging.basicConfig() LOG = logging.getLogger(__name__) progress_callback = None class ReportingSocket(object): '''Wrapper around a socket. Gives progress report through a callback function. ''' min_chunksize = 10240 def __init__(self, socket): self.socket = socket def sendall(self, bits): '''Sends all data, calling the callback function for every sent chunk. ''' LOG.debug("SENDING: %s..." % bits[0:30]) total = len(bits) sent = 0 chunksize = max(self.min_chunksize, total / 100) while len(bits) > 0: send = bits[0:chunksize] self.socket.sendall(send) sent += len(send) if progress_callback: progress = float(sent) / total * 100 progress_callback(progress, sent == total) bits = bits[chunksize:] def makefile(self, mode, bufsize): '''Returns a file-like object for the socket.''' return self.socket.makefile(mode, bufsize) def close(self): '''Closes the socket.''' return self.socket.close() class ProgressHTTPConnection(httplib.HTTPConnection): '''HTTPConnection that gives regular progress reports during sending of data. ''' def connect(self): '''Connects to a HTTP server.''' httplib.HTTPConnection.connect(self) self.sock = ReportingSocket(self.sock) class ProgressHTTPHandler(urllib2.HTTPHandler): '''HTTPHandler that gives regular progress reports during sending of data. ''' def http_open(self, req): return self.do_open(ProgressHTTPConnection, req) def set_callback(method): '''Sets the callback function to use for progress reports.''' global progress_callback # IGNORE:W0603 if not callable(method): raise ValueError('Callback method must be callable') progress_callback = method def urlopen(url_or_request, callback, body=None): '''Opens an URL using the ProgressHTTPHandler.''' set_callback(callback) opener = urllib2.build_opener(ProgressHTTPHandler) return opener.open(url_or_request, body) if __name__ == '__main__': def upload(progress, finished): '''Upload progress demo''' LOG.info("%3.0f - %s" % (progress, finished)) conn = urlopen("http://www.flickr.com/", 'x' * 10245, upload) data = conn.read() LOG.info("Read data") print data[:100].split('\n')[0] flickrapi-1.2/flickrapi/tokencache.py0000644000175000017500000000511611065261043017331 0ustar sybrensybren '''Persistent token cache management for the Flickr API''' import os.path import logging logging.basicConfig() LOG = logging.getLogger(__name__) LOG.setLevel(logging.INFO) __all__ = ('TokenCache', 'SimpleTokenCache') class SimpleTokenCache(object): '''In-memory token cache.''' def __init__(self): self.token = None def forget(self): '''Removes the cached token''' self.token = None class TokenCache(object): '''On-disk persistent token cache for a single application. The application is identified by the API key used. Per application multiple users are supported, with a single token per user. ''' def __init__(self, api_key, username=None): '''Creates a new token cache instance''' self.api_key = api_key self.username = username self.memory = {} def __get_cached_token_path(self): """Return the directory holding the app data.""" return os.path.expanduser(os.path.join("~", ".flickr", 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 = file(self.__get_cached_token_filename(), "r") token = f.read() f.close() return token.strip() except IOError: return None def __set_cached_token(self, token): """Cache a token for later use.""" # Remember for later use self.memory[self.username] = token path = self.__get_cached_token_path() if not os.path.exists(path): os.makedirs(path) f = file(self.__get_cached_token_filename(), "w") print >>f, token f.close() def forget(self): '''Removes the cached token''' if self.username in self.memory: del self.memory[self.username] filename = self.__get_cached_token_filename() if os.path.exists(filename): os.unlink(filename) token = property(__get_cached_token, __set_cached_token, forget, "The cached token") flickrapi-1.2/flickrapi/exceptions.py0000644000175000017500000000105011065261043017377 0ustar sybrensybren'''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. ''' class CancelUpload(Exception): '''Raise this exception in an upload/replace callback function to abort the upload. ''' flickrapi-1.2/doc/0000755000175000017500000000000011107627065013460 5ustar sybrensybrenflickrapi-1.2/doc/GNUmakefile0000644000175000017500000000046211065261043015525 0ustar sybrensybrenall: flickrapi.html %.html: %.rst documentation.css html4css1.css rst2html --stylesheet-path=documentation.css $< $@ clean: rm -f flickrapi.html upload: flickrapi.html rsync -va documentation.css flickrapi.html html4css1.css flickrapi.sf.net:/home/groups/f/fl/flickrapi/htdocs .PHONY: clean upload flickrapi-1.2/doc/html4css1.css0000644000175000017500000001265611065261043016017 0ustar sybrensybren/* :Author: David Goodger :Contact: goodger@users.sourceforge.net :Date: $Date: 2005-12-18 01:56:14 +0100 (Sun, 18 Dec 2005) $ :Revision: $Revision: 4224 $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to customize this style sheet. */ /* used to remove borders from tables and images */ .borderless, table.borderless td, table.borderless th { border: 0 } table.borderless td, table.borderless th { /* Override padding for "table.docutils td" with "! important". The right padding separates the table cells. */ padding: 0 0.5em 0 0 ! important } .first { /* Override more specific margin styles with "! important". */ margin-top: 0 ! important } .last, .with-subtitle { margin-bottom: 0 ! important } .hidden { display: none } a.toc-backref { text-decoration: none ; color: black } blockquote.epigraph { margin: 2em 5em ; } dl.docutils dd { margin-bottom: 0.5em } /* Uncomment (and remove this text!) to get bold-faced definition list terms dl.docutils dt { font-weight: bold } */ div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { margin: 2em ; border: medium outset ; padding: 1em } div.admonition p.admonition-title, div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title { font-weight: bold ; font-family: sans-serif } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title { color: red ; font-weight: bold ; font-family: sans-serif } /* Uncomment (and remove this text!) to get reduced vertical space in compound paragraphs. div.compound .compound-first, div.compound .compound-middle { margin-bottom: 0.5em } div.compound .compound-last, div.compound .compound-middle { margin-top: 0.5em } */ div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em ; margin-right: 2em } div.footer, div.header { clear: both; font-size: smaller } div.line-block { display: block ; margin-top: 1em ; margin-bottom: 1em } div.line-block div.line-block { margin-top: 0 ; margin-bottom: 0 ; margin-left: 1.5em } div.sidebar { margin-left: 1em ; border: medium outset ; padding: 1em ; background-color: #ffffee ; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { margin-top: 0.4em } h1.title { text-align: center } h2.subtitle { text-align: center } hr.docutils { width: 75% } img.align-left { clear: left } img.align-right { clear: right } ol.simple, ul.simple { margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.attribution { text-align: right ; margin-left: 50% } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.label { white-space: nowrap } p.rubric { font-weight: bold ; font-size: larger ; color: maroon ; text-align: center } p.sidebar-title { font-family: sans-serif ; font-weight: bold ; font-size: larger } p.sidebar-subtitle { font-family: sans-serif ; font-weight: bold } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font-family: serif ; font-size: 100% } pre.literal-block, pre.doctest-block { margin-left: 2em ; margin-right: 2em ; background-color: #eeeeee } span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.interpreted { font-family: sans-serif } span.option { white-space: nowrap } span.pre { white-space: pre } span.problematic { color: red } span.section-subtitle { /* font-size relative to parent (h1..h6 element) */ font-size: 80% } table.citation { border-left: solid 1px gray; margin-left: 1px } table.docinfo { margin: 2em 4em } table.docutils { margin-top: 0.5em ; margin-bottom: 0.5em } table.footnote { border-left: solid 1px black; margin-left: 1px } table.docutils td, table.docutils th, table.docinfo td, table.docinfo th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: top } table.docutils th.field-name, table.docinfo th.docinfo-name { font-weight: bold ; text-align: left ; white-space: nowrap ; padding-left: 0 } h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { font-size: 100% } tt.docutils { background-color: #eeeeee } ul.auto-toc { list-style-type: none } flickrapi-1.2/doc/flickrapi.rst0000644000175000017500000005466111065261043016163 0ustar sybrensybren====================================================================== Python FlickrAPI ====================================================================== :Version: 1.2 :Author: Sybren Stüvel .. contents:: .. sectnum:: Introduction ====================================================================== `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`_. 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. 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. Calling API functions ====================================================================== You start by creating a FlickrAPI object with your API key. This key 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 = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' flickr = flickrapi.FlickrAPI(api_key) photos = flickr.photos_search(user_id='73509078@N00', per_page='10') sets = flickr.photosets_getList(user_id='73509078@N00') There is a simple naming scheme here. If the flickr function is called ``flickr.photosets.getList`` just call ``photosets_getList`` on your ``flickr`` object. In other words: replace the dots with underscores. Parsing the return value ---------------------------------------------------------------------- Flickr sends back XML when you call a function. This XML is parsed and returned to you. There are two parsers available: ElementTree and XMLNode. ElementTree was introduced in version 1.1, and replaced XMLNode as the default parser as of version 1.2. In the following sections, we'll use a ``sets = flickr.photosets_getList(...)`` call and assume this was the response XML:: Test foo My Set bar Response parser: ElementTree ---------------------------------------------------------------------- The old XMLNode parser had some drawbacks. A better one is Python's standard ElementTree_. If you create the ``FlickrAPI`` instance like this, you'll use ElementTree:: flickr = flickrapi.FlickrAPI(api_key) or explicitly:: flickr = flickrapi.FlickrAPI(api_key, format='etree') The `ElementTree documentation`_ is quite clear, but to make things even easier, here are some examples using the same call and response XML as in the XMLNode example:: 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 is a more mature, better thought out XML parsing framework. It has several advantages over the old XMLNode parser: #. As a standard XML representation, ElementTree will be easier to plug into existing software. #. Easier to iterate over elements. For example, to list all "title" elements, you only need to do ``sets.getiterator('title')``. #. Developed by the Python team, which means it's subject to more rigorous testing and has a wider audience than the Python Flickr API module. This will result in a higher quality and less bugs. ElementTree in Python 2.4 ---------------------------------------------------------------------- Python 2.5 comes shipped with ElementTree. To get it running on Python 2.4 you'll have to install ElementTree yourself. The easiest way is to get setuptools and then just type:: easy_install elementtree easy_install flickrapi That'll get you both ElementTree and the latest version of the Python Flickr API. Another method is to get the Python FlickrAPI source and run:: python setup.py install easy_install elementtree As a last resort, you can `download ElementTree`_ and install it manually. 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. The old behaviour of the Python Flickr API was to simply return the error code in the XML. However, this is deprecated behaviour as we strive to notice an error condition as soon as possible. Checking the return value of every call is not Pythonic. For backward compatibility you can pass ``fail_on_error=False`` to the ``FlickrAPI`` constructor, but this behaviour is deprecated and will be removed in version 1.2. Unparsed response formats ---------------------------------------------------------------------- Flickr supports different response formats, such as JSON and XML-RPC. If you want, you can use such a different response format. Just add a ``format="json"`` option to the Flickr call. The Python Flickr API won't parse that format for you, though, so you just get the raw response:: >>> f = flickrapi.FlickrAPI(api_key) >>> 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, 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 not described in the "Response parser" sections is considered to be unparsed. Authentication ====================================================================== Her photos may be private. Access to her account is private for sure. A lot of Flickr API calls require the application to be authenticated. 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 document states "The auth_token and api_sig parameters should then be passed along with each request". You do *not* have to do this - the Python Flickr API takes care of that. Here is a simple example of Flickr's two-phase authentication:: import flickrapi api_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' api_secret = 'YYYYYYYYYYYYYYYY' flickr = flickrapi.FlickrAPI(api_key, api_secret) (token, frob) = flickr.get_token_part_one(perms='write') if not token: raw_input("Press ENTER after you authorized this program") flickr.get_token_part_two((token, frob)) The ``api_key`` and ``api_secret`` can be obtained from http://www.flickr.com/services/api/keys/. The call to ``flickr.get_token_part_one(...)`` does a lot of things. First, it checks the on-disk token cache. After all, the application may be authenticated already. If the application isn't authenticated, a browser opens the Flickr page, on which the user can grant the application the appropriate access. The application has to wait for the user to do this, hence the ``raw_input("Press ENTER after you authorized this program")``. A GUI application can use a popup for this, or some other way for the user to indicate she has performed the authentication ritual. Once this step is done, we can continue to store the token in the cache and remember it for future API calls. This is what ``flickr.get_token_part_two(...)`` does. 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. We'll assume you're following Flickr's `Web Applications How-To`_, and just tell you how things are splified when working with the Python Flickr API. 3. Create a login link. Use ``flickr.web_login_url(perms)``` for that. It'll return the login link for you, given the permissions you passed in the ``perms`` parameter. 5. Don't bother understanding the signing process; the ``FlickrAPI`` module takes care of that for you. Once you received the frob from Flickr, use ``flickr.get_token("the_frob")``. The FlickrAPI module will remember the token for you. 6. You can safely skip this, and just use the FlickrAPI module as usual. Only read this if you want to understand how the FlickrAPI module signs method calls for you. 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 the authentication token in a cookie. In step 5. above, use this:: token = flickr.get_token("the_frob") Then use your web framework to store the token in a cookie. When reading a token from a cookie, pass it on to the FlickrAPI constructor 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 ---------------------------------------------------------------------- Another way of preventing the storage of tokens is to pass ``store_token=False`` as the 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. Example using Django ---------------------------------------------------------------------- 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.web_login_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``. 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. ``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. ``callback`` This should be a method that receives two parameters, ``progress`` and ``done``. The callback method will be called every once in a while during uploading. Example:: def func(progress, done): if done: print "Done uploading" else: print "At %s%%" % progress flickr.upload(filename='test.jpg', callback=func) ``format`` The response format. This *must* be either ``rest`` or one of the parsed formats ``etree`` / ``xmlnode``. 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. ``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.). 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,-". 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. Requirements and compatibility ====================================================================== The Python Flickr API only uses built-in Python modules. It is compatible with Python 2.4 and newer. Usage of the "etree" format requires Python 2.5 or newer. Rendering the documentation requires `Docutils`_. Links ====================================================================== - `Python Flickr API interface`_ - `Flickr`_ - `Flickr API documentation`_ .. _`Flickr Services`: http://www.flickr.com/services/api/keys/apply/ .. _`Flickr API documentation`: http://www.flickr.com/services/api/ .. _`Flickr API`: http://www.flickr.com/services/api .. _`Flickr`: http://www.flickr.com/ .. _`Python Flickr API interface`: http://flickrapi.sourceforge.net/ .. _`Docutils`: http://docutils.sourceforge.net/ .. _`User Authentication`: http://www.flickr.com/services/api/misc.userauth.html .. _`Web Applications How-To`: http://www.flickr.com/services/api/auth.howto.web.html .. _Django: http://www.djangoproject.com/ .. _`PEP 318`: http://www.python.org/dev/peps/pep-0318/ .. _`ElementTree`: http://docs.python.org/lib/module-xml.etree.ElementTree.html .. _`ElementTree documentation`: http://docs.python.org/lib/module-xml.etree.ElementTree.html .. _`Django low-level cache API`: http://www.djangoproject.com/documentation/cache/#the-low-level-cache-api .. _`download ElementTree`: http://effbot.org/downloads/#elementtree flickrapi-1.2/doc/documentation.css0000644000175000017500000000053411065261043017036 0ustar sybrensybren@import url(html4css1.css); html { color: black; background-color: white; } body { margin-left: 10ex; margin-top: 5ex; padding-left: 1ex; border-left: 1px solid #006; width: 75ex; background-color: white; } h1 { border-bottom: 2px solid #006; } p { text-align: justify; } dt { font-weight: bold; } flickrapi-1.2/tests/0000755000175000017500000000000011107627065014055 5ustar sybrensybrenflickrapi-1.2/tests/test_xmlnode.py0000644000175000017500000000765111065261043017136 0ustar sybrensybren# -*- 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) flickrapi-1.2/tests/test_tokencache.py0000644000175000017500000000756411065261043017577 0ustar sybrensybren# -*- encoding: utf-8 -*- '''Unittest for the flickrapi.tokencache module''' import unittest import sys import os.path # Make sure the flickrapi module from the source distribution is used sys.path.insert(0, '..') import flickrapi class TestCache(unittest.TestCase): def setUp(self): '''Set the API key and remove the cache''' self.api_key = '123' self.remove_token() def tearDown(self): '''Remove the cache, again''' self.remove_token() def remove_token(self, username=None): tp = self.target_path(username) if os.path.exists(tp): os.unlink(tp) def target_path(self, username=None): if username: filename = 'auth-%s.token' % username else: filename = 'auth.token' return os.path.expanduser(os.path.join( "~", ".flickr", self.api_key, filename)) def test_set_get(self): token = 'xyz' cache = flickrapi.TokenCache(self.api_key) cache.token = token self.assertTrue(os.path.exists(self.target_path())) contents = file(self.target_path()).read() self.assertEquals(token, contents.strip()) self.assertEquals(token, cache.token) def test_get_from_file(self): token = 'xyz' # Store in one instance cache = flickrapi.TokenCache(self.api_key) cache.token = token # Read from another instance cache = flickrapi.TokenCache(self.api_key) self.assertEquals(token, cache.token) def test_remove(self): token = 'xyz' # Make sure the token doesn't exist yet before we start self.assertFalse(os.path.exists(self.target_path())) cache = flickrapi.TokenCache(self.api_key) # Make sure we can forget a token that doesn't exist cache.forget() self.assertFalse(os.path.exists(self.target_path())) self.assertEquals(None, cache.token) # Make sure remembering the token works cache.token = token self.assertTrue(os.path.exists(self.target_path())) # Make sure forgetting really works cache.forget() self.assertFalse(os.path.exists(self.target_path())) self.assertEquals(None, cache.token) def test_create_dir(self): token_path = self.target_path() tokendir = os.path.dirname(token_path) # Move token dir to a temporary dir tempdir = None if os.path.exists(tokendir): tempdir = '%s-DO-NOT-EXIST' % tokendir if os.path.exists(tempdir): raise Exception("Tempdir %s exists, please remove" % tempdir) os.rename(tokendir, tempdir) self.assertFalse(os.path.exists(tokendir)) cache = flickrapi.TokenCache(self.api_key) cache.token = 'x' self.assertTrue(os.path.exists(tokendir)) os.unlink(os.path.join(tokendir, 'auth.token')) os.rmdir(tokendir) if tempdir: os.rename(tempdir, tokendir) def test_multi_user(self): token = 'xyz' username = u'Sybren Stüvel' # Cache the auth token cache = flickrapi.TokenCache(self.api_key, username) cache.token = token # Ensure the token is stored in the right place self.assertTrue(os.path.exists(self.target_path(username))) # And that it contains the proper stuff contents = file(self.target_path(username)).read() self.assertEquals(token, contents.strip()) self.assertEquals(token, cache.token) # Ensure it can't be found by using another user cache = flickrapi.TokenCache(self.api_key, username + u'blah') self.assertEquals(None, cache.token) self.remove_token(username) flickrapi-1.2/tests/test_flicrkapi.py0000644000175000017500000004374011065261043017433 0ustar sybrensybren#!/usr/bin/env python '''Unittest for the FlickrAPI. Far from complete, but it's a start. ''' import unittest import sys import urllib import StringIO import exceptions import logging import pkg_resources # Make sure the flickrapi module from the source distribution is used sys.path.insert(0, '..') import flickrapi flickrapi.set_log_level(logging.FATAL) #flickrapi.set_log_level(logging.DEBUG) 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 = 'ecd01ab8f00faf13e1f8801586e126fd' secret = '2ee3f558fd79f292' logging.basicConfig() LOG = logging.getLogger(__name__) def etree_package(): '''Returns the name of the ElementTree package for the given Python version.''' current_version = sys.version_info[0:3] if current_version < (2, 5, 0): # For Python 2.4 and earlier, we assume ElementTree was # downloaded and installed from pypi. return 'elementtree.ElementTree' return 'xml.etree.ElementTree' class SuperTest(unittest.TestCase): '''Superclass for unittests, provides useful methods.''' def setUp(self): super(SuperTest, self).setUp() self.f = flickrapi.FlickrAPI(key, secret) self.f_noauth = flickrapi.FlickrAPI(key) # Remove/prevent any unwanted tokens self.f.token_cache.forget() self.f_noauth.token_cache = flickrapi.tokencache.SimpleTokenCache() 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 FlickrApiTest(SuperTest): 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_auth_url(self): '''Test the authentication URL generation''' args = dict(api_key=key, frob='frob', perms='read') args['api_sig'] = self.f.sign(args) url = self.f.auth_url(args['perms'], args['frob']) self.assertUrl('http', flickrapi.FlickrAPI.flickr_host, flickrapi.FlickrAPI.flickr_auth_form, args, url) def test_web_login_url(self): '''Test the web login URL.''' args = dict(api_key=key, perms='read') args['api_sig'] = self.f.sign(args) url = self.f.web_login_url(args['perms']) self.assertUrl('http', flickrapi.FlickrAPI.flickr_host, flickrapi.FlickrAPI.flickr_auth_form, args, url) def test_simple_search(self): '''Test simple Flickr search''' # We expect to be able to find kittens result = self.f.photos_search(tags='kitten') self.assertTrue(result.find('photos').attrib['total'] > 0) def test_token_constructor(self): '''Test passing a token to the constructor''' token = '123-abc-def' # Pass the token flickr = flickrapi.FlickrAPI(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.TokenCache(key)) def test_auth_token_without_secret(self): '''Auth tokens without secrets are meaningless''' token = '123-abc-def' # Create a normal FlickrAPI object flickr = flickrapi.FlickrAPI(key) flickr.token_cache.token = token self.assertRaises(exceptions.ValueError, flickr.photos_search, tags='kitten') def test_upload_without_filename(self): '''Uploading a file without filename is impossible''' self.assertRaises(flickrapi.exceptions.IllegalArgumentException, self.f.upload, '') self.assertRaises(flickrapi.exceptions.IllegalArgumentException, self.f.upload, None) def test_upload(self): photo = pkg_resources.resource_filename(__name__, 'photo.jpg') self.f.token_cache.username = 'unittest-upload' sys.stderr.write("If your browser starts, press ENTER after " "authentication") self.f.authenticate_console(perms='delete') result = self.f.upload(photo, is_public='0', content_type='2') # Now remove the photo from the stream again photo_id = result.find('photoid').text self.f.photos_delete(photo_id=photo_id) def test_cancel_upload(self): photo = pkg_resources.resource_filename(__name__, 'photo.jpg') self.f.token_cache.username = 'unittest-upload' sys.stderr.write("If your browser starts, press ENTER after " "authentication") self.f.authenticate_console(perms='delete') def callback(progress, done): '''Callback that immediately cancels the upload''' raise flickrapi.CancelUpload() try: self.f.upload(photo, callback=callback, is_public='0', content_type='2') self.fail("Expected exception not thrown") except flickrapi.CancelUpload, e: pass # Expected def test_store_token(self): '''Tests that store_token=False FlickrAPI uses SimpleTokenCache''' token_disk = '123-abc-disk' token_mem = '123-abc-mem' # Create a non-public-only instance, and set the on-disk token flickr = flickrapi.FlickrAPI(key, secret) flickr.token_cache.token = token_disk flickr = flickrapi.FlickrAPI(key, secret, store_token=False) # The token shouldn't be set self.assertEqual(None, flickr.token_cache.token) # Now set it flickr.token_cache.token = token_mem # It should not be in the on-disk token cache, only in memory self.assertEqual(token_disk, flickrapi.TokenCache(key).token) self.assertNotEqual(token_mem, flickrapi.TokenCache(key).token) 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._FlickrAPI__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._FlickrAPI__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 CachingTest(SuperTest): '''Tests that the caching framework works''' def test_cache_write(self): '''tests that the call result is written to cache''' photo_id = '2333478006' cache_key = ('api_key=%s' '&photo_id=%s' '&method=flickr.photos.getInfo' '&format=rest' % (key, photo_id)) f = flickrapi.FlickrAPI(key, store_token=False, format='rest') f.cache = flickrapi.SimpleCache() self.assertEqual(0, len(f.cache)) info = f.photos_getInfo(photo_id=photo_id) self.assertEqual(info, f.cache.get(cache_key)) def test_cache_read(self): '''Tests that cached data is returned if available''' photo_id = '2333478006' cache_key = ('api_key=%s' '&photo_id=%s' '&method=flickr.photos.getInfo' '&format=rest' % (key, photo_id)) faked_value = "FAKED_VALUE" f = flickrapi.FlickrAPI(key, store_token=False, format='rest') f.cache = flickrapi.SimpleCache() f.cache.set(cache_key, faked_value) info = f.photos_getInfo(photo_id=photo_id) self.assertEqual(faked_value, info) def test_cache_constructor_parameter(self): '''Tests that a cache is created when requested.''' f = flickrapi.FlickrAPI(key, cache=True) self.assertNotEqual(None, f.cache, "Cache should not be None") # Test list of non-cacheable method calls class FormatsTest(SuperTest): '''Tests the different parsed formats.''' def test_default_format(self): '''Test that the default format is etree''' f = flickrapi.FlickrAPI(key) etree = f.photos_getInfo(photo_id=u'2333478006') self.assertEqual(etree_package(), etree.__module__) def test_etree_format_happy(self): '''Test ETree format''' etree = self.f_noauth.photos_getInfo(photo_id=u'2333478006', format='etree') self.assertEqual(etree_package(), etree.__module__) 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 = flickrapi.FlickrAPI(key, format='etree') etree = f.photos_getInfo(photo_id=u'2333478006') self.assertEqual(etree_package(), etree.__module__) 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, basestring)) # Try to parse it rst = flickrapi.XMLNode.parse(xml, False) self.assertTrue(rst.photos[0]['total'] > 0) class SigningTest(SuperTest): '''Tests the signing of different arguments.''' def testSimple(self): '''Simple arguments, just ASCII''' signed = self.f.sign({'abc': 'def'}) self.assertEqual('9f215401af1a35e89da67a01be2333d2', signed) # Order shouldn't matter signed = self.f.sign({'abc': 'def', 'foo': 'bar'}) self.assertEqual('57ca69551c24c9c9ce2e2b5c832e61af', signed) signed = self.f.sign({'foo': 'bar', 'abc': 'def'}) self.assertEqual('57ca69551c24c9c9ce2e2b5c832e61af', signed) def testUnicode(self): '''Test signing of Unicode data''' # Unicode can't be signed directly self.assertRaises(flickrapi.IllegalArgumentException, self.f.sign, {'abc': u'def'}) # But converted to UTF-8 works just fine signed = self.f.sign({'abc': u'def'.encode('utf-8')}) self.assertEqual('9f215401af1a35e89da67a01be2333d2', signed) # Non-ASCII data should work too data = EURO_UNICODE + U_UML_UNICODE signed = self.f.sign({'abc': data.encode('utf-8')}) self.assertEqual('51188be8b03d1ee892ade48631bfc0fd', signed) # Straight UTF-8 should work too data = EURO_UTF8 + U_UML_UTF8 signed = self.f.sign({'abc': data}) self.assertEqual('51188be8b03d1ee892ade48631bfc0fd', signed) class EncodingTest(SuperTest): '''Test URL encoding + signing of data. Tests using sets, because we don't know in advance in which order the arguments will show up, and we don't care about that anyway. ''' def testSimple(self): '''Test simple ASCII-only data''' encoded = self.f.encode_and_sign({'abc': 'def', 'foo': 'bar'}) expected = set(['abc=def', 'foo=bar', 'api_sig=57ca69551c24c9c9ce2e2b5c832e61af' ]) self.assertEqual(expected, set(encoded.split('&'))) # Order shouldn't matter for the signature encoded = self.f.encode_and_sign({'foo': 'bar', 'abc': 'def'}) self.assertEqual(expected, set(encoded.split('&'))) def testUnicode(self): '''Test Unicode data''' # Unicode strings with ASCII data only should result in the # same as in the testSimple() test. encoded = self.f.encode_and_sign({'abc': u'def', 'foo': u'bar'}) expected = set(['abc=def', 'foo=bar', 'api_sig=57ca69551c24c9c9ce2e2b5c832e61af' ]) self.assertEqual(expected, set(encoded.split('&'))) # Non-ASCII UTF-8 data should work too # EURO = 0xE2 0x82 0xAC in UTF-8 # U_UML = 0xC3 0xBC in UTF-8 data = EURO_UNICODE + U_UML_UNICODE encoded = self.f.encode_and_sign({'abc': data.encode('utf-8')}) expected = set(['abc=%E2%82%AC%C3%BC', 'api_sig=51188be8b03d1ee892ade48631bfc0fd' ]) self.assertEqual(expected, set(encoded.split('&'))) # Straight Unicode should work too data = EURO_UNICODE + U_UML_UNICODE encoded = self.f.encode_and_sign({'abc': data}) self.assertEqual(expected, set(encoded.split('&'))) def testNoSecret(self): no_secret = flickrapi.FlickrAPI(key) data = EURO_UNICODE + U_UML_UNICODE encoded = no_secret.encode_and_sign({'abc': data}) self.assertEqual('abc=%E2%82%AC%C3%BC', encoded) class DynamicMethodTest(SuperTest): '''Tests the dynamic methods used to interface with Flickr.''' class FakeUrllib(object): '''Fake implementation of URLLib''' def __init__(self): self.data = None self.url = None def urlopen(self, url, postdata): self.url = url self.data = postdata return StringIO.StringIO(''' ''') def __getattr__(self, name): '''If we don't implement a method, call the original''' if not hasattr(urllib, name): raise AttributeError("No such attibute %s" % name) return getattr(urllib, name) #def original_caller(*args, **kwargs): # original(*args, **kwargs) def setUp(self): super(DynamicMethodTest, self).setUp() # Set fake urllib self.fake_url_lib = self.FakeUrllib() flickrapi.urllib = self.fake_url_lib def tearDown(self): super(DynamicMethodTest, self).tearDown() # Restore original urllib flickrapi.urllib = urllib def test_unicode_args(self): '''Tests whether Unicode arguments are properly handled. Tests using sets, since the order of the URL-encoded arguments can't be ensured. The order isn't important anyway. ''' # Plain ASCII should work self.f.photos_setMeta(monkey='lord') sent = set(self.fake_url_lib.data.split('&')) expected = set(['api_key=%s' % key, 'monkey=lord', 'method=flickr.photos.setMeta', 'api_sig=edb3c60b63becf1738e2cd8fcc42834a', 'format=rest' ]) self.assertEquals(expected, sent) # Unicode should work too self.f.photos_setMeta(title='monkeylord', description=EURO_UNICODE+U_UML_UNICODE) sent = set(self.fake_url_lib.data.split('&')) expected = set(['api_key=%s' % key, 'title=monkeylord', 'description=%E2%82%AC%C3%BC', 'method=flickr.photos.setMeta', 'api_sig=29fa7705fc721fded172a1c113304871', 'format=rest' ]) self.assertEquals(expected, sent) def test_private_attribute(self): '''Tests that we get an AttributeError when accessing an attribute starting with __. ''' self.assertRaises(AttributeError, self.f, '__get_photos') def test_get_dynamic_method(self): method = self.f.photos_setMeta self.assertTrue(callable(method)) self.assertEquals('flickr.photos.setMeta', method.method) # Test that we can get it again - should come from the cache, # but no way to test that. method = self.f.photos_setMeta self.assertTrue(callable(method)) self.assertEquals('flickr.photos.setMeta', method.method) if __name__ == '__main__': unittest.main() flickrapi-1.2/tests/test_multipart.py0000644000175000017500000000702611065261043017505 0ustar sybrensybren# -*- encoding: utf-8 -*- '''Unittest for the flickrapi.multipart module''' import unittest import sys import os # Make sure the flickrapi module from the source distribution is used sys.path.insert(0, '..') import flickrapi.multipart as multipart class PartTest(unittest.TestCase): def testSimplePart(self): p = multipart.Part({'name': 'title', 'purpose': 'test'}, "Little red kitty") expect = [ 'Content-Disposition: form-data; name="title"; purpose="test"', '', 'Little red kitty' ] self.assertEquals(expect, p.render()) def testContentType(self): p = multipart.Part({'name': 'title', 'purpose': 'test'}, "Little red kitty", "text/plain") expect = [ 'Content-Disposition: form-data; name="title"; purpose="test"', 'Content-Type: text/plain', '', 'Little red kitty' ] self.assertEquals(expect, p.render()) def testUnicodePayload(self): p = multipart.Part({'name': 'title', 'purpose': 'test'}, u"Little red kitty ©") expect = [ 'Content-Disposition: form-data; name="title"; purpose="test"', '', 'Little red kitty ©' ] self.assertEquals(expect, p.render()) def testFile(self): testfile_name = "testfile" testfile_payload = "This is a file" # Create the file testfile = open(testfile_name, "w") testfile.write(testfile_payload) testfile.close() p = multipart.FilePart({'name': 'textfile'}, testfile_name, "text/embedded") expect = [ 'Content-Disposition: form-data; name="textfile"; filename="%s"' % testfile_name, 'Content-Type: text/embedded', '', testfile_payload ] result = p.render() for index, line in enumerate(expect): self.assertEquals(line, result[index]) os.unlink(testfile_name) class MultipartTest(unittest.TestCase): def testSimple(self): m = multipart.Multipart() p = multipart.Part({'name': 'title', 'purpose': 'test'}, "Little red kitty") m.attach(p) lines = str(m).split('\r\n') self.assertEquals(m.header(), ('Content-Type', 'multipart/form-data; boundary=' + m.boundary)) self.assertEquals(lines[0], '--' + m.boundary) self.assertEquals(lines[1], 'Content-Disposition: form-data; name="title"; purpose="test"') self.assertEquals(lines[2], '') self.assertEquals(lines[3], 'Little red kitty') self.assertEquals(lines[4], '--' + m.boundary + '--') def testAttach(self): m = multipart.Multipart() p = multipart.Part({'name': 'title', 'purpose': 'test'}, "Little red kitty") m.attach(p) self.assertEquals([p], m.parts) def testUnicode(self): m = multipart.Multipart() p = multipart.Part({'name': 'title', 'purpose': 'test'}, u"Little red kitty © Ünicode") m.attach(p) lines = str(m).split('\r\n') self.assertEquals(m.header(), ('Content-Type', 'multipart/form-data; boundary=' + m.boundary)) self.assertEquals(lines[0], '--' + m.boundary) self.assertEquals(lines[1], 'Content-Disposition: form-data; name="title"; purpose="test"') self.assertEquals(lines[2], '') self.assertEquals(lines[3], 'Little red kitty © Ünicode') self.assertEquals(lines[4], '--' + m.boundary + '--') self.assertTrue(isinstance(str(m), str)) if __name__ == '__main__': unittest.main() flickrapi-1.2/tests/test_cache.py0000644000175000017500000000217511065261043016527 0ustar sybrensybren# -*- encoding: utf-8 -*- '''Unittest for the flickrapi.cache module''' import unittest import sys import time # 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 xrange(100): cache.set('key-%03d' % num, 'value') removed = float(max_entries) / cache.cull_frequency self.assertEqual(100 - removed, len(cache)) flickrapi-1.2/UPGRADING0000644000175000017500000000403711065261043014153 0ustar sybrensybrenUpgrading from previous versions ================================= 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-1.2/setup.cfg0000644000175000017500000000007311107627065014534 0ustar sybrensybren[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 flickrapi-1.2/MANIFEST0000644000175000017500000000066711065261043014046 0ustar sybrensybrenLICENSE MANIFEST README setup.py _setup.py ez_setup.py runtests UPGRADING flickrapi/exceptions.py flickrapi/__init__.py flickrapi/cache.py flickrapi/multipart.py flickrapi/reportinghttp.py flickrapi/tokencache.py flickrapi/xmlnode.py doc/documentation.css doc/flickrapi.rst doc/GNUmakefile doc/html4css1.css tests/photo.jpg tests/test_flicrkapi.py tests/test_cache.py tests/test_multipart.py tests/test_tokencache.py tests/test_xmlnode.py flickrapi-1.2/README0000644000175000017500000000075411065261043013572 0ustar sybrensybren====================================================================== Python FlickrAPI ====================================================================== Most of the info can be found in the 'doc' directory or on http://flickrapi.sourceforge.net/ To install the Python Flickr API module, run:: python setup.py install To run the unittests, install nose and pymock first using:: easy_install nose easy_install pymock then run ``nosetest`` in the top-level directory.flickrapi-1.2/runtests0000755000175000017500000000056011065261043014522 0ustar sybrensybren#!/usr/bin/env python # EASY-INSTALL-ENTRY-SCRIPT: 'nose>=0.9.2','console_scripts','nosetests' __requires__ = 'nose>=0.9.2' import sys from pkg_resources import load_entry_point sys.argv[1:] = ['--with-doctest', '--with-coverage', '--cover-erase', '--cover-package=flickrapi'] sys.exit( load_entry_point('nose>=0.9.2', 'console_scripts', 'nosetests')() ) flickrapi-1.2/ez_setup.py0000644000175000017500000002231311065261043015115 0ustar sybrensybren#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c6" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', } import sys, os def _validate_md5(egg_name, data): if egg_name in md5_data: from md5 import md5 digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re from md5 import md5 for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:])