conduit r1450 - in trunk: . conduit/modules/FlickrModule conduit/modules/FlickrModule/flickrapi conduit/modules/RTMModule scripts
- From: jstowers svn gnome org
- To: svn-commits-list gnome org
- Subject: conduit r1450 - in trunk: . conduit/modules/FlickrModule conduit/modules/FlickrModule/flickrapi conduit/modules/RTMModule scripts
- Date: Tue, 6 May 2008 14:46:04 +0100 (BST)
Author: jstowers
Date: Tue May 6 13:46:03 2008
New Revision: 1450
URL: http://svn.gnome.org/viewvc/conduit?rev=1450&view=rev
Log:
2008-05-07 John Stowers <john stowers gmail com>
* NEWS:
* conduit/modules/FlickrModule/FlickrModule.py:
* conduit/modules/FlickrModule/flickrapi/LICENSE:
* conduit/modules/FlickrModule/flickrapi/__init__.py:
* conduit/modules/FlickrModule/flickrapi/cache.py:
* conduit/modules/FlickrModule/flickrapi/exceptions.py:
* conduit/modules/FlickrModule/flickrapi/multi-username.patch:
* conduit/modules/FlickrModule/flickrapi/multipart.py:
* conduit/modules/FlickrModule/flickrapi/tokencache.py:
* conduit/modules/FlickrModule/flickrapi/xmlnode.py: Update to latest version
of flickrapi and adapt to API changes.
* conduit/modules/RTMModule/rtm.py: Remove logging.basicConfig() call which
overrides conduits log settings and screws up everything.
Added:
trunk/conduit/modules/FlickrModule/flickrapi/LICENSE
trunk/conduit/modules/FlickrModule/flickrapi/cache.py
trunk/conduit/modules/FlickrModule/flickrapi/exceptions.py
Removed:
trunk/conduit/modules/FlickrModule/flickrapi/multi-username.patch
Modified:
trunk/ChangeLog
trunk/NEWS
trunk/conduit/modules/FlickrModule/FlickrModule.py
trunk/conduit/modules/FlickrModule/flickrapi/__init__.py
trunk/conduit/modules/FlickrModule/flickrapi/multipart.py
trunk/conduit/modules/FlickrModule/flickrapi/tokencache.py
trunk/conduit/modules/FlickrModule/flickrapi/xmlnode.py
trunk/conduit/modules/RTMModule/rtm.py
trunk/scripts/ChangeLog
trunk/scripts/update-3rdparty-libs.sh
Modified: trunk/NEWS
==============================================================================
--- trunk/NEWS (original)
+++ trunk/NEWS Tue May 6 13:46:03 2008
@@ -1,7 +1,7 @@
NEW in 0.3.11:
==============
* Support ZOTO Photos
-* Update to latest version of pyfacebook
+* Update to latest version of pyfacebook, flickrapi
NEW in 0.3.10:
==============
Modified: trunk/conduit/modules/FlickrModule/FlickrModule.py
==============================================================================
--- trunk/conduit/modules/FlickrModule/FlickrModule.py (original)
+++ trunk/conduit/modules/FlickrModule/FlickrModule.py Tue May 6 13:46:03 2008
@@ -1,9 +1,6 @@
"""
Flickr Uploader.
"""
-import os, sys
-import traceback
-import md5
import logging
log = logging.getLogger("modules.Flickr")
@@ -22,43 +19,57 @@
Utils.dataprovider_add_dir_to_path(__file__)
import flickrapi
-if flickrapi.__version__.endswith("CONDUIT"):
+if flickrapi.__version__ == "1.1":
MODULES = {
"FlickrTwoWay" : { "type": "dataprovider" }
}
log.info("Module Information: %s" % Utils.get_module_information(flickrapi, "__version__"))
+ #turn of debugging in the library
+ flickrapi.set_log_level(logging.NOTSET)
else:
MODULES = {}
log.info("Flickr support disabled")
class MyFlickrAPI(flickrapi.FlickrAPI):
- def __init__(self, apiKey, secret, username):
- flickrapi.FlickrAPI.__init__(self,
- apiKey,
- secret,
- fail_on_error=True,
- username=username
- )
+ """
+ Wraps the FlickrAPI in order to override validate_frob to launch the conduit
+ web browser.
+ """
+ #Note that the order, and assignment of values to self.myFrob
+ #and self.myToken is important - if done incorrectly then FlickrAPI.__getattr__
+ #returns a handler function for them, and not the actual value requested
+ def __init__(self, api_key, secret, username):
+ flickrapi.FlickrAPI.__init__(self,
+ api_key=api_key,
+ secret=secret,
+ username=username,
+ token=None,
+ format='xmlnode',
+ store_token=True,
+ cache=False
+ )
+ self.myFrob = None
+ self.myToken = None
- def validateFrob(self, frob, perms):
- self.frob = frob
- encoded = self.encode_and_sign({
- "api_key": self.api_key,
- "frob": frob,
- "perms": perms})
- auth_url = "http://%s%s?%s" % (flickrapi.FlickrAPI.flickrHost, flickrapi.FlickrAPI.flickrAuthForm, encoded)
- Web.LoginMagic("Log into Flickr", auth_url, login_function=self.try_login)
-
+ def validate_frob(self, frob, perms):
+ self.myFrob = frob
+ Web.LoginMagic("Log into Flickr", self.auth_url(perms, frob), login_function=self.try_login)
+
def try_login(self):
try:
- self.getTokenPartTwo((self.token, self.frob))
+ self.myToken = self.get_token(self.myFrob)
return True
except flickrapi.FlickrError:
return False
def login(self):
- token, frob = self.getTokenPartOne(perms='delete')
- return token
+ token, frob = self.get_token_part_one(perms='delete')
+ if token:
+ log.debug("Got token from cache")
+ return token
+ else:
+ log.debug("Got token from web")
+ return self.myToken
class FlickrTwoWay(Image.ImageTwoWay):
@@ -69,7 +80,6 @@
API_KEY="65552e8722b21d299388120c9fa33580"
SHARED_SECRET="03182987bf7fc4d1"
- _perms_ = "delete"
def __init__(self, *args):
Image.ImageTwoWay.__init__(self)
@@ -87,24 +97,22 @@
"""
Returs used,total or -1,-1 on error
"""
- ret = self.fapi.people_getUploadStatus()
- if self.fapi.getRspErrorCode(ret) != 0:
- log.debug("Flickr people_getUploadStatus Error: %s" % self.fapi.getPrintableError(ret))
- return -1,-1,100
- else:
+ try:
+ ret = self.fapi.people_getUploadStatus()
totalkb = int(ret.user[0].bandwidth[0]["maxkb"])
usedkb = int(ret.user[0].bandwidth[0]["usedkb"])
p = (float(usedkb)/totalkb)*100.0
return usedkb,totalkb,p
+ except flickrapi.FlickrError, e:
+ log.debug("Error getting quota: %s" % e)
+ return -1,-1,100
def _get_photo_info(self, photoID):
- info = self.fapi.photos_getInfo(photo_id=photoID)
-
- if self.fapi.getRspErrorCode(info) != 0:
- log.debug("Flickr photos_getInfo Error: %s" % self.fapi.getPrintableError(info))
+ try:
+ return self.fapi.photos_getInfo(photo_id=photoID)
+ except flickrapi.FlickrError, e:
+ log.debug("Error getting photo info: %s" % e)
return None
- else:
- return info
def _get_raw_photo_url(self, photoInfo):
photo = photoInfo.photo[0]
@@ -113,18 +121,18 @@
return url
def _upload_photo (self, uploadInfo):
- ret = self.fapi.upload(
- filename=uploadInfo.url,
- title=uploadInfo.name,
- description=uploadInfo.caption,
- is_public="%i" % self.showPublic,
- tags=' '.join(tag.replace(' ', '_') for tag in uploadInfo.tags))
-
- if self.fapi.getRspErrorCode(ret) != 0:
- raise Exceptions.SyncronizeError("Flickr Upload Error: %s" % self.fapi.getPrintableError(ret))
+ try:
+ ret = self.fapi.upload(
+ filename=uploadInfo.url,
+ title=uploadInfo.name,
+ description=uploadInfo.caption,
+ is_public="%i" % self.showPublic,
+ tags=' '.join(tag.replace(' ', '_') for tag in uploadInfo.tags))
+ except flickrapi.FlickrError, e:
+ raise Exceptions.SyncronizeError("Flickr Upload Error: %s" % e)
# get the id
- photoId = ret.photoid[0].elementText
+ photoId = ret.photoid[0].text
# check if phtotoset exists, if not create it
firstPhoto = False
@@ -135,11 +143,12 @@
# add the photo to the photoset
if self.photoSetId and not firstPhoto:
- ret = self.fapi.photosets_addPhoto(
- photoset_id = self.photoSetId,
- photo_id = photoId)
- if self.fapi.getRspErrorCode(ret) != 0:
- log.warn("Flickr failed to add photo to set: %s" % self.fapi.getPrintableError(ret))
+ try:
+ ret = self.fapi.photosets_addPhoto(
+ photoset_id = self.photoSetId,
+ photo_id = photoId)
+ except flickrapi.FlickrError, e:
+ log.warn("Flickr failed to add %s to set: %s" % (photoId,e))
#return the photoID
return Rid(uid=photoId)
@@ -161,16 +170,14 @@
def _create_photoset(self, primaryPhotoId):
#create one with created photoID if not
- ret = self.fapi.photosets_create(
- title=self.photoSetName,
- primary_photo_id=primaryPhotoId)
-
- photoSetId = None
- if self.fapi.getRspErrorCode(ret) != 0:
- log.warn("Flickr failed to create photoset: %s" % self.fapi.getPrintableError(ret))
- else:
- photoSetId = ret.photoset[0]['id']
- return photoSetId
+ try:
+ ret = self.fapi.photosets_create(
+ title=self.photoSetName,
+ primary_photo_id=primaryPhotoId)
+ return ret.photoset[0]['id']
+ except flickrapi.FlickrError, e:
+ log.warn("Flickr failed to create photoset %s: %s" % (self.photoSetName,e))
+ return None
def _get_photoset(self):
for name, photoSetId in self._get_photosets():
@@ -179,38 +186,38 @@
self.photoSetId = photoSetId
def _get_photosets(self):
- ret = self.fapi.photosets_getList()
- if self.fapi.getRspErrorCode(ret) != 0:
- log.warn("Flickr Refresh Error: %s" % self.fapi.getPrintableError(ret))
- return []
-
photosets = []
- if hasattr(ret.photosets[0], 'photoset'):
- for pset in ret.photosets[0].photoset:
- photosets.append(
- (pset.title[0].elementText, #photoset name
- pset['id'])) #photoset id
+ try:
+ ret = self.fapi.photosets_getList()
+ if hasattr(ret.photosets[0], 'photoset'):
+ for pset in ret.photosets[0].photoset:
+ photosets.append(
+ (pset.title[0].text, #photoset name
+ pset['id'])) #photoset id
+ except flickrapi.FlickrError, e:
+ log.debug("Failed to get photosets: %s" % e)
- return photosets
+ return photosets
def _get_photos(self):
if not self.photoSetId:
return []
photoList = []
- ret = self.fapi.photosets_getPhotos(photoset_id=self.photoSetId)
- if self.fapi.getRspErrorCode(ret) != 0:
- log.warn("Flickr failed to get photos: %s" % self.fapi.getPrintableError(ret))
- else:
+ try:
+ ret = self.fapi.photosets_getPhotos(photoset_id=self.photoSetId)
for photo in ret.photoset[0].photo:
photoList.append(photo['id'])
+ except flickrapi.FlickrError, e:
+ log.warn("Flickr failed to get photos: %s" % e)
+
return photoList
# DataProvider methods
def refresh(self):
Image.ImageTwoWay.refresh(self)
self._login()
- self._get_photoset()
+ self._get_photoset()
used,tot,percent = self._get_user_quota()
log.debug("Used %2.1f%% of monthly badwidth quota (%skb/%skb)" % (percent,used,tot))
@@ -223,14 +230,14 @@
# get url
url = self._get_raw_photo_url (photoInfo)
# get the title
- title = str(photoInfo.photo[0].title[0].elementText)
+ title = str(photoInfo.photo[0].title[0].text)
# get tags
tagsNode = photoInfo.photo[0].tags[0]
# get caption
- caption = photoInfo.photo[0].description[0].elementText
+ caption = photoInfo.photo[0].description[0].text
if hasattr(tagsNode, 'tag'):
- tags = tuple(tag.elementText for tag in tagsNode.tag)
+ tags = tuple(tag.text for tag in tagsNode.tag)
else:
tags = ()
@@ -256,13 +263,13 @@
def delete(self, LUID):
if self._get_photo_info(LUID) != None:
- ret = self.fapi.photos_delete(photo_id=LUID)
- if self.fapi.getRspErrorCode(ret) != 0:
- log.warn("Flickr Error Deleting: %s" % self.fapi.getPrintableError(ret))
- else:
- log.debug("Successfully deleted photo [%s]" % LUID)
+ try:
+ ret = self.fapi.photos_delete(photo_id=LUID)
+ log.debug("Successfully deleted photo: %s" % LUID)
+ except flickrapi.FlickrError, e:
+ log.warn("Error deleting %s: %s" % (LUID,e))
else:
- log.warn("Photo doesnt exist")
+ log.warn("Error deleting %s: doesnt exist" % LUID)
def configure(self, window):
"""
Added: trunk/conduit/modules/FlickrModule/flickrapi/LICENSE
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/FlickrModule/flickrapi/LICENSE Tue May 6 13:46:03 2008
@@ -0,0 +1,27 @@
+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.
Modified: trunk/conduit/modules/FlickrModule/flickrapi/__init__.py
==============================================================================
--- trunk/conduit/modules/FlickrModule/flickrapi/__init__.py (original)
+++ trunk/conduit/modules/FlickrModule/flickrapi/__init__.py Tue May 6 13:46:03 2008
@@ -8,14 +8,20 @@
.. _`the FlickrAPI homepage`: http://flickrapi.sf.net/
'''
-__version__ = '0.16-beta0-CONDUIT'
-__revision__ = '$Revision: 114 $'
+__version__ = '1.1'
__all__ = ('FlickrAPI', 'IllegalArgumentException', 'FlickrError',
- 'XMLNode', 'set_log_level', '__version__', '__revision__')
+ '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
@@ -45,35 +51,15 @@
import copy
import webbrowser
-from flickrapi.tokencache import TokenCache
+from flickrapi.tokencache import TokenCache, SimpleTokenCache
from flickrapi.xmlnode import XMLNode
from flickrapi.multipart import Part, Multipart, FilePart
+from flickrapi.exceptions import IllegalArgumentException, FlickrError
+from flickrapi.cache import SimpleCache
from flickrapi import reportinghttp
LOG = logging.getLogger(__name__)
-########################################################################
-# Exceptions
-########################################################################
-
-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.
- '''
-
-########################################################################
-# Flickr functionality
-########################################################################
-
def make_utf8(dictionary):
'''Encodes all Unicode strings in the dictionary to UTF-8. Converts
all other objects to regular strings.
@@ -92,55 +78,191 @@
return result
+def debug(method):
+ '''Method decorator for debugging method calls.
-#-----------------------------------------------------------------------
-class FlickrAPI:
- """Encapsulated flickr functionality.
+ 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
- Example usage:
+ return debugged
- flickr = FlickrAPI(flickrAPIKey, flickrSecret)
- rsp = flickr.auth_checkToken(api_key=flickrAPIKey, auth_token=token)
+# 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')
"""
- flickrHost = "api.flickr.com"
- flickrRESTForm = "/services/rest/"
- flickrAuthForm = "/services/auth/"
- flickrUploadForm = "/services/upload/"
- flickrReplaceForm = "/services/replace/"
-
- #-------------------------------------------------------------------
- def __init__(self, apiKey, secret=None, fail_on_error=True, username=""):
- """Construct a new FlickrAPI instance for a given API key and secret."""
+ 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='xmlnode', 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)
+ """
- self.api_key = apiKey
+ 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.token_cache = TokenCache(apiKey, username)
- self.token = self.token_cache.token
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.
+ '''
+
+ rsp = self.reflection_getMethods(format='etree')
+
+ 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.'''
+
+ # Only import it here, to maintain Python 2.4 compatibility
+ import xml.etree.ElementTree
+
+ rsp = xml.etree.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
+ 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]
- keys = dictionary.keys()
- keys.sort()
- for key in keys:
+ for key in sorted(dictionary.keys()):
data.append(key)
datum = dictionary[key]
if isinstance(datum, unicode):
@@ -163,74 +285,141 @@
dictionary['api_sig'] = self.sign(dictionary)
return urllib.urlencode(dictionary)
- #-------------------------------------------------------------------
- def __getattr__(self, method):
+ def __getattr__(self, attrib):
"""Handle all the regular Flickr API calls.
+
+ Example::
- >>> flickr.auth_getFrob(apiKey="AAAAAA")
- >>> xmlnode = flickr.photos_getInfo(photo_id='1234')
- >>> json = flickr.photos_getInfo(photo_id='1234', format='json')
+ flickr.auth_getFrob(api_key="AAAAAA")
+ xmlnode = flickr.photos_getInfo(photo_id='1234')
+ xmlnode = flickr.photos_getInfo(photo_id='1234', format='xmlnode')
+ json = flickr.photos_getInfo(photo_id='1234', format='json')
+ etree = flickr.photos_getInfo(photo_id='1234', format='etree')
"""
# Refuse to act as a proxy for unimplemented special methods
- if method.startswith('__'):
- raise AttributeError("No such attribute '%s'" % method)
-
- if self.__handler_cache.has_key(method):
- # If we already have the handler, return it
- return self.__handler_cache.has_key(method)
-
- # Construct the method name and URL
- method = "flickr." + method.replace("_", ".")
- url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrRESTForm
+ 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,
+ 'auth_token': self.token_cache.token,
'api_key': self.api_key,
- 'format': 'rest'}
- for key, default_value in defaults.iteritems():
- if key not in args:
- args[key] = default_value
- # You are able to remove a default by assigning None
- if key in args and args[key] is None:
- del args[key]
-
- LOG.debug("Calling %s(%s)" % (method, args))
-
- post_data = self.encode_and_sign(args)
-
- flicksocket = urllib.urlopen(url, post_data)
- data = flicksocket.read()
- flicksocket.close()
-
- # Return the raw response when a non-REST format
- # was chosen.
- if args['format'] != 'rest':
- return data
-
- result = XMLNode.parseXML(data, True)
- if self.fail_on_error:
- FlickrAPI.testFailure(result, True)
+ 'format': self.default_format}
- return result
+ 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 self.__handler_cache[method]
+ return reply
- #-------------------------------------------------------------------
- def __get_auth_url(self, perms, frob):
+ 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()
+ perms
+ "read", "write", or "delete"
+ frob
+ picked up from an earlier call to FlickrAPI.auth_getFrob()
"""
@@ -239,8 +428,22 @@
"frob": frob,
"perms": perms})
- return "http://%s%s?%s" % (FlickrAPI.flickrHost, \
- FlickrAPI.flickrAuthForm, encoded)
+ 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 upload(self, filename, callback=None, **arg):
"""Upload a file to flickr.
@@ -250,14 +453,25 @@
Supported parameters:
- filename -- name of a file to upload
- callback -- method that gets progress reports
+ filename
+ name of a file to upload
+ callback
+ method that gets progress reports
title
+ title of the photo
description
- tags -- space-delimited list of tags, '''tag1 tag2 "long tag"'''
- is_public -- "1" or "0"
- is_friend -- "1" or "0"
- is_family -- "1" or "0"
+ 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
The callback method should take two parameters:
def callback(progress, done)
@@ -283,7 +497,8 @@
raise IllegalArgumentException("Unknown parameter "
"'%s' sent to FlickrAPI.upload" % a)
- arguments = {'auth_token': self.token, 'api_key': self.api_key}
+ arguments = {'auth_token': self.token_cache.token,
+ 'api_key': self.api_key}
arguments.update(arg)
# Convert to UTF-8 if an argument is an Unicode string
@@ -291,7 +506,7 @@
if self.secret:
arg["api_sig"] = self.sign(arg)
- url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrUploadForm
+ url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_upload_form
# construct POST data
body = Multipart()
@@ -306,15 +521,17 @@
filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg')
body.attach(filepart)
- return self.send_multipart(url, body, callback)
+ return self.__send_multipart(url, body, callback)
def replace(self, filename, photo_id):
"""Replace an existing photo.
Supported parameters:
- filename -- name of a file to upload
- photo_id -- the ID of the photo to replace
+ filename
+ name of a file to upload
+ photo_id
+ the ID of the photo to replace
"""
if not filename:
@@ -324,14 +541,14 @@
args = {'filename': filename,
'photo_id': photo_id,
- 'auth_token': self.token,
+ 'auth_token': self.token_cache.token,
'api_key': self.api_key}
args = make_utf8(args)
if self.secret:
args["api_sig"] = self.sign(args)
- url = "http://" + FlickrAPI.flickrHost + FlickrAPI.flickrReplaceForm
+ url = "http://" + FlickrAPI.flickr_host + FlickrAPI.flickr_replace_form
# construct POST data
body = Multipart()
@@ -347,9 +564,9 @@
filepart = FilePart({'name': 'photo'}, filename, 'image/jpeg')
body.attach(filepart)
- return self.send_multipart(url, body)
+ return self.__send_multipart(url, body)
- def send_multipart(self, url, body, progress_callback=None):
+ def __send_multipart(self, url, body, progress_callback=None):
'''Sends a Multipart object to an URL.
Returns the resulting XML from Flickr.
@@ -368,91 +585,104 @@
response = urllib2.urlopen(request)
rspXML = response.read()
- result = XMLNode.parseXML(rspXML)
- if self.fail_on_error:
- FlickrAPI.testFailure(result, True)
-
- return result
+ return self.parse_xmlnode(rspXML)
- #-----------------------------------------------------------------------
@classmethod
- def testFailure(cls, rsp, exception_on_error=True):
+ def test_failure(cls, rsp, exception_on_error=True):
"""Exit app if the rsp XMLNode indicates failure."""
+
+ LOG.warn("FlickrAPI.test_failure has been deprecated and will be "
+ "removed in FlickrAPI version 1.2.")
+
if rsp['stat'] != "fail":
return
- message = cls.getPrintableError(rsp)
+ message = cls.get_printable_error(rsp)
LOG.error(message)
if exception_on_error:
raise FlickrError(message)
- #-----------------------------------------------------------------------
@classmethod
- def getPrintableError(cls, rsp):
- """Return a printed error message string."""
- return "%s: error %s: %s" % (rsp.elementName, \
- cls.getRspErrorCode(rsp), cls.getRspErrorMsg(rsp))
+ def get_printable_error(cls, rsp):
+ """Return a printed error message string of an XMLNode Flickr response."""
+
+ LOG.warn("FlickrAPI.get_printable_error has been deprecated "
+ "and will be removed in FlickrAPI version 1.2.")
+
+ return "%s: error %s: %s" % (rsp.name, \
+ cls.get_rsp_error_code(rsp), cls.get_rsp_error_msg(rsp))
- #-----------------------------------------------------------------------
@classmethod
- def getRspErrorCode(cls, rsp):
- """Return the error code of a response, or 0 if no error."""
+ def get_rsp_error_code(cls, rsp):
+ """Return the error code of an XMLNode Flickr response, or 0 if no
+ error.
+ """
+
+ LOG.warn("FlickrAPI.get_rsp_error_code has been deprecated and will be "
+ "removed in FlickrAPI version 1.2.")
+
if rsp['stat'] == "fail":
- return rsp.err[0]['code']
+ return int(rsp.err[0]['code'])
return 0
- #-----------------------------------------------------------------------
@classmethod
- def getRspErrorMsg(cls, rsp):
- """Return the error message of a response, or "Success" if no error."""
+ def get_rsp_error_msg(cls, rsp):
+ """Return the error message of an XMLNode Flickr response, or "Success"
+ if no error.
+ """
+
+ LOG.warn("FlickrAPI.get_rsp_error_msg has been deprecated and will be "
+ "removed in FlickrAPI version 1.2.")
+
if rsp['stat'] == "fail":
return rsp.err[0]['msg']
return "Success"
- #-----------------------------------------------------------------------
- def validateFrob(self, frob, perms):
- auth_url = self.__get_auth_url(perms, frob)
+ 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 getTokenPartOne(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.
+ 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).
- If the browser needs to take over the terminal, use fork=False,
- otherwise use fork=True.
-
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
+ - 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.getTokenPartTwo(...) 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:
+ - Call flickrapi.get_token_part_two(...) and pass it the
+ result value you stored.
- (token, frob) = flickr.getTokenPartOne(perms='write')
- if not token: raw_input("Press ENTER after you authorized this program")
- flickr.getTokenPartTwo((token, frob))
+ 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))
"""
# see if we have a saved token
@@ -463,134 +693,77 @@
if token:
LOG.debug("Trying cached token '%s'" % token)
try:
- rsp = self.auth_checkToken(
- api_key=self.api_key,
- auth_token=token)
+ rsp = self.auth_checkToken(auth_token=token, format='xmlnode')
# see if we have enough permissions
- tokenPerms = rsp.auth[0].perms[0].elementText
+ 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
- self.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(api_key=self.api_key, auth_token=None)
- self.testFailure(rsp)
+ rsp = self.auth_getFrob(auth_token=None, format='xmlnode')
+ self.test_failure(rsp)
- frob = rsp.frob[0].elementText
+ frob = rsp.frob[0].text
# validate online
- self.validateFrob(frob, perms)
+ self.validate_frob(frob, perms)
return (token, frob)
- def getTokenPartTwo(self, (token, frob)):
- """Part two of getting a token, see getTokenPartOne(...) for details."""
+ 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, we're done
+ # If a valid token was obtained in the past, we're done
if token:
- LOG.debug("getTokenPartTwo: no need, token already there")
- self.token = token
+ LOG.debug("get_token_part_two: no need, token already there")
+ self.token_cache.token = token
return token
- LOG.debug("getTokenPartTwo: getting a new token for frob '%s'" % frob)
+ 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(api_key=self.api_key, frob=frob)
- self.testFailure(rsp)
+ rsp = self.auth_getToken(frob=frob, auth_token=None, format='xmlnode')
+ self.test_failure(rsp)
- token = rsp.auth[0].token[0].elementText
- LOG.debug("getTokenPartTwo: new token '%s'" % token)
+ 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 = rsp.xml
- self.token = token
+ self.token_cache.token = token
return token
- #-----------------------------------------------------------------------
- def getToken(self, perms="read"):
- """Use this method if you're sure that the browser process ends
- when the user has granted the autorization - not sooner and
- not later.
-
- This method is deprecated, and will no longer be supported in
- future versions of this API. That's also why we don't tell you
- what it does in this documentation.
-
- Use something this instead:
-
- (token, frob) = flickr.getTokenPartOne(perms='write')
- if not token: raw_input("Press ENTER after you authorized this program")
- flickr.getTokenPartTwo((token, frob))
- """
-
- LOG.warn("Deprecated method getToken(...) called")
-
- (token, frob) = self.getTokenPartOne(perms)
- return self.getTokenPartTwo((token, frob))
-
-
-########################################################################
-# App functionality
-########################################################################
-
-def main():
- '''This is just a demonstration of the FlickrAPI usage.
- For more information, see the package documentation in the 'doc'
- directory.
- '''
-
- # flickr auth information:
- flickr_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # API key
- flickr_secret = "yyyyyyyyyyyyyyyy" # shared "secret"
-
- # make a new FlickrAPI instance
- fapi = FlickrAPI(flickr_key, flickr_secret)
-
- # do the whole whatever-it-takes to get a valid token:
- (token, frob) = fapi.getTokenPartOne(browser='firefox', perms='write')
- if not token:
- raw_input("Press ENTER after you authorized this program")
- fapi.getTokenPartTwo((token, frob))
-
- # get my favorites
- rsp = fapi.favorites_getList()
- fapi.testFailure(rsp)
-
- # and print them
- for photo in rsp.photos[0].photo:
- print "%10(id)s: %(title)s" % photo
-
- # upload the file foo.jpg
- #rsp = fapi.upload(filename="foo.jpg", \
- # title="This is the title", description="This is the description", \
- # tags="tag1 tag2 tag3", is_public="1")
- #if rsp == None:
- # sys.stderr.write("can't find file\n")
- #else:
- # fapi.testFailure(rsp)
-
- return 0
-
def set_log_level(level):
'''Sets the log level of the logger used by the FlickrAPI module.
- >>> import flicrkapi
+ >>> import flickrapi
>>> import logging
>>> flickrapi.set_log_level(logging.INFO)
'''
+ import flickrapi.tokencache
+
LOG.setLevel(level)
-
-# run the main if we're not being imported:
-if __name__ == "__main__":
- sys.exit(main())
+ flickrapi.tokencache.LOG.setLevel(level)
+
+if __name__ == "__main__":
+ print "Running doctests"
+ import doctest
+ doctest.testmod()
+ print "Tests OK"
Added: trunk/conduit/modules/FlickrModule/flickrapi/cache.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/FlickrModule/flickrapi/cache.py Tue May 6 13:46:03 2008
@@ -0,0 +1,105 @@
+# -*- 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)
Added: trunk/conduit/modules/FlickrModule/flickrapi/exceptions.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/FlickrModule/flickrapi/exceptions.py Tue May 6 13:46:03 2008
@@ -0,0 +1,15 @@
+'''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.
+ '''
Modified: trunk/conduit/modules/FlickrModule/flickrapi/multipart.py
==============================================================================
--- trunk/conduit/modules/FlickrModule/flickrapi/multipart.py (original)
+++ trunk/conduit/modules/FlickrModule/flickrapi/multipart.py Tue May 6 13:46:03 2008
@@ -9,9 +9,13 @@
'''A single part of the multipart data.
>>> Part({'name': 'headline'}, 'Nice Photo')
-
- >>> image = 'photo.jpg'
+ ... # doctest: +ELLIPSIS
+ <flickrapi.multipart.Part object at 0x...>
+
+ >>> image = file('tests/photo.jpg')
>>> Part({'name': 'photo', 'filename': image}, image.read(), 'image/jpeg')
+ ... # doctest: +ELLIPSIS
+ <flickrapi.multipart.Part object at 0x...>
'''
def __init__(self, parameters, payload, content_type=None):
@@ -22,7 +26,8 @@
def render(self):
'''Renders this part -> List of Strings'''
- parameters = ['%s="%s"' % (k, v) for k, v in self.parameters.iteritems()]
+ parameters = ['%s="%s"' % (k, v)
+ for k, v in self.parameters.iteritems()]
lines = ['Content-Disposition: form-data; %s' % '; '.join(parameters)]
@@ -42,13 +47,16 @@
'''A single part with a file as the payload
This example has the same semantics as the second Part example:
- >>> FilePart({'name': 'photo'}, 'photo.jpg', 'image/jpeg')
+
+ >>> FilePart({'name': 'photo'}, 'tests/photo.jpg', 'image/jpeg')
+ ... #doctest: +ELLIPSIS
+ <flickrapi.multipart.FilePart object at 0x...>
'''
def __init__(self, parameters, filename, content_type):
parameters['filename'] = filename
- imagefile = open(filename)
+ imagefile = open(filename, 'rb')
payload = imagefile.read()
imagefile.close()
@@ -89,4 +97,5 @@
def header(self):
'''Returns the top-level HTTP header of this multipart'''
- return ("Content-Type", "multipart/form-data; boundary=%s" % self.boundary)
+ return ("Content-Type",
+ "multipart/form-data; boundary=%s" % self.boundary)
Modified: trunk/conduit/modules/FlickrModule/flickrapi/tokencache.py
==============================================================================
--- trunk/conduit/modules/FlickrModule/flickrapi/tokencache.py (original)
+++ trunk/conduit/modules/FlickrModule/flickrapi/tokencache.py Tue May 6 13:46:03 2008
@@ -2,73 +2,92 @@
'''Persistent token cache management for the Flickr API'''
import os.path
+import logging
-from flickrapi.xmlnode import XMLNode
+LOG = logging.getLogger(__name__)
-__all__ = ('TokenCache', )
+__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.
+ 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=""):
+ def __init__(self, api_key, username=None):
'''Creates a new token cache instance'''
self.api_key = api_key
- if username != "":
- self.auth_filename = "%s-auth.xml" % username
- else:
- self.auth_filename = "auth.xml"
+ self.username = username
+ self.memory = {}
- def __getCachedTokenPath(self):
+ 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 __getCachedTokenFilename(self):
+ def __get_cached_token_filename(self):
"""Return the full pathname of the cached token file."""
- return os.path.join(self.__getCachedTokenPath(), self.auth_filename)
+
+ if self.username:
+ filename = 'auth-%s.token' % self.username
+ else:
+ filename = 'auth.token'
+
+ return os.path.join(self.__get_cached_token_path(), filename)
- def __getCachedToken(self):
+ 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, which is basically the
- entire RSP response containing the auth element.
+ 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.__getCachedTokenFilename(), "r")
-
- data = f.read()
+ f = file(self.__get_cached_token_filename(), "r")
+ token = f.read()
f.close()
- rsp = XMLNode.parseXML(data)
-
- return rsp.auth[0].token[0].elementText
-
- except Exception:
+ return token.strip()
+ except IOError:
return None
- def __setCachedToken(self, token_xml):
- """Cache a token for later use.
-
- The cached tag is stored by simply saving the entire RSP response
- containing the auth element.
+ def __set_cached_token(self, token):
+ """Cache a token for later use."""
- """
+ # Remember for later use
+ self.memory[self.username] = token
- path = self.__getCachedTokenPath()
+ path = self.__get_cached_token_path()
if not os.path.exists(path):
os.makedirs(path)
- f = file(self.__getCachedTokenFilename(), "w")
- f.write(token_xml)
+ f = file(self.__get_cached_token_filename(), "w")
+ print >>f, token
f.close()
def forget(self):
'''Removes the cached token'''
- os.unlink(self.__getCachedTokenFilename())
+ 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(__getCachedToken, __setCachedToken, forget, "The cached token")
+ token = property(__get_cached_token, __set_cached_token, forget, "The cached token")
Modified: trunk/conduit/modules/FlickrModule/flickrapi/xmlnode.py
==============================================================================
--- trunk/conduit/modules/FlickrModule/flickrapi/xmlnode.py (original)
+++ trunk/conduit/modules/FlickrModule/flickrapi/xmlnode.py Tue May 6 13:46:03 2008
@@ -13,31 +13,36 @@
class XMLNode:
"""XMLNode -- generic class for holding an XML node
- xml_str = '''<xml foo="32">
- <name bar="10">Name0</name>
- <name bar="11" baz="12">Name1</name>
- </xml>'''
-
- f = XMLNode.parseXML(xml_str)
-
- print f.elementName # xml
- print f['foo'] # 32
- print f.name # [<name XMLNode>, <name XMLNode>]
- print f.name[0].elementName # name
- print f.name[0]["bar"] # 10
- print f.name[0].elementText # Name0
- print f.name[1].elementName # name
- print f.name[1]["bar"] # 11
- print f.name[1]["baz"] # 12
+ >>> xml_str = '''<xml foo="32">
+ ... <taggy bar="10">Name0</taggy>
+ ... <taggy bar="11" baz="12">Name1</taggy>
+ ... </xml>'''
+ >>> 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.elementName = ""
- self.elementText = ""
+ self.name = ""
+ self.text = ""
self.attrib = {}
- self.xml = ""
+ self.xml = None
def __setitem__(self, key, item):
"""Store a node's attribute in the attrib hash."""
@@ -47,9 +52,40 @@
"""Retrieve a node's attribute from the attrib hash."""
return self.attrib[key]
- #-----------------------------------------------------------------------
@classmethod
- def parseXML(cls, xml_str, store_xml=False):
+ 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 <name />
+ # 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
@@ -57,41 +93,10 @@
"""
- def __parseXMLElement(element, thisNode):
- """Recursive call to process this XMLNode."""
- thisNode.elementName = element.nodeName
-
- #print element.nodeName
-
- # add element attributes as attributes to this node
- for i in range(element.attributes.length):
- an = element.attributes.item(i)
- thisNode[an.name] = an.nodeValue
-
- for a in element.childNodes:
- if a.nodeType == xml.dom.Node.ELEMENT_NODE:
-
- child = XMLNode()
- try:
- list = getattr(thisNode, a.nodeName)
- except AttributeError:
- setattr(thisNode, a.nodeName, [])
-
- # add the child node as an attrib to this node
- list = getattr(thisNode, a.nodeName)
- list.append(child)
-
- __parseXMLElement(a, child)
-
- elif a.nodeType == xml.dom.Node.TEXT_NODE:
- thisNode.elementText += a.nodeValue
-
- return thisNode
-
dom = xml.dom.minidom.parseString(xml_str)
# get the root
- rootNode = XMLNode()
- if store_xml: rootNode.xml = xml_str
+ root_node = XMLNode()
+ if store_xml: root_node.xml = xml_str
- return __parseXMLElement(dom.firstChild, rootNode)
+ return cls.__parse_element(dom.firstChild, root_node)
Modified: trunk/conduit/modules/RTMModule/rtm.py
==============================================================================
--- trunk/conduit/modules/RTMModule/rtm.py (original)
+++ trunk/conduit/modules/RTMModule/rtm.py Tue May 6 13:46:03 2008
@@ -20,9 +20,7 @@
except ImportError:
pass
-logging.basicConfig()
LOG = logging.getLogger(__name__)
-LOG.setLevel(logging.INFO)
SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
@@ -418,9 +416,5 @@
import rtm
rtm2 = rtm.createRTM(apiKey='fe049e2cec86568f3d79c964d4a45f5c',secret='b57757de51f7e919',token=None)
get_all_tasks(rtm2)
-
-
-
-
-
\ No newline at end of file
+
Modified: trunk/scripts/update-3rdparty-libs.sh
==============================================================================
--- trunk/scripts/update-3rdparty-libs.sh (original)
+++ trunk/scripts/update-3rdparty-libs.sh Tue May 6 13:46:03 2008
@@ -7,13 +7,15 @@
fi
#update flickrapi
-#svn export --force https://flickrapi.svn.sourceforge.net/svnroot/flickrapi/trunk/flickrapi/ conduit/modules/FlickrModule/flickrapi/
-#patch -p0 < conduit/modules/FlickrModule/flickrapi/multi-username.patch
+echo "Please use stable flickr api releases"
#update pyfacebook
svn export --force http://pyfacebook.googlecode.com/svn/trunk/facebook/__init__.py conduit/modules/FacebookModule/pyfacebook/__init__.py
#update pybackpack
-#wget -qO - http://hg.west.spy.net/hg/python/backpack/archive/tip.tar.gz | tar --wildcards -xzOf - */backpack.py > conduit/modules/BackpackModule/backpack/backpack.py
+#for i in COPYING backpack.py; do
+# wget -qO conduit/modules/BackpackModule/backpack/${i} http://github.com/dustin/py-backpack/tree/master%2F${i}?raw=true
+#done
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]