← Back to team overview

gtg-user team mailing list archive

[Merge] lp:~gtg-user/gtg/rtm-backend into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/rtm-backend into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


Remember the milk backend. Tested, documented and ready to go.
-- 
https://code.launchpad.net/~gtg-user/gtg/rtm-backend/+merge/33610
Your team Gtg users is subscribed to branch lp:~gtg-user/gtg/rtm-backend.
=== added file 'GTG/backends/backend_rtm.py'
--- GTG/backends/backend_rtm.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_rtm.py	2010-08-25 05:35:52 +0000
@@ -0,0 +1,915 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Remember the milk backend
+'''
+
+import os
+import uuid
+import threading
+import datetime
+import subprocess
+import exceptions
+from dateutil.tz                        import tzutc, tzlocal
+
+from GTG.backends.genericbackend        import GenericBackend
+from GTG                                import _
+from GTG.backends.backendsignals        import BackendSignals
+from GTG.backends.syncengine            import SyncEngine, SyncMeme
+from GTG.backends.rtm.rtm               import createRTM, RTMError, RTMAPIError
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.tools.dates                    import RealDate, NoDate
+from GTG.core.task                      import Task
+from GTG.tools.interruptible            import interruptible
+from GTG.tools.logger                   import Log
+
+
+
+
+
+class Backend(PeriodicImportBackend):
+    
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME:       "backend_rtm", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Remember The Milk"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_READWRITE, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("This backend synchronizes your tasks with the web service"
+              " RememberTheMilk (http://rememberthemilk.com).\n"
+              "Note: This product uses the Remember The Milk API but is not"
+              " endorsed or certified by Remember The Milk"),\
+        }
+
+    _static_parameters = { \
+        "period": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 10, },
+        }
+
+###############################################################################
+### Backend standard methods ##################################################
+###############################################################################
+
+    def __init__(self, parameters):
+        """
+        See GenericBackend for an explanation of this function.
+        Loads the saved state of the sync, if any
+        """
+        super(Backend, self).__init__(parameters)
+        #loading the saved state of the synchronization, if any
+        self.sync_engine_path = os.path.join('backends/rtm/', \
+                                      "sync_engine-" + self.get_id())
+        self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
+                                                   SyncEngine())
+        #reloading the oauth authentication token, if any
+        self.token_path = os.path.join('backends/rtm/', \
+                                      "auth_token-" + self.get_id())
+        self.token = self._load_pickled_file(self.token_path, None)
+        self.enqueued_start_get_task = False
+        self.login_event = threading.Event()
+        
+    def initialize(self):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        super(Backend, self).initialize()
+        self.rtm_proxy = RTMProxy(self._ask_user_to_confirm_authentication,
+                                  self.token)
+
+    def save_state(self):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        self._store_pickled_file(self.sync_engine_path, self.sync_engine)
+
+    def _ask_user_to_confirm_authentication(self):
+        '''
+        Calls for a user interaction during authentication
+        '''
+        self.login_event.clear()
+        BackendSignals().interaction_requested(self.get_id(),
+            "You need to authenticate to Remember The Milk. A browser"
+            " is opening with a login page.\n When you have "
+            " logged in and given GTG the requested permissions,\n"
+            " press the 'Confirm' button", \
+            BackendSignals().INTERACTION_CONFIRM, \
+            "on_login")
+        self.login_event.wait()
+        
+    def on_login(self):
+        '''
+        Called when the user confirms the login
+        '''
+        self.login_event.set()
+
+###############################################################################
+### TWO WAY SYNC ##############################################################
+###############################################################################
+
+    def do_periodic_import(self):
+        """
+        See PeriodicImportBackend for an explanation of this function.
+        """
+        #we verify authentication
+        if not self.rtm_proxy.is_authenticated():
+            #we try to reach RTM to trigger the authentication process
+            threading.Thread(target = self.rtm_proxy.get_rtm_tasks_dict).start()
+            if self.enqueued_start_get_task:
+                return
+            else:
+                self.enqueued_start_get_task = True
+                self.rtm_proxy.wait_for_authentication()
+                self.enqueued_start_get_task = False
+        
+        #we save the token, if authentication was ok
+        if not hasattr(self, 'token_saved'):
+            self._store_pickled_file(self.token_path,
+                                 self.rtm_proxy.get_auth_token())
+            self.token_saved = True
+
+        #we get the old list of synced tasks, and compare with the new tasks set
+        stored_rtm_task_ids = self.sync_engine.get_all_remote()
+        current_rtm_task_ids = [tid for tid in \
+                            self.rtm_proxy.get_rtm_tasks_dict().iterkeys()]
+        for rtm_task_id in current_rtm_task_ids:
+            self.cancellation_point()
+            #Adding and updating
+            self._process_rtm_task(rtm_task_id)
+
+        for rtm_task_id in set(stored_rtm_task_ids).difference(\
+                                        set(current_rtm_task_ids)):
+            self.cancellation_point()
+            #Removing the old ones
+            if not self.please_quit:
+                tid = self.sync_engine.get_local_id(rtm_task_id)
+                self.datastore.request_task_deletion(tid)
+                try:
+                    self.sync_engine.break_relationship(remote_id = \
+                                                        rtm_task_id)
+                    self.save_state()
+                except KeyError:
+                    pass
+        
+    @interruptible
+    def remove_task(self, tid):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        if not self.rtm_proxy.is_authenticated():
+            return
+        self.cancellation_point()
+        try:
+            rtm_task_id = self.sync_engine.get_remote_id(tid)
+            if rtm_task_id not in self.rtm_proxy.get_rtm_tasks_dict():
+                #we might need to refresh our task cache
+                self.rtm_proxy.refresh_rtm_tasks_dict()
+            rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+            rtm_task.delete()
+            Log.debug("removing task %s from RTM" % rtm_task_id)
+        except KeyError:
+            pass
+            try:
+                self.sync_engine.break_relationship(local_id = tid)
+                self.save_state()
+            except:
+                pass
+
+
+###############################################################################
+### Process tasks #############################################################
+###############################################################################
+
+    @interruptible
+    def set_task(self, task):
+        """
+        See GenericBackend for an explanation of this function.
+        """
+        if not self.rtm_proxy.is_authenticated():
+            return
+        self.cancellation_point()
+        tid = task.get_id()
+        is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
+        action, rtm_task_id = self.sync_engine.analyze_local_id( \
+                                tid, \
+                                self.datastore.has_task, \
+                                self.rtm_proxy.has_rtm_task, \
+                                is_syncable)
+        Log.debug("GTG->RTM set task (%s, %s)" % (action, is_syncable))
+
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            rtm_task = self.rtm_proxy.create_new_rtm_task(task.get_title())
+            self._populate_rtm_task(task, rtm_task)
+            meme = SyncMeme(task.get_modified(),
+                            rtm_task.get_modified(),
+                            "GTG")
+            self.sync_engine.record_relationship( \
+                local_id = tid, remote_id = rtm_task.get_id(), meme = meme)
+
+        elif action == SyncEngine.UPDATE:
+            try:
+                rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+            except KeyError:
+                #in this case, we don't have yet the task in our local cache
+                # of what's on the rtm website
+                self.rtm_proxy.refresh_rtm_tasks_dict()
+                rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+            with self.datastore.get_backend_mutex():
+                meme = self.sync_engine.get_meme_from_local_id(task.get_id())
+                newest = meme.which_is_newest(task.get_modified(),
+                                              rtm_task.get_modified())
+                if newest == "local":
+                    self._populate_rtm_task(task, rtm_task)
+                    meme.set_remote_last_modified(rtm_task.get_modified())
+                    meme.set_local_last_modified(task.get_modified())
+                else:
+                    #we skip saving the state
+                    return
+
+        elif action == SyncEngine.REMOVE:
+            self.datastore.request_task_deletion(tid)
+            try:
+                self.sync_engine.break_relationship(local_id = tid)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            try:
+                rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+            except KeyError:
+                #in this case, we don't have yet the task in our local cache
+                # of what's on the rtm website
+                self.rtm_proxy.refresh_rtm_tasks_dict()
+                rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+            self._exec_lost_syncability(tid, rtm_task)
+
+            self.save_state()
+
+    def _exec_lost_syncability(self, tid, rtm_task):
+        '''
+        Executed when a relationship between tasks loses its syncability
+        property. See SyncEngine for an explanation of that.
+
+        @param tid: a GTG task tid
+        @param note: a RTM task
+        '''
+        self.cancellation_point()
+        meme = self.sync_engine.get_meme_from_local_id(tid)
+        #First of all, the relationship is lost
+        self.sync_engine.break_relationship(local_id = tid)
+        if meme.get_origin() == "GTG":
+            rtm_task.delete()
+        else:
+            self.datastore.request_task_deletion(tid)
+            
+    def _process_rtm_task(self, rtm_task_id):
+        self.cancellation_point()
+        if not self.rtm_proxy.is_authenticated():
+            return
+        rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
+        is_syncable = self._rtm_task_is_syncable_per_attached_tags(rtm_task)
+        action, tid = self.sync_engine.analyze_remote_id( \
+                                             rtm_task_id,
+                                             self.datastore.has_task,
+                                             self.rtm_proxy.has_rtm_task,
+                                             is_syncable)
+        Log.debug("GTG<-RTM set task (%s, %s)" % (action, is_syncable))
+
+        if action == None:
+            return
+
+        if action == SyncEngine.ADD:
+            tid = str(uuid.uuid4())
+            task = self.datastore.task_factory(tid)
+            self._populate_task(task, rtm_task)
+            meme = SyncMeme(task.get_modified(),
+                            rtm_task.get_modified(),
+                            "RTM")
+            self.sync_engine.record_relationship( \
+                    local_id = tid,
+                    remote_id = rtm_task_id,
+                    meme = meme)
+            self.datastore.push_task(task)
+
+        elif action == SyncEngine.UPDATE:
+            task = self.datastore.get_task(tid)
+            with self.datastore.get_backend_mutex():
+                meme = self.sync_engine.get_meme_from_remote_id(rtm_task_id)
+                newest = meme.which_is_newest(task.get_modified(),
+                                              rtm_task.get_modified())
+                if newest == "remote":
+                    self._populate_task(task, rtm_task)
+                    meme.set_remote_last_modified(rtm_task.get_modified())
+                    meme.set_local_last_modified(task.get_modified())
+                else:
+                    #we skip saving the state
+                    return
+
+        elif action == SyncEngine.REMOVE:
+            try:
+                rtm_task.delete()
+                self.sync_engine.break_relationship(remote_id = rtm_task_id)
+            except KeyError:
+                pass
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            self._exec_lost_syncability(tid, rtm_task)
+
+        self.save_state()
+
+###############################################################################
+### Helper methods ############################################################
+###############################################################################
+
+    def _populate_task(self, task, rtm_task):
+        '''
+        Copies the content of a RTMTask in a Task
+        '''
+        task.set_title(rtm_task.get_title())
+        task.set_text(rtm_task.get_text())
+        task.set_due_date(rtm_task.get_due_date())
+        status = rtm_task.get_status()
+        if GTG_TO_RTM_STATUS[task.get_status()] != status:
+            task.set_status(rtm_task.get_status())
+        #tags
+        tags = set(['@%s' % tag for tag in rtm_task.get_tags()])
+        gtg_tags_lower = set([t.get_name().lower() for t in task.get_tags()])
+        #tags to remove
+        for tag in gtg_tags_lower.difference(tags):
+            task.remove_tag(tag)
+        #tags to add
+        for tag in tags.difference(gtg_tags_lower):
+            gtg_all_tags = [t.get_name() for t in \
+                            self.datastore.get_all_tags()]
+            matching_tags = filter(lambda t: t.lower() == tag, gtg_all_tags)
+            if len(matching_tags) !=  0:
+                tag = matching_tags[0]
+            task.add_tag(tag)
+
+    def _populate_rtm_task(self, task, rtm_task):
+        '''
+        Copies the content of a Task into a RTMTask
+        '''
+        #Get methods of an rtm_task are fast, set are slow: therefore,
+        # we try to use set as rarely as possible
+
+        #first thing: the status. This way, if we are syncing a completed
+        # task it doesn't linger for ten seconds in the RTM Inbox
+        status = task.get_status()
+        if rtm_task.get_status() != status:
+            rtm_task.set_status(status)
+        title = task.get_title()
+        if rtm_task.get_title() != title:
+            rtm_task.set_title(title)
+        text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
+        if rtm_task.get_text() != text:
+            rtm_task.set_text(text)
+        tags = task.get_tags_name()
+        rtm_task_tags = []
+        for tag in rtm_task.get_tags():
+            if tag[0] != '@':
+                tag = '@' + tag
+            rtm_task_tags.append(tag)
+        #rtm tags are lowercase only
+        if rtm_task_tags != [t.lower() for t in tags]:
+            rtm_task.set_tags(tags)
+        if isinstance(task.get_due_date(), NoDate):
+            due_date = None
+        else:
+            due_date = task.get_due_date().to_py_date()
+        if rtm_task.get_due_date() != due_date:
+            rtm_task.set_due_date(due_date)
+
+    def _rtm_task_is_syncable_per_attached_tags(self, rtm_task):
+        '''
+        Helper function which checks if the given task satisfies the filtering
+        imposed by the tags attached to the backend.
+        That means, if a user wants a backend to sync only tasks tagged @works,
+        this function should be used to check if that is verified.
+
+        @returns bool: True if the task should be synced
+        '''
+        attached_tags = self.get_attached_tags()
+        if GenericBackend.ALLTASKS_TAG in attached_tags:
+            return True
+        for tag in rtm_task.get_tags():
+            if "@" + tag in attached_tags:
+                return  True
+        return False
+
+###############################################################################
+### RTM PROXY #################################################################
+###############################################################################
+
+class RTMProxy(object):
+    '''
+    The purpose of this class is producing an updated list of RTMTasks.
+    To do that, it handles:
+        - authentication to RTM
+        - keeping the list fresh
+        - downloading the list
+    '''
+
+
+    PUBLIC_KEY = "2a440fdfe9d890c343c25a91afd84c7e"
+    PRIVATE_KEY = "ca078fee48d0bbfa"
+
+    def __init__(self,
+                 auth_confirm_fun,
+                 token = None):
+        self.auth_confirm = auth_confirm_fun
+        self.token = token
+        self.authenticated = threading.Event()
+        self.login_event = threading.Event()
+        self.is_not_refreshing = threading.Event()
+        self.is_not_refreshing.set()
+
+    ##########################################################################
+    ### AUTHENTICATION #######################################################
+    ##########################################################################
+
+    def start_authentication(self):
+        '''
+        Launches the authentication process
+        '''
+        initialize_thread = threading.Thread(target = self._authenticate)
+        initialize_thread.setDaemon(True)
+        initialize_thread.start()
+    
+    def is_authenticated(self):
+        '''
+        Returns true if we've autheticated to RTM
+        '''
+        return self.authenticated.isSet()
+
+    def wait_for_authentication(self):
+        '''
+        Inhibits the thread until authentication occours
+        '''
+        self.authenticated.wait()
+
+    def get_auth_token(self):
+        '''
+        Returns the oauth token, or none
+        '''
+        try:
+            return self.token
+        except:
+            return None
+
+    def _authenticate(self):
+        '''
+        authentication main function
+        '''
+        self.authenticated.clear()
+        while not self.authenticated.isSet():
+            if not self.token:
+                self.rtm= createRTM(self.PUBLIC_KEY, self.PRIVATE_KEY, self.token)
+                subprocess.Popen(['xdg-open', self.rtm.getAuthURL()])
+                self.auth_confirm()
+                try:
+                    self.token = self.rtm.getToken()
+                except Exception, e:
+                    #something went wrong.
+                    self.token = None
+                    continue
+            try:
+                if self._login():
+                    self.authenticated.set()
+            except exceptions.IOError, e:
+                BackendSignals().backend_failed(self.get_id(), \
+                            BackendSignals.ERRNO_NETWORK)
+
+    def _login(self):
+        '''
+        Tries to establish a connection to rtm with a token got from the
+        authentication process
+        '''
+        try:
+            self.rtm = createRTM(self.PUBLIC_KEY, self.PRIVATE_KEY, self.token)
+            self.timeline = self.rtm.timelines.create().timeline
+            return True
+        except (RTMError, RTMAPIError), e:
+            Log.error("RTM ERROR" + str(e))
+        return False
+    
+    ##########################################################################
+    ### RTM TASKS HANDLING ###################################################
+    ##########################################################################
+
+    def get_rtm_tasks_dict(self):
+        '''
+        Returns a dict of RTMtasks. It will start authetication if necessary.
+        The dict is kept updated automatically.
+        '''
+        if not hasattr(self, '_rtm_task_dict'):
+            self.refresh_rtm_tasks_dict()
+        else:
+            time_difference = datetime.datetime.now() - \
+                          self.__rtm_task_dict_timestamp
+            if time_difference.seconds > 60:
+                self.refresh_rtm_tasks_dict()
+        return self._rtm_task_dict.copy()
+
+    def __getattr_the_rtm_way(self, an_object, attribute):
+        '''
+        RTM, to compress the XML file they send to you, cuts out all the
+        unnecessary stuff.
+        Because of that, getting an attribute from an object must check if one
+        of those optimizations has been used.
+        This function always returns a list wrapping the objects found (if any).
+        '''
+        try:
+            list_or_object = getattr(an_object, attribute)
+        except AttributeError:
+            return []
+
+        if isinstance(list_or_object, list):
+            return list_or_object
+        else:
+            return [list_or_object]
+
+    def __get_rtm_lists(self):
+        '''
+        Gets the list of the RTM Lists (the tabs on the top of rtm website)
+        '''
+        rtm_get_list_output = self.rtm.lists.getList()
+        #Here's the attributes of RTM lists. For the list of them, see
+        #http://www.rememberthemilk.com/services/api/methods/rtm.lists.getList.rtm
+        return self.__getattr_the_rtm_way(self.rtm.lists.getList().lists, 'list')
+
+    def __get_rtm_taskseries_in_list(self, list_id):
+        '''
+        Gets the list of "taskseries" objects in a rtm list.
+        For an explenation of what are those, see
+        http://www.rememberthemilk.com/services/api/tasks.rtm
+        '''
+        list_object_wrapper = self.rtm.tasks.getList(list_id = list_id, \
+                                filter = 'includeArchived:true').tasks
+        list_object_list = self.__getattr_the_rtm_way(list_object_wrapper, 'list')
+        if not list_object_list:
+            return []
+        #we asked for one, so we should get one
+        assert(len(list_object_list), 1)
+        list_object = list_object_list[0]
+        #check that the given list is the correct one
+        assert(list_object.id == list_id)
+        return self.__getattr_the_rtm_way(list_object, 'taskseries')
+
+    def refresh_rtm_tasks_dict(self):
+        '''
+        Builds a list of RTMTasks fetched from RTM
+        '''
+        if not self.is_authenticated():
+            self.start_authentication()
+            self.wait_for_authentication()
+
+        if not self.is_not_refreshing.isSet():
+            #if we're already refreshing, we just wait for that to happen and
+            # then we immediately return
+            self.is_not_refreshing.wait()
+            return
+        self.is_not_refreshing.clear()
+        Log.debug('refreshing rtm')
+
+        #To understand what this function does, here's a sample output of the
+        #plain getLists() from RTM api:
+        #    http://www.rememberthemilk.com/services/api/tasks.rtm
+
+        #our purpose is to fill this with "tasks_id: RTMTask" items
+        rtm_tasks_dict = {}
+
+        rtm_lists_list = self.__get_rtm_lists()
+        #for each rtm list, we retrieve all the tasks in it
+        for rtm_list in rtm_lists_list:
+            if rtm_list.archived != '0' or rtm_list.smart != '0':
+                #we skip archived and smart lists
+                continue
+            rtm_taskseries_list = self.__get_rtm_taskseries_in_list(rtm_list.id)
+            for rtm_taskseries in rtm_taskseries_list:
+                #we drill down to actual tasks
+                rtm_tasks_list = self.__getattr_the_rtm_way(rtm_taskseries, 'task')
+                for rtm_task in rtm_tasks_list:
+                    rtm_tasks_dict[rtm_task.id] = RTMTask(rtm_task,
+                                                          rtm_taskseries,
+                                                          rtm_list,
+                                                          self.rtm,
+                                                          self.timeline)
+
+        #we're done: we store the dict in this class and we annotate the time we
+        # got it
+        self._rtm_task_dict = rtm_tasks_dict
+        self.__rtm_task_dict_timestamp = datetime.datetime.now()
+        print "UNLOCK"
+        self.is_not_refreshing.set()
+
+    def has_rtm_task(self, rtm_task_id):
+        '''
+        Returns True if we have seen that task id
+        '''
+        cache_result = rtm_task_id in self.get_rtm_tasks_dict()
+        return cache_result
+        #it may happen that the rtm_task is on the website but we haven't
+        #downloaded it yet. We need to update the local cache.
+
+        #it's a big speed loss. Let's see if we can avoid it.
+        #self.refresh_rtm_tasks_dict()
+        #return rtm_task_id in self.get_rtm_tasks_dict()
+
+    def create_new_rtm_task(self, title):
+        '''
+        Creates a new rtm task
+        '''
+        result = self.rtm.tasks.add(timeline = self.timeline,  name = title)
+        rtm_task = RTMTask(result.list.taskseries.task,
+                           result.list.taskseries,
+                           result.list,
+                           self.rtm,
+                           self.timeline)
+        #adding to the dict right away
+        if hasattr(self, '_rtm_task_dict'):
+            #if the list hasn't been downloaded yet, we do not create a list,
+            # because the fact that the list is created is used to keep track of
+            # list updates
+            self._rtm_task_dict[rtm_task.get_id()] = rtm_task
+        return rtm_task
+
+
+
+###############################################################################
+### RTM TASK ##################################################################
+###############################################################################
+
+#dictionaries to translate a RTM status into a GTG one (and back)
+GTG_TO_RTM_STATUS = {Task.STA_ACTIVE: True,
+                     Task.STA_DONE: False,
+                     Task.STA_DISMISSED: False}
+
+RTM_TO_GTG_STATUS = {True: Task.STA_ACTIVE,
+                     False: Task.STA_DONE}
+
+
+
+class RTMTask(object):
+    '''
+    A proxy object that encapsulates a RTM task, giving an easier API to access
+    and modify its attributes.
+    This backend already uses a library to interact with RTM, but that is just a
+    thin proxy for HTML gets and posts.
+    The meaning of all "special words"
+        http://www.rememberthemilk.com/services/api/tasks.rtm
+    '''
+    
+
+    def __init__(self, rtm_task, rtm_taskseries, rtm_list, rtm, timeline):
+        '''
+        sets up the various parameters needed to interact with a task.
+
+         @param task: the task object given by the underlying library
+         @param rtm_list: the rtm list the task resides in.
+         @param rtm_taskseries: all the tasks are encapsulated in a taskseries
+                               object. From RTM website:
+                               "A task series is a grouping of tasks generated
+                               by a recurrence pattern (more specifically, a
+                               recurrence pattern of type every – an after type
+                               recurrence generates a new task series for every
+                               occurrence). Task series' share common
+                               properties such as:
+                                 Name.
+                                 Recurrence pattern.
+                                 Tags.
+                                 Notes.
+                                 Priority."
+        @param rtm: a handle of the rtm object, to be able to speak with rtm.
+                    Authentication should have already been done.
+        @param timeline: a "timeline" is a series of operations rtm can undo in
+                         bulk. We are free of requesting new timelines as we
+                         please, with the obvious drawback of being slower.
+        '''
+        self.rtm_task = rtm_task
+        self.rtm_list = rtm_list
+        self.rtm_taskseries = rtm_taskseries
+        self.rtm = rtm
+        self.timeline = timeline
+
+    def get_title(self):
+        '''Returns the title of the task, if any'''
+        return self.rtm_taskseries.name
+
+    def set_title(self, title):
+        '''Sets the task title'''
+        self.rtm.tasks.setName(timeline      = self.timeline,
+                               list_id       = self.rtm_list.id,
+                               taskseries_id = self.rtm_taskseries.id,
+                               task_id       = self.rtm_task.id,
+                               name          = title)
+
+    def get_id(self):
+        '''Return the task id. The taskseries id is *different*'''
+        return self.rtm_task.id
+
+    def get_status(self):
+        '''Returns the task status, in GTG terminology'''
+        return RTM_TO_GTG_STATUS[self.rtm_task.completed == ""]
+
+    def set_status(self, gtg_status):
+        '''Sets the task status, in GTG terminology'''
+        status = GTG_TO_RTM_STATUS[gtg_status]
+        if status == True:
+            api_call = self.rtm.tasks.uncomplete
+        else:
+            api_call = self.rtm.tasks.complete
+        api_call(timeline      = self.timeline,
+                 list_id       = self.rtm_list.id,
+                 taskseries_id = self.rtm_taskseries.id,
+                 task_id       = self.rtm_task.id)
+
+    def get_tags(self):
+        '''Returns the task tags'''
+        tags = self.rtm_taskseries.tags
+        if not tags:
+            return []
+        else:
+            return self.__getattr_the_rtm_way(tags, 'tag')
+
+    def __getattr_the_rtm_way(self, an_object, attribute):
+        '''
+        RTM, to compress the XML file they send to you, cuts out all the
+        unnecessary stuff.
+        Because of that, getting an attribute from an object must check if one
+        of those optimizations has been used.
+        This function always returns a list wrapping the objects found (if any).
+        '''
+        try:
+            list_or_object = getattr(an_object, attribute)
+        except AttributeError:
+            return []
+        if isinstance(list_or_object, list):
+            return list_or_object
+        else:
+            return [list_or_object]
+
+    def set_tags(self, tags):
+        '''
+        Sets a new set of tags to a task. Old tags are deleted.
+        '''
+        #RTM accept tags without "@" as prefix,  and lowercase
+        tags = [tag[1:].lower() for tag in tags]
+        #formatting them in a comma-separated string
+        if len(tags) > 0:
+            tagstxt = reduce(lambda x,y: x + ", " + y, tags)
+        else:
+            tagstxt = ""
+        self.rtm.tasks.setTags(timeline     = self.timeline,
+                              list_id       = self.rtm_list.id,
+                              taskseries_id = self.rtm_taskseries.id,
+                              task_id       = self.rtm_task.id,
+                              tags          = tagstxt)
+
+    def get_text(self):
+        '''
+        Gets the content of RTM notes, aggregated in a single string
+        '''
+        notes = self.rtm_taskseries.notes
+        if not notes:
+            return ""
+        else:
+            note_list = self.__getattr_the_rtm_way(notes, 'note')
+            return "".join(map(lambda note: "%s\n" %getattr(note, '$t'),
+                                note_list))
+
+    def set_text(self, text):
+        '''
+        deletes all the old notes in a task and sets a single note with the
+        given text
+        '''
+        #delete old notes
+        notes = self.rtm_taskseries.notes
+        if notes:
+            note_list = self.__getattr_the_rtm_way(notes, 'note')
+            for note_id in [note.id for note in note_list]:
+                self.rtm.tasksNotes.delete(timeline = self.timeline,
+                                           note_id  = note_id)
+        if text == "":
+            return
+        self.rtm.tasksNotes.add(timeline      = self.timeline,
+                                list_id       = self.rtm_list.id,
+                                taskseries_id = self.rtm_taskseries.id,
+                                task_id       = self.rtm_task.id,
+                                note_title    = "",
+                                note_text     = text)
+
+    def get_due_date(self):
+        '''
+        Gets the task due date
+        '''
+        due = self.rtm_task.due
+        if due == "":
+            return NoDate()
+        date = self.__time_rtm_to_datetime(due).date()
+        if date:
+            return RealDate(date)
+        else:
+            return NoDate()
+
+    def set_due_date(self, due):
+        '''
+        Sets the task due date
+        '''
+        if due != None:
+            due_string = self.__time_date_to_rtm(due)
+            self.rtm.tasks.setDueDate(timeline      = self.timeline,
+                                      list_id       = self.rtm_list.id,
+                                      taskseries_id = self.rtm_taskseries.id,
+                                      task_id       = self.rtm_task.id,
+                                      parse = 1, \
+                                      due=due_string)
+        else:
+            self.rtm.tasks.setDueDate(timeline      = self.timeline,
+                                      list_id       = self.rtm_list.id,
+                                      taskseries_id = self.rtm_taskseries.id,
+                                      task_id       = self.rtm_task.id)
+
+    def get_modified(self):
+        '''
+        Gets the task modified time, in local time
+        '''
+        #RTM does not set a "modified" attribute in a new note because it uses a
+        # "added" attribute. We need to check for both.
+        if hasattr(self.rtm_task, 'modified'):
+            rtm_task_modified = self.__time_rtm_to_datetime(\
+                                                    self.rtm_task.modified)
+        else:
+            rtm_task_modified = self.__time_rtm_to_datetime(\
+                                                    self.rtm_task.added)
+        if hasattr(self.rtm_taskseries, 'modified'):
+            rtm_taskseries_modified = self.__time_rtm_to_datetime(\
+                                                self.rtm_taskseries.modified)
+        else:
+            rtm_taskseries_modified = self.__time_rtm_to_datetime(\
+                                                self.rtm_taskseries.added)
+        return max(rtm_task_modified, rtm_taskseries_modified)
+
+    def delete(self):
+        self.rtm.tasks.delete(timeline      = self.timeline,
+                              list_id       = self.rtm_list.id,
+                              taskseries_id = self.rtm_taskseries.id,
+                              task_id       = self.rtm_task.id)
+
+    #RTM speaks utc, and accepts utc if the "parse" option is set.
+    def __tz_utc_to_local(self, dt):
+        dt = dt.replace(tzinfo = tzutc())
+        dt = dt.astimezone(tzlocal())
+        return dt.replace(tzinfo = None)
+
+    def __tz_local_to_utc(self, dt):
+        dt = dt.replace(tzinfo = tzlocal())
+        dt = dt.astimezone(tzutc())
+        return dt.replace(tzinfo = None)
+
+    def __time_rtm_to_datetime(self, string):
+        string = string.split('.')[0].split('Z')[0]
+        dt = datetime.datetime.strptime(string.split(".")[0], \
+                                          "%Y-%m-%dT%H:%M:%S")
+        return self.__tz_utc_to_local(dt)
+        
+    def __time_rtm_to_date(self, string):
+        string = string.split('.')[0].split('Z')[0]
+        dt = datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
+        return self.__tz_utc_to_local(dt)
+
+    def __time_datetime_to_rtm(self, timeobject):
+        if timeobject == None:
+            return ""
+        timeobject = self.__tz_local_to_utc(timeobject)
+        return timeobject.strftime("%Y-%m-%dT%H:%M:%S")
+
+    def __time_date_to_rtm(self, timeobject):
+        if timeobject == None:
+            return ""
+        #WARNING: no timezone? seems to break the symmetry.
+        return timeobject.strftime("%Y-%m-%d")
+
+    def __str__(self):
+        return "Task %s (%s)" % (self.get_title(), self.get_id())

=== added directory 'GTG/backends/rtm'
=== added file 'GTG/backends/rtm/__init__.py'
=== added file 'GTG/backends/rtm/rtm.py'
--- GTG/backends/rtm/rtm.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/rtm/rtm.py	2010-08-25 05:35:52 +0000
@@ -0,0 +1,402 @@
+# Python library for Remember The Milk API
+
+__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
+__all__ = (
+    'API',
+    'createRTM',
+    'set_log_level',
+        )
+
+
+import warnings
+import urllib
+import time
+from hashlib import md5
+from GTG import _
+
+warnings.simplefilter('default', ImportWarning)
+
+_use_simplejson = False
+try:
+    import simplejson
+    _use_simplejson = True
+except ImportError:
+    try:
+        from django.utils import simplejson
+        _use_simplejson = True
+    except ImportError:
+        pass
+    
+if not _use_simplejson:
+    warnings.warn("simplejson module is not available, "
+             "falling back to the internal JSON parser. "
+             "Please consider installing the simplejson module from "
+             "http://pypi.python.org/pypi/simplejson.";, ImportWarning,
+             stacklevel=2)
+
+#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/'
+
+
+class RTMError(Exception): pass
+
+class RTMAPIError(RTMError): pass
+
+class AuthStateMachine(object):
+
+    class NoData(RTMError): pass
+
+    def __init__(self, states):
+        self.states = states
+        self.data = {}
+
+    def dataReceived(self, state, datum):
+        if state not in self.states:
+            error_string = _("Invalid state")+" <%s>" 
+
+            raise RTMError, error_string % state
+        self.data[state] = datum
+
+    def get(self, state):
+        if state in self.data:
+            return self.data[state]
+        else:
+            raise AuthStateMachine.NoData, 'No data for <%s>' % state
+
+
+class RTM(object):
+
+    def __init__(self, apiKey, secret, token=None):
+        self.apiKey = apiKey
+        self.secret = secret
+        self.authInfo = AuthStateMachine(['frob', 'token'])
+
+        # this enables one to do 'rtm.tasks.getList()', for example
+        for prefix, methods in API.items():
+            setattr(self, prefix,
+                    RTMAPICategory(self, prefix, methods))
+
+        if token:
+            self.authInfo.dataReceived('token', token)
+
+    def _sign(self, params):
+        "Sign the parameters with MD5 hash"
+        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
+        return md5(self.secret+pairs).hexdigest()
+
+    def get(self, **params):
+        "Get the XML response for the passed `params`."
+        params['api_key'] = self.apiKey
+        params['format'] = 'json'
+        params['api_sig'] = self._sign(params)
+
+        json = openURL(SERVICE_URL, params).read()
+
+        #LOG.debug("JSON response: \n%s" % json)
+        if _use_simplejson:
+            data = dottedDict('ROOT', simplejson.loads(json))
+        else:
+            data = dottedJSON(json)
+        rsp = data.rsp
+
+        if rsp.stat == 'fail':
+            raise RTMAPIError, 'API call failed - %s (%s)' % (
+                rsp.err.msg, rsp.err.code)
+        else:
+            return rsp
+
+    def getNewFrob(self):
+        rsp = self.get(method='rtm.auth.getFrob')
+        self.authInfo.dataReceived('frob', rsp.frob)
+        return rsp.frob
+
+    def getAuthURL(self):
+        try:
+            frob = self.authInfo.get('frob')
+        except AuthStateMachine.NoData:
+            frob = self.getNewFrob()
+
+        params = {
+            'api_key': self.apiKey,
+            'perms'  : 'delete',
+            'frob'   : frob
+            }
+        params['api_sig'] = self._sign(params)
+        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
+
+    def getToken(self):
+        frob = self.authInfo.get('frob')
+        rsp = self.get(method='rtm.auth.getToken', frob=frob)
+        self.authInfo.dataReceived('token', rsp.auth.token)
+        return rsp.auth.token
+
+class RTMAPICategory:
+    "See the `API` structure and `RTM.__init__`"
+
+    def __init__(self, rtm, prefix, methods):
+        self.rtm = rtm
+        self.prefix = prefix
+        self.methods = methods
+
+    def __getattr__(self, attr):
+        if attr in self.methods:
+            rargs, oargs = self.methods[attr]
+            if self.prefix == 'tasksNotes':
+                aname = 'rtm.tasks.notes.%s' % attr
+            else:
+                aname = 'rtm.%s.%s' % (self.prefix, attr)
+            return lambda **params: self.callMethod(
+                aname, rargs, oargs, **params)
+        else:
+            raise AttributeError, 'No such attribute: %s' % attr
+
+    def callMethod(self, aname, rargs, oargs, **params):
+        # Sanity checks
+        for requiredArg in rargs:
+            if requiredArg not in params:
+                raise TypeError, 'Required parameter (%s) missing' % requiredArg
+
+        for param in params:
+            if param not in rargs + oargs:
+                warnings.warn('Invalid parameter (%s)' % param)
+
+        return self.rtm.get(method=aname,
+                            auth_token=self.rtm.authInfo.get('token'),
+                            **params)
+
+
+
+# Utility functions
+
+def sortedItems(dictionary):
+    "Return a list of (key, value) sorted based on keys"
+    keys = dictionary.keys()
+    keys.sort()
+    for key in keys:
+        yield key, dictionary[key]
+
+def openURL(url, queryArgs=None):
+    if queryArgs:
+        url = url + '?' + urllib.urlencode(queryArgs)
+    #LOG.debug("URL> %s", url)
+    return urllib.urlopen(url)
+
+class dottedDict(object):
+    """Make dictionary items accessible via the object-dot notation."""
+
+    def __init__(self, name, dictionary):
+        self._name = name
+
+        if type(dictionary) is dict:
+            for key, value in dictionary.items():
+                if type(value) is dict:
+                    value = dottedDict(key, value)
+                elif type(value) in (list, tuple) and key != 'tag':
+                    value = [dottedDict('%s_%d' % (key, i), item)
+                             for i, item in indexed(value)]
+                setattr(self, key, value)
+        else:
+            raise ValueError, 'not a dict: %s' % dictionary
+
+    def __repr__(self):
+        children = [c for c in dir(self) if not c.startswith('_')]
+        return 'dotted <%s> : %s' % (
+            self._name,
+            ', '.join(children))
+
+
+def safeEval(string):
+    return eval(string, {}, {})
+
+def dottedJSON(json):
+    return dottedDict('ROOT', safeEval(json))
+
+def indexed(seq):
+    index = 0
+    for item in seq:
+        yield index, item
+        index += 1
+
+
+# API spec
+
+API = {
+   'auth': {
+       'checkToken':
+           [('auth_token',), ()],
+       'getFrob':
+           [(), ()],
+       'getToken':
+           [('frob',), ()]
+       },
+    'contacts': {
+        'add':
+            [('timeline', 'contact'), ()],
+        'delete':
+            [('timeline', 'contact_id'), ()],
+        'getList':
+            [(), ()]
+        },
+    'groups': {
+        'add':
+            [('timeline', 'group'), ()],
+        'addContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        'delete':
+            [('timeline', 'group_id'), ()],
+        'getList':
+            [(), ()],
+        'removeContact':
+            [('timeline', 'group_id', 'contact_id'), ()],
+        },
+    'lists': {
+        'add':
+            [('timeline', 'name',), ('filter',)],
+        'archive':
+            [('timeline', 'list_id'), ()],
+        'delete':
+            [('timeline', 'list_id'), ()],
+        'getList':
+            [(), ()],
+        'setDefaultList':
+            [('timeline'), ('list_id')],
+        'setName':
+            [('timeline', 'list_id', 'name'), ()],
+        'unarchive':
+            [('timeline',), ('list_id',)]
+        },
+    'locations': {
+        'getList':
+            [(), ()]
+        },
+    'reflection': {
+        'getMethodInfo':
+            [('methodName',), ()],
+        'getMethods':
+            [(), ()]
+        },
+    'settings': {
+        'getList':
+            [(), ()]
+        },
+    'tasks': {
+        'add':
+            [('timeline', 'name',), ('list_id', 'parse',)],
+        'addTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'complete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
+        'delete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
+        'getList':
+            [(),
+             ('list_id', 'filter', 'last_sync')],
+        'movePriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
+             ()],
+        'moveTo':
+            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'postpone':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        'removeTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
+             ()],
+        'setDueDate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('due', 'has_due_time', 'parse')],
+        'setEstimate':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('estimate',)],
+        'setLocation':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('location_id',)],
+        'setName':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
+             ()],
+        'setPriority':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('priority',)],
+        'setRecurrence':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('repeat',)],
+        'setTags':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('tags',)],
+        'setURL':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ('url',)],
+        'uncomplete':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
+             ()],
+        },
+    'tasksNotes': {
+        'add':
+            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
+        'delete':
+            [('timeline', 'note_id'), ()],
+        'edit':
+            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
+        },
+    'test': {
+        'echo':
+            [(), ()],
+        'login':
+            [(), ()]
+        },
+    'time': {
+        'convert':
+            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
+        'parse':
+            [('text',), ('timezone', 'dateformat')]
+        },
+    'timelines': {
+        'create':
+            [(), ()]
+        },
+    'timezones': {
+        'getList':
+            [(), ()]
+        },
+    'transactions': {
+        'undo':
+            [('timeline', 'transaction_id'), ()]
+        },
+    }
+
+def createRTM(apiKey, secret, token=None):
+    rtm = RTM(apiKey, secret, token)
+#    if token is None:
+#        print 'No token found'
+#        print 'Give me access here:', rtm.getAuthURL()
+#        raw_input('Press enter once you gave access')
+#        print 'Note down this token for future use:', rtm.getToken()
+
+    return rtm
+
+def test(apiKey, secret, token=None):
+    rtm = createRTM(apiKey, secret, token)
+
+    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
+    print [t.name for t in rspTasks.tasks.list.taskseries]
+    print rspTasks.tasks.list.id
+
+    rspLists = rtm.lists.getList()
+    # print rspLists.lists.list
+    print [(x.name, x.id) for x in rspLists.lists.list]
+
+def set_log_level(level):
+    '''Sets the log level of the logger used by the module.
+    
+    >>> import rtm
+    >>> import logging
+    >>> rtm.set_log_level(logging.INFO)
+    '''
+    
+    #LOG.setLevel(level)

=== removed file 'GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg'
--- GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg	2009-09-11 20:47:31 +0000
+++ GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg	1970-01-01 00:00:00 +0000
@@ -1,206 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/";
-   xmlns:cc="http://creativecommons.org/ns#";
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
-   xmlns:svg="http://www.w3.org/2000/svg";
-   xmlns="http://www.w3.org/2000/svg";
-   xmlns:xlink="http://www.w3.org/1999/xlink";
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
-   width="64px"
-   height="64px"
-   id="svg3727"
-   sodipodi:version="0.32"
-   inkscape:version="0.46"
-   sodipodi:docname="rtm_image.svg"
-   inkscape:output_extension="org.inkscape.output.svg.inkscape"
-   inkscape:export-filename="/home/luca/gtg/rtm-sync-plugin/GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png"
-   inkscape:export-xdpi="22.5"
-   inkscape:export-ydpi="22.5">
-  <defs
-     id="defs3729">
-    <filter
-       inkscape:collect="always"
-       id="filter3502"
-       x="-0.044082869"
-       width="1.0881657"
-       y="-0.19633912"
-       height="1.3926782">
-      <feGaussianBlur
-         inkscape:collect="always"
-         stdDeviation="1.4852688"
-         id="feGaussianBlur3504" />
-    </filter>
-    <linearGradient
-       id="linearGradient3661">
-      <stop
-         style="stop-color:#3399ff;stop-opacity:1;"
-         offset="0"
-         id="stop3663" />
-      <stop
-         id="stop3675"
-         offset="0.5"
-         style="stop-color:#3399ff;stop-opacity:1;" />
-      <stop
-         style="stop-color:#3399ff;stop-opacity:0.68627451;"
-         offset="0.75"
-         id="stop3685" />
-      <stop
-         id="stop3687"
-         offset="0.875"
-         style="stop-color:#3399ff;stop-opacity:0.52941176;" />
-      <stop
-         style="stop-color:#3399ff;stop-opacity:0.37719297;"
-         offset="1"
-         id="stop3665" />
-    </linearGradient>
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3661"
-       id="linearGradient3725"
-       gradientUnits="userSpaceOnUse"
-       x1="129.75728"
-       y1="658.44305"
-       x2="232.12813"
-       y2="657.89764" />
-    <clipPath
-       clipPathUnits="userSpaceOnUse"
-       id="clipPath3545">
-      <rect
-         style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
-         id="rect3547"
-         width="188.70778"
-         height="271.60831"
-         x="83.991325"
-         y="571.32098" />
-    </clipPath>
-    <linearGradient
-       id="linearGradient3677">
-      <stop
-         style="stop-color:#ececec;stop-opacity:1"
-         offset="0"
-         id="stop3679" />
-      <stop
-         id="stop3689"
-         offset="0.5"
-         style="stop-color:#ffffff;stop-opacity:0.49803922;" />
-      <stop
-         style="stop-color:#ececec;stop-opacity:1"
-         offset="1"
-         id="stop3681" />
-    </linearGradient>
-    <linearGradient
-       inkscape:collect="always"
-       xlink:href="#linearGradient3677"
-       id="linearGradient3723"
-       gradientUnits="userSpaceOnUse"
-       x1="115.46449"
-       y1="774.37683"
-       x2="235.97925"
-       y2="775.91943" />
-    <inkscape:perspective
-       sodipodi:type="inkscape:persp3d"
-       inkscape:vp_x="0 : 32 : 1"
-       inkscape:vp_y="0 : 1000 : 0"
-       inkscape:vp_z="64 : 32 : 1"
-       inkscape:persp3d-origin="32 : 21.333333 : 1"
-       id="perspective3735" />
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="1.9445436"
-     inkscape:cx="21.790111"
-     inkscape:cy="76.61186"
-     inkscape:current-layer="layer1"
-     showgrid="true"
-     inkscape:document-units="px"
-     inkscape:grid-bbox="true"
-     inkscape:window-width="640"
-     inkscape:window-height="628"
-     inkscape:window-x="506"
-     inkscape:window-y="22" />
-  <metadata
-     id="metadata3732">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     id="layer1"
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer">
-    <g
-       id="g3711"
-       transform="matrix(0.1214085,0.1214085,-0.1214085,0.1214085,61.002827,-86.966337)"
-       inkscape:transform-center-x="-16.513266"
-       inkscape:transform-center-y="-36.581756">
-      <path
-         id="path3396"
-         d="M 414.51135,403.70408 C 414.51135,414.20049 395.48743,422.71931 372.04725,422.71931 C 348.60706,422.71931 329.58314,414.20049 329.58314,403.70408 C 329.58314,393.20767 348.60706,384.68885 372.04725,384.68885 C 395.48743,384.68885 414.51135,393.20767 414.51135,403.70408 z"
-         inkscape:transform-center-y="41.974325"
-         inkscape:transform-center-x="-9.0994502"
-         style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         id="path3400"
-         d="M 414.51135,393.70408 C 414.51135,404.20049 395.48743,412.71931 372.04725,412.71931 C 348.60706,412.71931 329.58314,404.20049 329.58314,393.70408 C 329.58314,383.20767 348.60706,374.68885 372.04725,374.68885 C 395.48743,374.68885 414.51135,383.20767 414.51135,393.70408 z"
-         inkscape:transform-center-y="41.974325"
-         inkscape:transform-center-x="-9.0994502"
-         style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         id="path3402"
-         d="M 414.51135,403.70408 C 414.51135,414.20049 395.48743,422.71931 372.04725,422.71931 C 348.60706,422.71931 329.58314,414.20049 329.58314,403.70408 C 329.58314,393.20767 348.60706,384.68885 372.04725,384.68885 C 395.48743,384.68885 414.51135,393.20767 414.51135,403.70408 z"
-         inkscape:transform-center-y="41.974325"
-         inkscape:transform-center-x="-9.0994502"
-         style="opacity:0.91085271;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         transform="matrix(0.9856192,0,0,1,199.34766,-2.7713095)"
-         clip-path="url(#clipPath3545)"
-         sodipodi:nodetypes="cccccccccccccccccccccc"
-         id="path3534"
-         d="M 138.90625,409.375 L 139,410.375 L 139.09375,411.34375 C 140.75679,432.38408 139.68245,453.56341 139.21875,474.59375 C 130.4306,497.46685 103.76148,508.08521 95.34375,531.25 C 94.719742,559.63205 95.8781,588.24637 95.84375,616.71875 C 96.525929,671.97803 96.53609,727.46035 97.21875,782.59375 C 100.28884,803.27664 119.73439,816.6627 138.25,823.09375 C 170.29613,833.76601 208.57233,831.13483 236.25,810.71875 C 247.88637,802.02197 256.05515,787.89092 254.28125,773.0625 C 254.46462,692.74557 255.55912,612.36257 256.0625,532.09375 C 249.30834,511.89397 228.57494,501.13943 217,484.1875 C 206.62003,465.60923 211.70898,431.09823 211.625,409.375 C 187.38542,409.37499 163.14583,409.375 138.90625,409.375 z M 173.96875,528.46875 C 180.68858,528.33398 187.4842,528.79077 194.09375,529.625 C 215.45628,532.8583 239.25025,540.68239 251.28125,559.875 C 259.12082,572.0293 254.62503,588.43585 244.15625,597.4375 C 224.14426,615.68821 195.2559,620.79378 168.875,619.71875 C 144.89679,618.14655 118.37989,611.34257 102.8125,591.625 C 93.866467,580.60806 95.372797,563.83495 105.125,553.8125 C 122.5306,535.22564 149.33071,529.07853 173.96875,528.46875 z"
-         style="fill:url(#linearGradient3723);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         id="path3508"
-         d="M 369.94777,528.00618 C 345.30973,528.61593 318.50962,534.76307 301.10402,553.34993 C 291.35182,563.37235 289.84549,580.14549 298.79152,591.16243 C 314.35891,610.87997 340.87468,617.66905 364.85289,619.24125 C 391.23379,620.31629 420.12328,615.22564 440.13527,596.97493 C 450.60405,587.97325 455.09984,571.56673 447.26027,559.41243 C 435.22927,540.21979 411.4353,532.39573 390.07277,529.16243 C 383.46322,528.32817 376.6676,527.87141 369.94777,528.00618 z"
-         style="opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         style="fill:url(#linearGradient3725);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1"
-         d="M 138.90625,409.375 L 139,410.375 L 139.09375,411.34375 C 140.75679,432.38408 139.68245,453.56341 139.21875,474.59375 C 130.4306,497.46685 103.76148,508.08521 95.34375,531.25 C 94.719742,559.63205 95.8781,588.24637 95.84375,616.71875 C 96.525929,671.97803 96.53609,631.46035 97.21875,686.59375 C 100.28884,707.27664 119.73439,720.6627 138.25,727.09375 C 170.29613,737.76601 208.57233,735.13483 236.25,714.71875 C 247.88637,706.02197 256.05515,691.89092 254.28125,677.0625 C 254.46462,596.74557 255.55912,612.36257 256.0625,532.09375 C 249.30834,511.89397 228.57494,501.13943 217,484.1875 C 206.62003,465.60923 211.70898,431.09823 211.625,409.375 C 187.38542,409.37499 163.14583,409.375 138.90625,409.375 z M 173.96875,528.46875 C 180.68858,528.33398 187.4842,528.79077 194.09375,529.625 C 215.45628,532.8583 239.25025,540.68239 251.28125,559.875 C 259.12082,572.0293 254.62503,588.43585 244.15625,597.4375 C 224.14426,615.68821 195.2559,620.79378 168.875,619.71875 C 144.89679,618.14655 118.37989,611.34257 102.8125,591.625 C 93.866467,580.60806 95.372797,563.83495 105.125,553.8125 C 122.5306,535.22564 149.33071,529.07853 173.96875,528.46875 z"
-         id="path3659"
-         sodipodi:nodetypes="cccccccccccccccccccccc"
-         clip-path="url(#clipPath3545)"
-         transform="matrix(0.9856192,0,0,1,199.34766,37.228691)" />
-      <path
-         id="path3563"
-         d="M 333.14099,404.25022 C 335.49334,426.41602 333.99301,448.60995 333.79724,470.81275 C 333.33145,472.18625 332.79969,473.46195 332.20349,474.62525 C 322.27441,493.99985 294.30805,508.61965 289.39099,529.43775 C 291.10411,668.49535 291.42224,773.93775 291.42224,773.93775 C 291.40471,774.39025 291.42224,774.85755 291.42224,775.31275 C 291.42226,775.76805 291.40469,776.23525 291.42224,776.68775 L 291.42224,780.46905 L 291.79724,780.46905 C 295.756,807.17195 330.16049,828.06275 372.01599,828.09405 C 372.04741,828.09405 372.07833,828.09405 372.10974,828.09405 C 413.96526,828.06275 448.36974,807.17185 452.32849,780.46905 L 452.70349,780.46905 L 452.70349,776.68775 C 452.72106,776.23515 452.70348,775.76805 452.70349,775.31275 C 452.70351,774.85755 452.72103,774.39025 452.70349,773.93775 C 452.70351,773.93775 452.99041,668.49525 454.70349,529.43775 C 449.78645,508.61975 421.82008,493.99985 411.89099,474.62525 C 411.30699,473.48575 410.78728,472.21675 410.32849,470.87525 L 410.04724,404.25022 L 333.14099,404.25022 z M 337.54724,408.25022 C 360.39099,408.25022 383.23474,408.25022 406.07849,408.25022 C 406.12735,420.43772 406.18585,432.62522 406.23474,444.81275 C 407.10693,454.22905 404.81823,463.84295 406.98474,473.06275 C 415.68479,495.66375 441.89079,505.80335 450.29724,528.71905 C 450.30128,608.04355 448.768,687.54235 448.70349,766.93775 C 450.40115,779.71305 445.88556,792.90285 436.45349,801.71905 C 411.40508,824.19635 374.0633,828.57625 342.29724,820.15645 C 322.49257,814.98065 300.65448,802.54255 295.95349,780.96905 C 294.88669,751.98605 295.34734,722.85275 294.92224,693.81275 C 294.94021,638.71625 293.4657,583.62415 293.79724,528.53145 C 302.5795,505.57595 328.81514,495.25805 337.51599,472.21905 C 338.26259,451.56455 339.063,430.72372 337.67224,410.03142 L 337.60974,409.25022 L 337.54724,408.25022 z"
-         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         id="path3417"
-         d="M 414.47141,393.86713 C 414.16923,404.24732 395.25459,412.61713 372.00266,412.61713 C 349.29499,412.61714 330.73443,404.62835 329.59641,394.58588 L 329.56516,403.61713 C 329.50442,414.11336 348.56248,422.61714 372.00266,422.61713 C 395.44284,422.61713 414.47141,414.11354 414.47141,403.61713 C 414.10399,400.99347 414.71184,396.16626 414.47141,393.86713 z"
-         style="opacity:1;fill:#0060be;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         id="path3404"
-         d="M 414.51135,393.70408 C 414.51135,404.20049 395.48743,412.71931 372.04725,412.71931 C 348.60706,412.71931 329.58314,404.20049 329.58314,393.70408 C 329.58314,383.20767 348.60706,374.68885 372.04725,374.68885 C 395.48743,374.68885 414.51135,383.20767 414.51135,393.70408 z"
-         inkscape:transform-center-y="41.974325"
-         inkscape:transform-center-x="-9.0994502"
-         style="opacity:0.91085271;fill:#0060be;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
-      <path
-         sodipodi:nodetypes="cccccccc"
-         id="path3434"
-         d="M 331.61606,403.625 C 332.77186,412.45706 343.31251,415.58934 350.58481,417.96875 C 369.00699,422.23097 390.1947,422.18632 406.67856,412.1875 C 409.76447,410.08284 413.03827,406.75523 412.39731,402.65625 C 399.53954,413.82702 381.02227,415.1994 364.61901,414.32955 C 352.97865,413.20127 340.32229,410.83165 331.61606,402.5 L 331.61606,403.5 L 331.61606,403.625 z"
-         style="opacity:1;fill:#004185;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:9.60000038;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter3502)" />
-    </g>
-  </g>
-</svg>

=== removed directory 'GTG/plugins/rtm_sync/pyrtm'
=== removed file 'GTG/plugins/rtm_sync/pyrtm/README'
--- GTG/plugins/rtm_sync/pyrtm/README	2009-08-07 04:13:36 +0000
+++ GTG/plugins/rtm_sync/pyrtm/README	1970-01-01 00:00:00 +0000
@@ -1,10 +0,0 @@
-======================================================================
-Python library for Remember The Milk API
-======================================================================
-
-Copyright (c) 2008 by Sridhar Ratnakumar <http://nearfar.org/>
-
-Contributors:
- - Mariano Draghi (cHagHi) <mariano at chaghi dot com dot ar>
-
-See app.py for examples

=== removed file 'GTG/plugins/rtm_sync/pyrtm/__init__.py'
=== removed file 'GTG/plugins/rtm_sync/pyrtm/rtm.py'
--- GTG/plugins/rtm_sync/pyrtm/rtm.py	2010-03-17 03:55:32 +0000
+++ GTG/plugins/rtm_sync/pyrtm/rtm.py	1970-01-01 00:00:00 +0000
@@ -1,402 +0,0 @@
-# Python library for Remember The Milk API
-
-__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
-__all__ = (
-    'API',
-    'createRTM',
-    'set_log_level',
-        )
-
-
-import warnings
-import urllib
-import time
-from hashlib import md5
-from GTG import _
-
-warnings.simplefilter('default', ImportWarning)
-
-_use_simplejson = False
-try:
-    import simplejson
-    _use_simplejson = True
-except ImportError:
-    try:
-        from django.utils import simplejson
-        _use_simplejson = True
-    except ImportError:
-        pass
-    
-if not _use_simplejson:
-    warnings.warn("simplejson module is not available, "
-             "falling back to the internal JSON parser. "
-             "Please consider installing the simplejson module from "
-             "http://pypi.python.org/pypi/simplejson.";, ImportWarning,
-             stacklevel=2)
-
-#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/'
-
-
-class RTMError(Exception): pass
-
-class RTMAPIError(RTMError): pass
-
-class AuthStateMachine(object):
-
-    class NoData(RTMError): pass
-
-    def __init__(self, states):
-        self.states = states
-        self.data = {}
-
-    def dataReceived(self, state, datum):
-        if state not in self.states:
-            error_string = _("Invalid state")+" <%s>" 
-
-            raise RTMError, error_string % state
-        self.data[state] = datum
-
-    def get(self, state):
-        if state in self.data:
-            return self.data[state]
-        else:
-            raise AuthStateMachine.NoData, 'No data for <%s>' % state
-
-
-class RTM(object):
-
-    def __init__(self, apiKey, secret, token=None):
-        self.apiKey = apiKey
-        self.secret = secret
-        self.authInfo = AuthStateMachine(['frob', 'token'])
-
-        # this enables one to do 'rtm.tasks.getList()', for example
-        for prefix, methods in API.items():
-            setattr(self, prefix,
-                    RTMAPICategory(self, prefix, methods))
-
-        if token:
-            self.authInfo.dataReceived('token', token)
-
-    def _sign(self, params):
-        "Sign the parameters with MD5 hash"
-        pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
-        return md5(self.secret+pairs).hexdigest()
-
-    def get(self, **params):
-        "Get the XML response for the passed `params`."
-        params['api_key'] = self.apiKey
-        params['format'] = 'json'
-        params['api_sig'] = self._sign(params)
-
-        json = openURL(SERVICE_URL, params).read()
-
-        #LOG.debug("JSON response: \n%s" % json)
-        if _use_simplejson:
-            data = dottedDict('ROOT', simplejson.loads(json))
-        else:
-            data = dottedJSON(json)
-        rsp = data.rsp
-
-        if rsp.stat == 'fail':
-            raise RTMAPIError, 'API call failed - %s (%s)' % (
-                rsp.err.msg, rsp.err.code)
-        else:
-            return rsp
-
-    def getNewFrob(self):
-        rsp = self.get(method='rtm.auth.getFrob')
-        self.authInfo.dataReceived('frob', rsp.frob)
-        return rsp.frob
-
-    def getAuthURL(self):
-        try:
-            frob = self.authInfo.get('frob')
-        except AuthStateMachine.NoData:
-            frob = self.getNewFrob()
-
-        params = {
-            'api_key': self.apiKey,
-            'perms'  : 'delete',
-            'frob'   : frob
-            }
-        params['api_sig'] = self._sign(params)
-        return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
-
-    def getToken(self):
-        frob = self.authInfo.get('frob')
-        rsp = self.get(method='rtm.auth.getToken', frob=frob)
-        self.authInfo.dataReceived('token', rsp.auth.token)
-        return rsp.auth.token
-
-class RTMAPICategory:
-    "See the `API` structure and `RTM.__init__`"
-
-    def __init__(self, rtm, prefix, methods):
-        self.rtm = rtm
-        self.prefix = prefix
-        self.methods = methods
-
-    def __getattr__(self, attr):
-        if attr in self.methods:
-            rargs, oargs = self.methods[attr]
-            if self.prefix == 'tasksNotes':
-                aname = 'rtm.tasks.notes.%s' % attr
-            else:
-                aname = 'rtm.%s.%s' % (self.prefix, attr)
-            return lambda **params: self.callMethod(
-                aname, rargs, oargs, **params)
-        else:
-            raise AttributeError, 'No such attribute: %s' % attr
-
-    def callMethod(self, aname, rargs, oargs, **params):
-        # Sanity checks
-        for requiredArg in rargs:
-            if requiredArg not in params:
-                raise TypeError, 'Required parameter (%s) missing' % requiredArg
-
-        for param in params:
-            if param not in rargs + oargs:
-                warnings.warn('Invalid parameter (%s)' % param)
-
-        return self.rtm.get(method=aname,
-                            auth_token=self.rtm.authInfo.get('token'),
-                            **params)
-
-
-
-# Utility functions
-
-def sortedItems(dictionary):
-    "Return a list of (key, value) sorted based on keys"
-    keys = dictionary.keys()
-    keys.sort()
-    for key in keys:
-        yield key, dictionary[key]
-
-def openURL(url, queryArgs=None):
-    if queryArgs:
-        url = url + '?' + urllib.urlencode(queryArgs)
-    #LOG.debug("URL> %s", url)
-    return urllib.urlopen(url)
-
-class dottedDict(object):
-    """Make dictionary items accessible via the object-dot notation."""
-
-    def __init__(self, name, dictionary):
-        self._name = name
-
-        if type(dictionary) is dict:
-            for key, value in dictionary.items():
-                if type(value) is dict:
-                    value = dottedDict(key, value)
-                elif type(value) in (list, tuple) and key != 'tag':
-                    value = [dottedDict('%s_%d' % (key, i), item)
-                             for i, item in indexed(value)]
-                setattr(self, key, value)
-        else:
-            raise ValueError, 'not a dict: %s' % dictionary
-
-    def __repr__(self):
-        children = [c for c in dir(self) if not c.startswith('_')]
-        return 'dotted <%s> : %s' % (
-            self._name,
-            ', '.join(children))
-
-
-def safeEval(string):
-    return eval(string, {}, {})
-
-def dottedJSON(json):
-    return dottedDict('ROOT', safeEval(json))
-
-def indexed(seq):
-    index = 0
-    for item in seq:
-        yield index, item
-        index += 1
-
-
-# API spec
-
-API = {
-   'auth': {
-       'checkToken':
-           [('auth_token',), ()],
-       'getFrob':
-           [(), ()],
-       'getToken':
-           [('frob',), ()]
-       },
-    'contacts': {
-        'add':
-            [('timeline', 'contact'), ()],
-        'delete':
-            [('timeline', 'contact_id'), ()],
-        'getList':
-            [(), ()]
-        },
-    'groups': {
-        'add':
-            [('timeline', 'group'), ()],
-        'addContact':
-            [('timeline', 'group_id', 'contact_id'), ()],
-        'delete':
-            [('timeline', 'group_id'), ()],
-        'getList':
-            [(), ()],
-        'removeContact':
-            [('timeline', 'group_id', 'contact_id'), ()],
-        },
-    'lists': {
-        'add':
-            [('timeline', 'name',), ('filter',)],
-        'archive':
-            [('timeline', 'list_id'), ()],
-        'delete':
-            [('timeline', 'list_id'), ()],
-        'getList':
-            [(), ()],
-        'setDefaultList':
-            [('timeline'), ('list_id')],
-        'setName':
-            [('timeline', 'list_id', 'name'), ()],
-        'unarchive':
-            [('timeline',), ('list_id',)]
-        },
-    'locations': {
-        'getList':
-            [(), ()]
-        },
-    'reflection': {
-        'getMethodInfo':
-            [('methodName',), ()],
-        'getMethods':
-            [(), ()]
-        },
-    'settings': {
-        'getList':
-            [(), ()]
-        },
-    'tasks': {
-        'add':
-            [('timeline', 'name',), ('list_id', 'parse',)],
-        'addTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
-             ()],
-        'complete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
-        'delete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
-        'getList':
-            [(),
-             ('list_id', 'filter', 'last_sync')],
-        'movePriority':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
-             ()],
-        'moveTo':
-            [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
-             ()],
-        'postpone':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ()],
-        'removeTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
-             ()],
-        'setDueDate':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('due', 'has_due_time', 'parse')],
-        'setEstimate':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('estimate',)],
-        'setLocation':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('location_id',)],
-        'setName':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
-             ()],
-        'setPriority':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('priority',)],
-        'setRecurrence':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('repeat',)],
-        'setTags':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('tags',)],
-        'setURL':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ('url',)],
-        'uncomplete':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id'),
-             ()],
-        },
-    'tasksNotes': {
-        'add':
-            [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
-        'delete':
-            [('timeline', 'note_id'), ()],
-        'edit':
-            [('timeline', 'note_id', 'note_title', 'note_text'), ()]
-        },
-    'test': {
-        'echo':
-            [(), ()],
-        'login':
-            [(), ()]
-        },
-    'time': {
-        'convert':
-            [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
-        'parse':
-            [('text',), ('timezone', 'dateformat')]
-        },
-    'timelines': {
-        'create':
-            [(), ()]
-        },
-    'timezones': {
-        'getList':
-            [(), ()]
-        },
-    'transactions': {
-        'undo':
-            [('timeline', 'transaction_id'), ()]
-        },
-    }
-
-def createRTM(apiKey, secret, token=None):
-    rtm = RTM(apiKey, secret, token)
-#    if token is None:
-#        print 'No token found'
-#        print 'Give me access here:', rtm.getAuthURL()
-#        raw_input('Press enter once you gave access')
-#        print 'Note down this token for future use:', rtm.getToken()
-
-    return rtm
-
-def test(apiKey, secret, token=None):
-    rtm = createRTM(apiKey, secret, token)
-
-    rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
-    print [t.name for t in rspTasks.tasks.list.taskseries]
-    print rspTasks.tasks.list.id
-
-    rspLists = rtm.lists.getList()
-    # print rspLists.lists.list
-    print [(x.name, x.id) for x in rspLists.lists.list]
-
-def set_log_level(level):
-    '''Sets the log level of the logger used by the module.
-    
-    >>> import rtm
-    >>> import logging
-    >>> rtm.set_log_level(logging.INFO)
-    '''
-    
-    #LOG.setLevel(level)

=== added file 'data/icons/hicolor/scalable/apps/backend_rtm.png'
Binary files data/icons/hicolor/scalable/apps/backend_rtm.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_rtm.png	2010-08-25 05:35:52 +0000 differ

Follow ups