gtg-user team mailing list archive
-
gtg-user team
-
Mailing list archive
-
Message #00307
[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