conduit r1450 - in trunk: . conduit/modules/FlickrModule conduit/modules/FlickrModule/flickrapi conduit/modules/RTMModule scripts



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]