GTG

Merge lp:~gtg-user/gtg/rtm-backend into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
Status: Merged
Merged at revision: 880
Proposed branch: lp:~gtg-user/gtg/rtm-backend
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 2077 lines (+1428/-618)
5 files modified
GTG/backends/backend_rtm.py (+1026/-0)
GTG/backends/rtm/rtm.py (+402/-0)
GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg (+0/-206)
GTG/plugins/rtm_sync/pyrtm/README (+0/-10)
GTG/plugins/rtm_sync/pyrtm/rtm.py (+0/-402)
To merge this branch: bzr merge lp:~gtg-user/gtg/rtm-backend
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+33610@code.launchpad.net

Description of the change

Remember the milk backend. Tested, documented and ready to go.

To post a comment you must log in.
lp:~gtg-user/gtg/rtm-backend updated
878. By Luca Invernizzi

found one bug when dealing with long tasks (fixed).
RTM backend now has undo support if something goes wrong (and it works!)!

879. By Luca Invernizzi

better rtm description

880. By Luca Invernizzi

merge w/ trunk

881. By Luca Invernizzi

cherrypicking from my development branch

882. By Luca Invernizzi

cherrypicking from my development branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_rtm.py'
2--- GTG/backends/backend_rtm.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_rtm.py 2010-09-03 23:47:39 +0000
4@@ -0,0 +1,1026 @@
5+# -*- coding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things Gnome! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
9+#
10+# This program is free software: you can redistribute it and/or modify it under
11+# the terms of the GNU General Public License as published by the Free Software
12+# Foundation, either version 3 of the License, or (at your option) any later
13+# version.
14+#
15+# This program is distributed in the hope that it will be useful, but WITHOUT
16+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18+# details.
19+#
20+# You should have received a copy of the GNU General Public License along with
21+# this program. If not, see <http://www.gnu.org/licenses/>.
22+# -----------------------------------------------------------------------------
23+
24+'''
25+Remember the milk backend
26+'''
27+
28+import os
29+import cgi
30+import uuid
31+import time
32+import threading
33+import datetime
34+import subprocess
35+import exceptions
36+from dateutil.tz import tzutc, tzlocal
37+
38+from GTG.backends.genericbackend import GenericBackend
39+from GTG import _
40+from GTG.backends.backendsignals import BackendSignals
41+from GTG.backends.syncengine import SyncEngine, SyncMeme
42+from GTG.backends.rtm.rtm import createRTM, RTMError, RTMAPIError
43+from GTG.backends.periodicimportbackend import PeriodicImportBackend
44+from GTG.tools.dates import RealDate, NoDate
45+from GTG.core.task import Task
46+from GTG.tools.interruptible import interruptible
47+from GTG.tools.logger import Log
48+
49+
50+
51+
52+
53+class Backend(PeriodicImportBackend):
54+
55+
56+ _general_description = { \
57+ GenericBackend.BACKEND_NAME: "backend_rtm", \
58+ GenericBackend.BACKEND_HUMAN_NAME: _("Remember The Milk"), \
59+ GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
60+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
61+ GenericBackend.BACKEND_DESCRIPTION: \
62+ _("This backend synchronizes your tasks with the web service"
63+ " RememberTheMilk:\n\t\thttp://rememberthemilk.com\n\n"
64+ "Note: This product uses the Remember The Milk API but is not"
65+ " endorsed or certified by Remember The Milk"),\
66+ }
67+
68+ _static_parameters = { \
69+ "period": { \
70+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
71+ GenericBackend.PARAM_DEFAULT_VALUE: 10, },
72+ "is-first-run": { \
73+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
74+ GenericBackend.PARAM_DEFAULT_VALUE: True, },
75+ }
76+
77+###############################################################################
78+### Backend standard methods ##################################################
79+###############################################################################
80+
81+ def __init__(self, parameters):
82+ '''
83+ See GenericBackend for an explanation of this function.
84+ Loads the saved state of the sync, if any
85+ '''
86+ super(Backend, self).__init__(parameters)
87+ #loading the saved state of the synchronization, if any
88+ self.sync_engine_path = os.path.join('backends/rtm/', \
89+ "sync_engine-" + self.get_id())
90+ self.sync_engine = self._load_pickled_file(self.sync_engine_path, \
91+ SyncEngine())
92+ #reloading the oauth authentication token, if any
93+ self.token_path = os.path.join('backends/rtm/', \
94+ "auth_token-" + self.get_id())
95+ self.token = self._load_pickled_file(self.token_path, None)
96+ self.enqueued_start_get_task = False
97+ self.login_event = threading.Event()
98+ self._this_is_the_first_loop = True
99+
100+ def initialize(self):
101+ """
102+ See GenericBackend for an explanation of this function.
103+ """
104+ super(Backend, self).initialize()
105+ self.rtm_proxy = RTMProxy(self._ask_user_to_confirm_authentication,
106+ self.token)
107+
108+ def save_state(self):
109+ """
110+ See GenericBackend for an explanation of this function.
111+ """
112+ self._store_pickled_file(self.sync_engine_path, self.sync_engine)
113+
114+ def _ask_user_to_confirm_authentication(self):
115+ '''
116+ Calls for a user interaction during authentication
117+ '''
118+ self.login_event.clear()
119+ BackendSignals().interaction_requested(self.get_id(),
120+ "You need to authenticate to Remember The Milk. A browser"
121+ " is opening with a login page.\n When you have "
122+ " logged in and given GTG the requested permissions,\n"
123+ " press the 'Confirm' button", \
124+ BackendSignals().INTERACTION_CONFIRM, \
125+ "on_login")
126+ self.login_event.wait()
127+
128+ def on_login(self):
129+ '''
130+ Called when the user confirms the login
131+ '''
132+ self.login_event.set()
133+
134+###############################################################################
135+### TWO WAY SYNC ##############################################################
136+###############################################################################
137+
138+ def do_periodic_import(self):
139+ """
140+ See PeriodicImportBackend for an explanation of this function.
141+ """
142+
143+ #we get the old list of synced tasks, and compare with the new tasks set
144+ stored_rtm_task_ids = self.sync_engine.get_all_remote()
145+ current_rtm_task_ids = [tid for tid in \
146+ self.rtm_proxy.get_rtm_tasks_dict().iterkeys()]
147+
148+ if self._this_is_the_first_loop:
149+ self._on_successful_authentication()
150+
151+ #If it's the very first time the backend is run, it's possible that the
152+ # user already synced his tasks in some way (but we don't know that).
153+ # Therefore, we attempt to induce those tasks relationships matching the
154+ # titles.
155+ if self._parameters["is-first-run"]:
156+ gtg_titles_dic = {}
157+ for tid in self.datastore.get_all_tasks():
158+ gtg_task = self.datastore.get_task(tid)
159+ if not self._gtg_task_is_syncable_per_attached_tags(gtg_task):
160+ continue
161+ gtg_title = gtg_task.get_title()
162+ if gtg_titles_dic.has_key(gtg_title):
163+ gtg_titles_dic[gtg_task.get_title()].append(tid)
164+ else:
165+ gtg_titles_dic[gtg_task.get_title()] = [tid]
166+ for rtm_task_id in current_rtm_task_ids:
167+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
168+ try:
169+ tids = gtg_titles_dic[rtm_task.get_title()]
170+ #we remove the tid, so that it can't be linked to two
171+ # different rtm tasks
172+ tid = tids.pop()
173+ gtg_task = self.datastore.get_task(tid)
174+ meme = SyncMeme(gtg_task.get_modified(),
175+ rtm_task.get_modified(),
176+ "GTG")
177+ self.sync_engine.record_relationship( \
178+ local_id = tid,
179+ remote_id = rtm_task.get_id(),
180+ meme = meme)
181+ except KeyError:
182+ pass
183+ #a first run has been completed successfully
184+ self._parameters["is-first-run"] = False
185+
186+ for rtm_task_id in current_rtm_task_ids:
187+ self.cancellation_point()
188+ #Adding and updating
189+ self._process_rtm_task(rtm_task_id)
190+
191+ for rtm_task_id in set(stored_rtm_task_ids).difference(\
192+ set(current_rtm_task_ids)):
193+ self.cancellation_point()
194+ #Removing the old ones
195+ if not self.please_quit:
196+ tid = self.sync_engine.get_local_id(rtm_task_id)
197+ self.datastore.request_task_deletion(tid)
198+ try:
199+ self.sync_engine.break_relationship(remote_id = \
200+ rtm_task_id)
201+ self.save_state()
202+ except KeyError:
203+ pass
204+
205+ def _on_successful_authentication(self):
206+ '''
207+ Saves the token and requests a full flush on first autentication
208+ '''
209+ self._this_is_the_first_loop = False
210+ self._store_pickled_file(self.token_path,
211+ self.rtm_proxy.get_auth_token())
212+ #we ask the Datastore to flush all the tasks on us
213+ threading.Timer(10,
214+ self.datastore.flush_all_tasks,
215+ args =(self.get_id(),)).start()
216+
217+ @interruptible
218+ def remove_task(self, tid):
219+ """
220+ See GenericBackend for an explanation of this function.
221+ """
222+ if not self.rtm_proxy.is_authenticated():
223+ return
224+ self.cancellation_point()
225+ try:
226+ rtm_task_id = self.sync_engine.get_remote_id(tid)
227+ if rtm_task_id not in self.rtm_proxy.get_rtm_tasks_dict():
228+ #we might need to refresh our task cache
229+ self.rtm_proxy.refresh_rtm_tasks_dict()
230+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
231+ rtm_task.delete()
232+ Log.debug("removing task %s from RTM" % rtm_task_id)
233+ except KeyError:
234+ pass
235+ try:
236+ self.sync_engine.break_relationship(local_id = tid)
237+ self.save_state()
238+ except:
239+ pass
240+
241+
242+###############################################################################
243+### Process tasks #############################################################
244+###############################################################################
245+
246+ @interruptible
247+ def set_task(self, task):
248+ """
249+ See GenericBackend for an explanation of this function.
250+ """
251+ if not self.rtm_proxy.is_authenticated():
252+ return
253+ self.cancellation_point()
254+ tid = task.get_id()
255+ is_syncable = self._gtg_task_is_syncable_per_attached_tags(task)
256+ action, rtm_task_id = self.sync_engine.analyze_local_id( \
257+ tid, \
258+ self.datastore.has_task, \
259+ self.rtm_proxy.has_rtm_task, \
260+ is_syncable)
261+ Log.debug("GTG->RTM set task (%s, %s)" % (action, is_syncable))
262+
263+ if action == None:
264+ return
265+
266+ if action == SyncEngine.ADD:
267+ if task.get_status() != Task.STA_ACTIVE:
268+ #OPTIMIZATION:
269+ #we don't sync tasks that have already been closed before we
270+ # even synced them once
271+ return
272+ try:
273+ rtm_task = self.rtm_proxy.create_new_rtm_task(task.get_title())
274+ self._populate_rtm_task(task, rtm_task)
275+ except:
276+ rtm_task.delete()
277+ raise
278+ meme = SyncMeme(task.get_modified(),
279+ rtm_task.get_modified(),
280+ "GTG")
281+ self.sync_engine.record_relationship( \
282+ local_id = tid, remote_id = rtm_task.get_id(), meme = meme)
283+
284+ elif action == SyncEngine.UPDATE:
285+ try:
286+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
287+ except KeyError:
288+ #in this case, we don't have yet the task in our local cache
289+ # of what's on the rtm website
290+ self.rtm_proxy.refresh_rtm_tasks_dict()
291+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
292+ with self.datastore.get_backend_mutex():
293+ meme = self.sync_engine.get_meme_from_local_id(task.get_id())
294+ newest = meme.which_is_newest(task.get_modified(),
295+ rtm_task.get_modified())
296+ if newest == "local":
297+ transaction_ids = []
298+ try:
299+ self._populate_rtm_task(task, rtm_task, transaction_ids)
300+ except:
301+ self.rtm_proxy.unroll_changes(transaction_ids)
302+ raise
303+ meme.set_remote_last_modified(rtm_task.get_modified())
304+ meme.set_local_last_modified(task.get_modified())
305+ else:
306+ #we skip saving the state
307+ return
308+
309+ elif action == SyncEngine.REMOVE:
310+ self.datastore.request_task_deletion(tid)
311+ try:
312+ self.sync_engine.break_relationship(local_id = tid)
313+ except KeyError:
314+ pass
315+
316+ elif action == SyncEngine.LOST_SYNCABILITY:
317+ try:
318+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
319+ except KeyError:
320+ #in this case, we don't have yet the task in our local cache
321+ # of what's on the rtm website
322+ self.rtm_proxy.refresh_rtm_tasks_dict()
323+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
324+ self._exec_lost_syncability(tid, rtm_task)
325+
326+ self.save_state()
327+
328+ def _exec_lost_syncability(self, tid, rtm_task):
329+ '''
330+ Executed when a relationship between tasks loses its syncability
331+ property. See SyncEngine for an explanation of that.
332+
333+ @param tid: a GTG task tid
334+ @param note: a RTM task
335+ '''
336+ self.cancellation_point()
337+ meme = self.sync_engine.get_meme_from_local_id(tid)
338+ #First of all, the relationship is lost
339+ self.sync_engine.break_relationship(local_id = tid)
340+ if meme.get_origin() == "GTG":
341+ rtm_task.delete()
342+ else:
343+ self.datastore.request_task_deletion(tid)
344+
345+ def _process_rtm_task(self, rtm_task_id):
346+ '''
347+ Takes a rtm task id and carries out the necessary operations to
348+ refresh the sync state
349+ '''
350+ self.cancellation_point()
351+ if not self.rtm_proxy.is_authenticated():
352+ return
353+ rtm_task = self.rtm_proxy.get_rtm_tasks_dict()[rtm_task_id]
354+ is_syncable = self._rtm_task_is_syncable_per_attached_tags(rtm_task)
355+ action, tid = self.sync_engine.analyze_remote_id( \
356+ rtm_task_id,
357+ self.datastore.has_task,
358+ self.rtm_proxy.has_rtm_task,
359+ is_syncable)
360+ Log.debug("GTG<-RTM set task (%s, %s)" % (action, is_syncable))
361+
362+ if action == None:
363+ return
364+
365+ if action == SyncEngine.ADD:
366+ if rtm_task.get_status() != Task.STA_ACTIVE:
367+ #OPTIMIZATION:
368+ #we don't sync tasks that have already been closed before we
369+ # even saw them
370+ return
371+ tid = str(uuid.uuid4())
372+ task = self.datastore.task_factory(tid)
373+ self._populate_task(task, rtm_task)
374+ meme = SyncMeme(task.get_modified(),
375+ rtm_task.get_modified(),
376+ "RTM")
377+ self.sync_engine.record_relationship( \
378+ local_id = tid,
379+ remote_id = rtm_task_id,
380+ meme = meme)
381+ self.datastore.push_task(task)
382+
383+ elif action == SyncEngine.UPDATE:
384+ task = self.datastore.get_task(tid)
385+ with self.datastore.get_backend_mutex():
386+ meme = self.sync_engine.get_meme_from_remote_id(rtm_task_id)
387+ newest = meme.which_is_newest(task.get_modified(),
388+ rtm_task.get_modified())
389+ if newest == "remote":
390+ self._populate_task(task, rtm_task)
391+ meme.set_remote_last_modified(rtm_task.get_modified())
392+ meme.set_local_last_modified(task.get_modified())
393+ else:
394+ #we skip saving the state
395+ return
396+
397+ elif action == SyncEngine.REMOVE:
398+ try:
399+ rtm_task.delete()
400+ self.sync_engine.break_relationship(remote_id = rtm_task_id)
401+ except KeyError:
402+ pass
403+
404+ elif action == SyncEngine.LOST_SYNCABILITY:
405+ self._exec_lost_syncability(tid, rtm_task)
406+
407+ self.save_state()
408+
409+###############################################################################
410+### Helper methods ############################################################
411+###############################################################################
412+
413+ def _populate_task(self, task, rtm_task):
414+ '''
415+ Copies the content of a RTMTask in a Task
416+ '''
417+ task.set_title(rtm_task.get_title())
418+ task.set_text(rtm_task.get_text())
419+ task.set_due_date(rtm_task.get_due_date())
420+ status = rtm_task.get_status()
421+ if GTG_TO_RTM_STATUS[task.get_status()] != status:
422+ task.set_status(rtm_task.get_status())
423+ #tags
424+ tags = set(['@%s' % tag for tag in rtm_task.get_tags()])
425+ gtg_tags_lower = set([t.get_name().lower() for t in task.get_tags()])
426+ #tags to remove
427+ for tag in gtg_tags_lower.difference(tags):
428+ task.remove_tag(tag)
429+ #tags to add
430+ for tag in tags.difference(gtg_tags_lower):
431+ gtg_all_tags = [t.get_name() for t in \
432+ self.datastore.get_all_tags()]
433+ matching_tags = filter(lambda t: t.lower() == tag, gtg_all_tags)
434+ if len(matching_tags) != 0:
435+ tag = matching_tags[0]
436+ task.add_tag(tag)
437+
438+ def _populate_rtm_task(self, task, rtm_task, transaction_ids = []):
439+ '''
440+ Copies the content of a Task into a RTMTask
441+
442+ @param task: a GTG Task
443+ @param rtm_task: an RTMTask
444+ @param transaction_ids: a list to fill with transaction ids
445+ '''
446+ #Get methods of an rtm_task are fast, set are slow: therefore,
447+ # we try to use set as rarely as possible
448+
449+ #first thing: the status. This way, if we are syncing a completed
450+ # task it doesn't linger for ten seconds in the RTM Inbox
451+ status = task.get_status()
452+ if rtm_task.get_status() != status:
453+ self.__call_or_retry(rtm_task.set_status, status, transaction_ids)
454+ title = task.get_title()
455+ if rtm_task.get_title() != title:
456+ self.__call_or_retry(rtm_task.set_title, title, transaction_ids)
457+ text = task.get_excerpt(strip_tags = True, strip_subtasks = True)
458+ if rtm_task.get_text() != text:
459+ self.__call_or_retry(rtm_task.set_text, text, transaction_ids)
460+ tags = task.get_tags_name()
461+ rtm_task_tags = []
462+ for tag in rtm_task.get_tags():
463+ if tag[0] != '@':
464+ tag = '@' + tag
465+ rtm_task_tags.append(tag)
466+ #rtm tags are lowercase only
467+ if rtm_task_tags != [t.lower() for t in tags]:
468+ self.__call_or_retry(rtm_task.set_tags, tags, transaction_ids)
469+ if isinstance(task.get_due_date(), NoDate):
470+ due_date = None
471+ else:
472+ due_date = task.get_due_date().to_py_date()
473+ if rtm_task.get_due_date() != due_date:
474+ self.__call_or_retry(rtm_task.set_due_date, due_date,
475+ transaction_ids)
476+
477+ def __call_or_retry(self, fun, *args):
478+ '''
479+ This function cannot stand the call "fun" to fail, so it retries
480+ three times before giving up.
481+ '''
482+ MAX_ATTEMPTS = 3
483+ for i in xrange(MAX_ATTEMPTS):
484+ try:
485+ return fun(*args)
486+ except:
487+ if i >= MAX_ATTEMPTS:
488+ raise
489+
490+ def _rtm_task_is_syncable_per_attached_tags(self, rtm_task):
491+ '''
492+ Helper function which checks if the given task satisfies the filtering
493+ imposed by the tags attached to the backend.
494+ That means, if a user wants a backend to sync only tasks tagged @works,
495+ this function should be used to check if that is verified.
496+
497+ @returns bool: True if the task should be synced
498+ '''
499+ attached_tags = self.get_attached_tags()
500+ if GenericBackend.ALLTASKS_TAG in attached_tags:
501+ return True
502+ for tag in rtm_task.get_tags():
503+ if "@" + tag in attached_tags:
504+ return True
505+ return False
506+
507+###############################################################################
508+### RTM PROXY #################################################################
509+###############################################################################
510+
511+class RTMProxy(object):
512+ '''
513+ The purpose of this class is producing an updated list of RTMTasks.
514+ To do that, it handles:
515+ - authentication to RTM
516+ - keeping the list fresh
517+ - downloading the list
518+ '''
519+
520+
521+ PUBLIC_KEY = "2a440fdfe9d890c343c25a91afd84c7e"
522+ PRIVATE_KEY = "ca078fee48d0bbfa"
523+
524+ def __init__(self,
525+ auth_confirm_fun,
526+ token = None):
527+ self.auth_confirm = auth_confirm_fun
528+ self.token = token
529+ self.authenticated = threading.Event()
530+ self.login_event = threading.Event()
531+ self.is_not_refreshing = threading.Event()
532+ self.is_not_refreshing.set()
533+
534+ ##########################################################################
535+ ### AUTHENTICATION #######################################################
536+ ##########################################################################
537+
538+ def start_authentication(self):
539+ '''
540+ Launches the authentication process
541+ '''
542+ initialize_thread = threading.Thread(target = self._authenticate)
543+ initialize_thread.setDaemon(True)
544+ initialize_thread.start()
545+
546+ def is_authenticated(self):
547+ '''
548+ Returns true if we've autheticated to RTM
549+ '''
550+ return self.authenticated.isSet()
551+
552+ def wait_for_authentication(self):
553+ '''
554+ Inhibits the thread until authentication occours
555+ '''
556+ self.authenticated.wait()
557+
558+ def get_auth_token(self):
559+ '''
560+ Returns the oauth token, or none
561+ '''
562+ try:
563+ return self.token
564+ except:
565+ return None
566+
567+ def _authenticate(self):
568+ '''
569+ authentication main function
570+ '''
571+ self.authenticated.clear()
572+ while not self.authenticated.isSet():
573+ if not self.token:
574+ self.rtm= createRTM(self.PUBLIC_KEY, self.PRIVATE_KEY, self.token)
575+ subprocess.Popen(['xdg-open', self.rtm.getAuthURL()])
576+ self.auth_confirm()
577+ try:
578+ time.sleep(1)
579+ self.token = self.rtm.getToken()
580+ except Exception, e:
581+ #something went wrong.
582+ self.token = None
583+ continue
584+ try:
585+ if self._login():
586+ self.authenticated.set()
587+ except exceptions.IOError, e:
588+ BackendSignals().backend_failed(self.get_id(), \
589+ BackendSignals.ERRNO_NETWORK)
590+
591+ def _login(self):
592+ '''
593+ Tries to establish a connection to rtm with a token got from the
594+ authentication process
595+ '''
596+ try:
597+ self.rtm = createRTM(self.PUBLIC_KEY, self.PRIVATE_KEY, self.token)
598+ self.timeline = self.rtm.timelines.create().timeline
599+ return True
600+ except (RTMError, RTMAPIError), e:
601+ Log.error("RTM ERROR" + str(e))
602+ return False
603+
604+ ##########################################################################
605+ ### RTM TASKS HANDLING ###################################################
606+ ##########################################################################
607+
608+ def unroll_changes(self, transaction_ids):
609+ '''
610+ Roll backs the changes tracked by the list of transaction_ids given
611+ '''
612+ for transaction_id in transaction_ids:
613+ self.rtm.transactions.undo(timeline = self.timeline,
614+ transaction_id = transaction_id)
615+
616+ def get_rtm_tasks_dict(self):
617+ '''
618+ Returns a dict of RTMtasks. It will start authetication if necessary.
619+ The dict is kept updated automatically.
620+ '''
621+ if not hasattr(self, '_rtm_task_dict'):
622+ self.refresh_rtm_tasks_dict()
623+ else:
624+ time_difference = datetime.datetime.now() - \
625+ self.__rtm_task_dict_timestamp
626+ if time_difference.seconds > 60:
627+ self.refresh_rtm_tasks_dict()
628+ return self._rtm_task_dict.copy()
629+
630+ def __getattr_the_rtm_way(self, an_object, attribute):
631+ '''
632+ RTM, to compress the XML file they send to you, cuts out all the
633+ unnecessary stuff.
634+ Because of that, getting an attribute from an object must check if one
635+ of those optimizations has been used.
636+ This function always returns a list wrapping the objects found (if any).
637+ '''
638+ try:
639+ list_or_object = getattr(an_object, attribute)
640+ except AttributeError:
641+ return []
642+
643+ if isinstance(list_or_object, list):
644+ return list_or_object
645+ else:
646+ return [list_or_object]
647+
648+ def __get_rtm_lists(self):
649+ '''
650+ Gets the list of the RTM Lists (the tabs on the top of rtm website)
651+ '''
652+ rtm_get_list_output = self.rtm.lists.getList()
653+ #Here's the attributes of RTM lists. For the list of them, see
654+ #http://www.rememberthemilk.com/services/api/methods/rtm.lists.getList.rtm
655+ return self.__getattr_the_rtm_way(self.rtm.lists.getList().lists, 'list')
656+
657+ def __get_rtm_taskseries_in_list(self, list_id):
658+ '''
659+ Gets the list of "taskseries" objects in a rtm list.
660+ For an explenation of what are those, see
661+ http://www.rememberthemilk.com/services/api/tasks.rtm
662+ '''
663+ list_object_wrapper = self.rtm.tasks.getList(list_id = list_id, \
664+ filter = 'includeArchived:true').tasks
665+ list_object_list = self.__getattr_the_rtm_way(list_object_wrapper, 'list')
666+ if not list_object_list:
667+ return []
668+ #we asked for one, so we should get one
669+ assert(len(list_object_list), 1)
670+ list_object = list_object_list[0]
671+ #check that the given list is the correct one
672+ assert(list_object.id == list_id)
673+ return self.__getattr_the_rtm_way(list_object, 'taskseries')
674+
675+ def refresh_rtm_tasks_dict(self):
676+ '''
677+ Builds a list of RTMTasks fetched from RTM
678+ '''
679+ if not self.is_authenticated():
680+ self.start_authentication()
681+ self.wait_for_authentication()
682+
683+ if not self.is_not_refreshing.isSet():
684+ #if we're already refreshing, we just wait for that to happen and
685+ # then we immediately return
686+ self.is_not_refreshing.wait()
687+ return
688+ self.is_not_refreshing.clear()
689+ Log.debug('refreshing rtm')
690+
691+ #To understand what this function does, here's a sample output of the
692+ #plain getLists() from RTM api:
693+ # http://www.rememberthemilk.com/services/api/tasks.rtm
694+
695+ #our purpose is to fill this with "tasks_id: RTMTask" items
696+ rtm_tasks_dict = {}
697+
698+ rtm_lists_list = self.__get_rtm_lists()
699+ #for each rtm list, we retrieve all the tasks in it
700+ for rtm_list in rtm_lists_list:
701+ if rtm_list.archived != '0' or rtm_list.smart != '0':
702+ #we skip archived and smart lists
703+ continue
704+ rtm_taskseries_list = self.__get_rtm_taskseries_in_list(rtm_list.id)
705+ for rtm_taskseries in rtm_taskseries_list:
706+ #we drill down to actual tasks
707+ rtm_tasks_list = self.__getattr_the_rtm_way(rtm_taskseries, 'task')
708+ for rtm_task in rtm_tasks_list:
709+ rtm_tasks_dict[rtm_task.id] = RTMTask(rtm_task,
710+ rtm_taskseries,
711+ rtm_list,
712+ self.rtm,
713+ self.timeline)
714+
715+ #we're done: we store the dict in this class and we annotate the time we
716+ # got it
717+ self._rtm_task_dict = rtm_tasks_dict
718+ self.__rtm_task_dict_timestamp = datetime.datetime.now()
719+ self.is_not_refreshing.set()
720+
721+ def has_rtm_task(self, rtm_task_id):
722+ '''
723+ Returns True if we have seen that task id
724+ '''
725+ cache_result = rtm_task_id in self.get_rtm_tasks_dict()
726+ return cache_result
727+ #it may happen that the rtm_task is on the website but we haven't
728+ #downloaded it yet. We need to update the local cache.
729+
730+ #it's a big speed loss. Let's see if we can avoid it.
731+ #self.refresh_rtm_tasks_dict()
732+ #return rtm_task_id in self.get_rtm_tasks_dict()
733+
734+ def create_new_rtm_task(self, title, transaction_ids = []):
735+ '''
736+ Creates a new rtm task
737+ '''
738+ result = self.rtm.tasks.add(timeline = self.timeline, name = title)
739+ rtm_task = RTMTask(result.list.taskseries.task,
740+ result.list.taskseries,
741+ result.list,
742+ self.rtm,
743+ self.timeline)
744+ #adding to the dict right away
745+ if hasattr(self, '_rtm_task_dict'):
746+ #if the list hasn't been downloaded yet, we do not create a list,
747+ # because the fact that the list is created is used to keep track of
748+ # list updates
749+ self._rtm_task_dict[rtm_task.get_id()] = rtm_task
750+ transaction_ids.append(result.transaction.id)
751+ return rtm_task
752+
753+
754+
755+###############################################################################
756+### RTM TASK ##################################################################
757+###############################################################################
758+
759+#dictionaries to translate a RTM status into a GTG one (and back)
760+GTG_TO_RTM_STATUS = {Task.STA_ACTIVE: True,
761+ Task.STA_DONE: False,
762+ Task.STA_DISMISSED: False}
763+
764+RTM_TO_GTG_STATUS = {True: Task.STA_ACTIVE,
765+ False: Task.STA_DONE}
766+
767+
768+
769+class RTMTask(object):
770+ '''
771+ A proxy object that encapsulates a RTM task, giving an easier API to access
772+ and modify its attributes.
773+ This backend already uses a library to interact with RTM, but that is just a
774+ thin proxy for HTML gets and posts.
775+ The meaning of all "special words"
776+ http://www.rememberthemilk.com/services/api/tasks.rtm
777+ '''
778+
779+
780+ def __init__(self, rtm_task, rtm_taskseries, rtm_list, rtm, timeline):
781+ '''
782+ sets up the various parameters needed to interact with a task.
783+
784+ @param task: the task object given by the underlying library
785+ @param rtm_list: the rtm list the task resides in.
786+ @param rtm_taskseries: all the tasks are encapsulated in a taskseries
787+ object. From RTM website:
788+ "A task series is a grouping of tasks generated
789+ by a recurrence pattern (more specifically, a
790+ recurrence pattern of type every – an after type
791+ recurrence generates a new task series for every
792+ occurrence). Task series' share common
793+ properties such as:
794+ Name.
795+ Recurrence pattern.
796+ Tags.
797+ Notes.
798+ Priority."
799+ @param rtm: a handle of the rtm object, to be able to speak with rtm.
800+ Authentication should have already been done.
801+ @param timeline: a "timeline" is a series of operations rtm can undo in
802+ bulk. We are free of requesting new timelines as we
803+ please, with the obvious drawback of being slower.
804+ '''
805+ self.rtm_task = rtm_task
806+ self.rtm_list = rtm_list
807+ self.rtm_taskseries = rtm_taskseries
808+ self.rtm = rtm
809+ self.timeline = timeline
810+
811+ def get_title(self):
812+ '''Returns the title of the task, if any'''
813+ return self.rtm_taskseries.name
814+
815+ def set_title(self, title, transaction_ids = []):
816+ '''Sets the task title'''
817+ title = cgi.escape(title)
818+ result = self.rtm.tasks.setName(timeline = self.timeline,
819+ list_id = self.rtm_list.id,
820+ taskseries_id = self.rtm_taskseries.id,
821+ task_id = self.rtm_task.id,
822+ name = title)
823+ transaction_ids.append(result.transaction.id)
824+
825+ def get_id(self):
826+ '''Return the task id. The taskseries id is *different*'''
827+ return self.rtm_task.id
828+
829+ def get_status(self):
830+ '''Returns the task status, in GTG terminology'''
831+ return RTM_TO_GTG_STATUS[self.rtm_task.completed == ""]
832+
833+ def set_status(self, gtg_status, transaction_ids = []):
834+ '''Sets the task status, in GTG terminology'''
835+ status = GTG_TO_RTM_STATUS[gtg_status]
836+ if status == True:
837+ api_call = self.rtm.tasks.uncomplete
838+ else:
839+ api_call = self.rtm.tasks.complete
840+ result = api_call(timeline = self.timeline,
841+ list_id = self.rtm_list.id,
842+ taskseries_id = self.rtm_taskseries.id,
843+ task_id = self.rtm_task.id)
844+ transaction_ids.append(result.transaction.id)
845+
846+
847+ def get_tags(self):
848+ '''Returns the task tags'''
849+ tags = self.rtm_taskseries.tags
850+ if not tags:
851+ return []
852+ else:
853+ return self.__getattr_the_rtm_way(tags, 'tag')
854+
855+ def __getattr_the_rtm_way(self, an_object, attribute):
856+ '''
857+ RTM, to compress the XML file they send to you, cuts out all the
858+ unnecessary stuff.
859+ Because of that, getting an attribute from an object must check if one
860+ of those optimizations has been used.
861+ This function always returns a list wrapping the objects found (if any).
862+ '''
863+ try:
864+ list_or_object = getattr(an_object, attribute)
865+ except AttributeError:
866+ return []
867+ if isinstance(list_or_object, list):
868+ return list_or_object
869+ else:
870+ return [list_or_object]
871+
872+ def set_tags(self, tags, transaction_ids = []):
873+ '''
874+ Sets a new set of tags to a task. Old tags are deleted.
875+ '''
876+ #RTM accept tags without "@" as prefix, and lowercase
877+ tags = [tag[1:].lower() for tag in tags]
878+ #formatting them in a comma-separated string
879+ if len(tags) > 0:
880+ tagstxt = reduce(lambda x,y: x + ", " + y, tags)
881+ else:
882+ tagstxt = ""
883+ result = self.rtm.tasks.setTags(timeline = self.timeline,
884+ list_id = self.rtm_list.id,
885+ taskseries_id = self.rtm_taskseries.id,
886+ task_id = self.rtm_task.id,
887+ tags = tagstxt)
888+ transaction_ids.append(result.transaction.id)
889+
890+ def get_text(self):
891+ '''
892+ Gets the content of RTM notes, aggregated in a single string
893+ '''
894+ notes = self.rtm_taskseries.notes
895+ if not notes:
896+ return ""
897+ else:
898+ note_list = self.__getattr_the_rtm_way(notes, 'note')
899+ return "".join(map(lambda note: "%s\n" %getattr(note, '$t'),
900+ note_list))
901+
902+ def set_text(self, text, transaction_ids = []):
903+ '''
904+ deletes all the old notes in a task and sets a single note with the
905+ given text
906+ '''
907+ #delete old notes
908+ notes = self.rtm_taskseries.notes
909+ if notes:
910+ note_list = self.__getattr_the_rtm_way(notes, 'note')
911+ for note_id in [note.id for note in note_list]:
912+ result = self.rtm.tasksNotes.delete(timeline = self.timeline,
913+ note_id = note_id)
914+ transaction_ids.append(result.transaction.id)
915+
916+ if text == "":
917+ return
918+ text = cgi.escape(text)
919+
920+ #RTM does not support well long notes (that is, it denies the request)
921+ #Thus, we split long text in chunks. To make them show in the correct
922+ #order on the website, we have to upload them from the last to the first
923+ # (they show the most recent on top)
924+ text_cursor_end = len(text)
925+ while True:
926+ text_cursor_start = text_cursor_end - 1000
927+ if text_cursor_start < 0:
928+ text_cursor_start = 0
929+
930+ result = self.rtm.tasksNotes.add(timeline = self.timeline,
931+ list_id = self.rtm_list.id,
932+ taskseries_id = self.rtm_taskseries.id,
933+ task_id = self.rtm_task.id,
934+ note_title = "",
935+ note_text = text[text_cursor_start:
936+ text_cursor_end])
937+ transaction_ids.append(result.transaction.id)
938+ if text_cursor_start <= 0:
939+ break
940+ text_cursor_end = text_cursor_start - 1
941+
942+ def get_due_date(self):
943+ '''
944+ Gets the task due date
945+ '''
946+ due = self.rtm_task.due
947+ if due == "":
948+ return NoDate()
949+ date = self.__time_rtm_to_datetime(due).date()
950+ if date:
951+ return RealDate(date)
952+ else:
953+ return NoDate()
954+
955+ def set_due_date(self, due, transaction_ids = []):
956+ '''
957+ Sets the task due date
958+ '''
959+ kwargs = {'timeline': self.timeline,
960+ 'list_id': self.rtm_list.id,
961+ 'taskseries_id': self.rtm_taskseries.id,
962+ 'task_id': self.rtm_task.id}
963+ if due != None:
964+ kwargs['parse'] = 1
965+ kwargs['due'] = self.__time_date_to_rtm(due)
966+ result = self.rtm.tasks.setDueDate(**kwargs)
967+ transaction_ids.append(result.transaction.id)
968+
969+ def get_modified(self):
970+ '''
971+ Gets the task modified time, in local time
972+ '''
973+ #RTM does not set a "modified" attribute in a new note because it uses a
974+ # "added" attribute. We need to check for both.
975+ if hasattr(self.rtm_task, 'modified'):
976+ rtm_task_modified = self.__time_rtm_to_datetime(\
977+ self.rtm_task.modified)
978+ else:
979+ rtm_task_modified = self.__time_rtm_to_datetime(\
980+ self.rtm_task.added)
981+ if hasattr(self.rtm_taskseries, 'modified'):
982+ rtm_taskseries_modified = self.__time_rtm_to_datetime(\
983+ self.rtm_taskseries.modified)
984+ else:
985+ rtm_taskseries_modified = self.__time_rtm_to_datetime(\
986+ self.rtm_taskseries.added)
987+ return max(rtm_task_modified, rtm_taskseries_modified)
988+
989+ def delete(self):
990+ self.rtm.tasks.delete(timeline = self.timeline,
991+ list_id = self.rtm_list.id,
992+ taskseries_id = self.rtm_taskseries.id,
993+ task_id = self.rtm_task.id)
994+
995+ #RTM speaks utc, and accepts utc if the "parse" option is set.
996+ def __tz_utc_to_local(self, dt):
997+ dt = dt.replace(tzinfo = tzutc())
998+ dt = dt.astimezone(tzlocal())
999+ return dt.replace(tzinfo = None)
1000+
1001+ def __tz_local_to_utc(self, dt):
1002+ dt = dt.replace(tzinfo = tzlocal())
1003+ dt = dt.astimezone(tzutc())
1004+ return dt.replace(tzinfo = None)
1005+
1006+ def __time_rtm_to_datetime(self, string):
1007+ string = string.split('.')[0].split('Z')[0]
1008+ dt = datetime.datetime.strptime(string.split(".")[0], \
1009+ "%Y-%m-%dT%H:%M:%S")
1010+ return self.__tz_utc_to_local(dt)
1011+
1012+ def __time_rtm_to_date(self, string):
1013+ string = string.split('.')[0].split('Z')[0]
1014+ dt = datetime.datetime.strptime(string.split(".")[0], "%Y-%m-%d")
1015+ return self.__tz_utc_to_local(dt)
1016+
1017+ def __time_datetime_to_rtm(self, timeobject):
1018+ if timeobject == None:
1019+ return ""
1020+ timeobject = self.__tz_local_to_utc(timeobject)
1021+ return timeobject.strftime("%Y-%m-%dT%H:%M:%S")
1022+
1023+ def __time_date_to_rtm(self, timeobject):
1024+ if timeobject == None:
1025+ return ""
1026+ #WARNING: no timezone? seems to break the symmetry.
1027+ return timeobject.strftime("%Y-%m-%d")
1028+
1029+ def __str__(self):
1030+ return "Task %s (%s)" % (self.get_title(), self.get_id())
1031
1032=== added directory 'GTG/backends/rtm'
1033=== added file 'GTG/backends/rtm/__init__.py'
1034=== added file 'GTG/backends/rtm/rtm.py'
1035--- GTG/backends/rtm/rtm.py 1970-01-01 00:00:00 +0000
1036+++ GTG/backends/rtm/rtm.py 2010-09-03 23:47:39 +0000
1037@@ -0,0 +1,402 @@
1038+# Python library for Remember The Milk API
1039+
1040+__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
1041+__all__ = (
1042+ 'API',
1043+ 'createRTM',
1044+ 'set_log_level',
1045+ )
1046+
1047+
1048+import warnings
1049+import urllib
1050+import time
1051+from hashlib import md5
1052+from GTG import _
1053+
1054+warnings.simplefilter('default', ImportWarning)
1055+
1056+_use_simplejson = False
1057+try:
1058+ import simplejson
1059+ _use_simplejson = True
1060+except ImportError:
1061+ try:
1062+ from django.utils import simplejson
1063+ _use_simplejson = True
1064+ except ImportError:
1065+ pass
1066+
1067+if not _use_simplejson:
1068+ warnings.warn("simplejson module is not available, "
1069+ "falling back to the internal JSON parser. "
1070+ "Please consider installing the simplejson module from "
1071+ "http://pypi.python.org/pypi/simplejson.", ImportWarning,
1072+ stacklevel=2)
1073+
1074+#logging.basicConfig()
1075+#LOG = logging.getLogger(__name__)
1076+#LOG.setLevel(logging.INFO)
1077+
1078+SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
1079+AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
1080+
1081+
1082+class RTMError(Exception): pass
1083+
1084+class RTMAPIError(RTMError): pass
1085+
1086+class AuthStateMachine(object):
1087+
1088+ class NoData(RTMError): pass
1089+
1090+ def __init__(self, states):
1091+ self.states = states
1092+ self.data = {}
1093+
1094+ def dataReceived(self, state, datum):
1095+ if state not in self.states:
1096+ error_string = _("Invalid state")+" <%s>"
1097+
1098+ raise RTMError, error_string % state
1099+ self.data[state] = datum
1100+
1101+ def get(self, state):
1102+ if state in self.data:
1103+ return self.data[state]
1104+ else:
1105+ raise AuthStateMachine.NoData, 'No data for <%s>' % state
1106+
1107+
1108+class RTM(object):
1109+
1110+ def __init__(self, apiKey, secret, token=None):
1111+ self.apiKey = apiKey
1112+ self.secret = secret
1113+ self.authInfo = AuthStateMachine(['frob', 'token'])
1114+
1115+ # this enables one to do 'rtm.tasks.getList()', for example
1116+ for prefix, methods in API.items():
1117+ setattr(self, prefix,
1118+ RTMAPICategory(self, prefix, methods))
1119+
1120+ if token:
1121+ self.authInfo.dataReceived('token', token)
1122+
1123+ def _sign(self, params):
1124+ "Sign the parameters with MD5 hash"
1125+ pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
1126+ return md5(self.secret+pairs).hexdigest()
1127+
1128+ def get(self, **params):
1129+ "Get the XML response for the passed `params`."
1130+ params['api_key'] = self.apiKey
1131+ params['format'] = 'json'
1132+ params['api_sig'] = self._sign(params)
1133+
1134+ json = openURL(SERVICE_URL, params).read()
1135+
1136+ #LOG.debug("JSON response: \n%s" % json)
1137+ if _use_simplejson:
1138+ data = dottedDict('ROOT', simplejson.loads(json))
1139+ else:
1140+ data = dottedJSON(json)
1141+ rsp = data.rsp
1142+
1143+ if rsp.stat == 'fail':
1144+ raise RTMAPIError, 'API call failed - %s (%s)' % (
1145+ rsp.err.msg, rsp.err.code)
1146+ else:
1147+ return rsp
1148+
1149+ def getNewFrob(self):
1150+ rsp = self.get(method='rtm.auth.getFrob')
1151+ self.authInfo.dataReceived('frob', rsp.frob)
1152+ return rsp.frob
1153+
1154+ def getAuthURL(self):
1155+ try:
1156+ frob = self.authInfo.get('frob')
1157+ except AuthStateMachine.NoData:
1158+ frob = self.getNewFrob()
1159+
1160+ params = {
1161+ 'api_key': self.apiKey,
1162+ 'perms' : 'delete',
1163+ 'frob' : frob
1164+ }
1165+ params['api_sig'] = self._sign(params)
1166+ return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
1167+
1168+ def getToken(self):
1169+ frob = self.authInfo.get('frob')
1170+ rsp = self.get(method='rtm.auth.getToken', frob=frob)
1171+ self.authInfo.dataReceived('token', rsp.auth.token)
1172+ return rsp.auth.token
1173+
1174+class RTMAPICategory:
1175+ "See the `API` structure and `RTM.__init__`"
1176+
1177+ def __init__(self, rtm, prefix, methods):
1178+ self.rtm = rtm
1179+ self.prefix = prefix
1180+ self.methods = methods
1181+
1182+ def __getattr__(self, attr):
1183+ if attr in self.methods:
1184+ rargs, oargs = self.methods[attr]
1185+ if self.prefix == 'tasksNotes':
1186+ aname = 'rtm.tasks.notes.%s' % attr
1187+ else:
1188+ aname = 'rtm.%s.%s' % (self.prefix, attr)
1189+ return lambda **params: self.callMethod(
1190+ aname, rargs, oargs, **params)
1191+ else:
1192+ raise AttributeError, 'No such attribute: %s' % attr
1193+
1194+ def callMethod(self, aname, rargs, oargs, **params):
1195+ # Sanity checks
1196+ for requiredArg in rargs:
1197+ if requiredArg not in params:
1198+ raise TypeError, 'Required parameter (%s) missing' % requiredArg
1199+
1200+ for param in params:
1201+ if param not in rargs + oargs:
1202+ warnings.warn('Invalid parameter (%s)' % param)
1203+
1204+ return self.rtm.get(method=aname,
1205+ auth_token=self.rtm.authInfo.get('token'),
1206+ **params)
1207+
1208+
1209+
1210+# Utility functions
1211+
1212+def sortedItems(dictionary):
1213+ "Return a list of (key, value) sorted based on keys"
1214+ keys = dictionary.keys()
1215+ keys.sort()
1216+ for key in keys:
1217+ yield key, dictionary[key]
1218+
1219+def openURL(url, queryArgs=None):
1220+ if queryArgs:
1221+ url = url + '?' + urllib.urlencode(queryArgs)
1222+ #LOG.debug("URL> %s", url)
1223+ return urllib.urlopen(url)
1224+
1225+class dottedDict(object):
1226+ """Make dictionary items accessible via the object-dot notation."""
1227+
1228+ def __init__(self, name, dictionary):
1229+ self._name = name
1230+
1231+ if type(dictionary) is dict:
1232+ for key, value in dictionary.items():
1233+ if type(value) is dict:
1234+ value = dottedDict(key, value)
1235+ elif type(value) in (list, tuple) and key != 'tag':
1236+ value = [dottedDict('%s_%d' % (key, i), item)
1237+ for i, item in indexed(value)]
1238+ setattr(self, key, value)
1239+ else:
1240+ raise ValueError, 'not a dict: %s' % dictionary
1241+
1242+ def __repr__(self):
1243+ children = [c for c in dir(self) if not c.startswith('_')]
1244+ return 'dotted <%s> : %s' % (
1245+ self._name,
1246+ ', '.join(children))
1247+
1248+
1249+def safeEval(string):
1250+ return eval(string, {}, {})
1251+
1252+def dottedJSON(json):
1253+ return dottedDict('ROOT', safeEval(json))
1254+
1255+def indexed(seq):
1256+ index = 0
1257+ for item in seq:
1258+ yield index, item
1259+ index += 1
1260+
1261+
1262+# API spec
1263+
1264+API = {
1265+ 'auth': {
1266+ 'checkToken':
1267+ [('auth_token',), ()],
1268+ 'getFrob':
1269+ [(), ()],
1270+ 'getToken':
1271+ [('frob',), ()]
1272+ },
1273+ 'contacts': {
1274+ 'add':
1275+ [('timeline', 'contact'), ()],
1276+ 'delete':
1277+ [('timeline', 'contact_id'), ()],
1278+ 'getList':
1279+ [(), ()]
1280+ },
1281+ 'groups': {
1282+ 'add':
1283+ [('timeline', 'group'), ()],
1284+ 'addContact':
1285+ [('timeline', 'group_id', 'contact_id'), ()],
1286+ 'delete':
1287+ [('timeline', 'group_id'), ()],
1288+ 'getList':
1289+ [(), ()],
1290+ 'removeContact':
1291+ [('timeline', 'group_id', 'contact_id'), ()],
1292+ },
1293+ 'lists': {
1294+ 'add':
1295+ [('timeline', 'name',), ('filter',)],
1296+ 'archive':
1297+ [('timeline', 'list_id'), ()],
1298+ 'delete':
1299+ [('timeline', 'list_id'), ()],
1300+ 'getList':
1301+ [(), ()],
1302+ 'setDefaultList':
1303+ [('timeline'), ('list_id')],
1304+ 'setName':
1305+ [('timeline', 'list_id', 'name'), ()],
1306+ 'unarchive':
1307+ [('timeline',), ('list_id',)]
1308+ },
1309+ 'locations': {
1310+ 'getList':
1311+ [(), ()]
1312+ },
1313+ 'reflection': {
1314+ 'getMethodInfo':
1315+ [('methodName',), ()],
1316+ 'getMethods':
1317+ [(), ()]
1318+ },
1319+ 'settings': {
1320+ 'getList':
1321+ [(), ()]
1322+ },
1323+ 'tasks': {
1324+ 'add':
1325+ [('timeline', 'name',), ('list_id', 'parse',)],
1326+ 'addTags':
1327+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
1328+ ()],
1329+ 'complete':
1330+ [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
1331+ 'delete':
1332+ [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
1333+ 'getList':
1334+ [(),
1335+ ('list_id', 'filter', 'last_sync')],
1336+ 'movePriority':
1337+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
1338+ ()],
1339+ 'moveTo':
1340+ [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
1341+ ()],
1342+ 'postpone':
1343+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1344+ ()],
1345+ 'removeTags':
1346+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
1347+ ()],
1348+ 'setDueDate':
1349+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1350+ ('due', 'has_due_time', 'parse')],
1351+ 'setEstimate':
1352+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1353+ ('estimate',)],
1354+ 'setLocation':
1355+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1356+ ('location_id',)],
1357+ 'setName':
1358+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
1359+ ()],
1360+ 'setPriority':
1361+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1362+ ('priority',)],
1363+ 'setRecurrence':
1364+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1365+ ('repeat',)],
1366+ 'setTags':
1367+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1368+ ('tags',)],
1369+ 'setURL':
1370+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1371+ ('url',)],
1372+ 'uncomplete':
1373+ [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1374+ ()],
1375+ },
1376+ 'tasksNotes': {
1377+ 'add':
1378+ [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
1379+ 'delete':
1380+ [('timeline', 'note_id'), ()],
1381+ 'edit':
1382+ [('timeline', 'note_id', 'note_title', 'note_text'), ()]
1383+ },
1384+ 'test': {
1385+ 'echo':
1386+ [(), ()],
1387+ 'login':
1388+ [(), ()]
1389+ },
1390+ 'time': {
1391+ 'convert':
1392+ [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
1393+ 'parse':
1394+ [('text',), ('timezone', 'dateformat')]
1395+ },
1396+ 'timelines': {
1397+ 'create':
1398+ [(), ()]
1399+ },
1400+ 'timezones': {
1401+ 'getList':
1402+ [(), ()]
1403+ },
1404+ 'transactions': {
1405+ 'undo':
1406+ [('timeline', 'transaction_id'), ()]
1407+ },
1408+ }
1409+
1410+def createRTM(apiKey, secret, token=None):
1411+ rtm = RTM(apiKey, secret, token)
1412+# if token is None:
1413+# print 'No token found'
1414+# print 'Give me access here:', rtm.getAuthURL()
1415+# raw_input('Press enter once you gave access')
1416+# print 'Note down this token for future use:', rtm.getToken()
1417+
1418+ return rtm
1419+
1420+def test(apiKey, secret, token=None):
1421+ rtm = createRTM(apiKey, secret, token)
1422+
1423+ rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
1424+ print [t.name for t in rspTasks.tasks.list.taskseries]
1425+ print rspTasks.tasks.list.id
1426+
1427+ rspLists = rtm.lists.getList()
1428+ # print rspLists.lists.list
1429+ print [(x.name, x.id) for x in rspLists.lists.list]
1430+
1431+def set_log_level(level):
1432+ '''Sets the log level of the logger used by the module.
1433+
1434+ >>> import rtm
1435+ >>> import logging
1436+ >>> rtm.set_log_level(logging.INFO)
1437+ '''
1438+
1439+ #LOG.setLevel(level)
1440
1441=== removed file 'GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg'
1442--- GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg 2009-09-11 20:47:31 +0000
1443+++ GTG/plugins/rtm_sync/icons/hicolor/svg/rtm_image.svg 1970-01-01 00:00:00 +0000
1444@@ -1,206 +0,0 @@
1445-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1446-<!-- Created with Inkscape (http://www.inkscape.org/) -->
1447-<svg
1448- xmlns:dc="http://purl.org/dc/elements/1.1/"
1449- xmlns:cc="http://creativecommons.org/ns#"
1450- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1451- xmlns:svg="http://www.w3.org/2000/svg"
1452- xmlns="http://www.w3.org/2000/svg"
1453- xmlns:xlink="http://www.w3.org/1999/xlink"
1454- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1455- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1456- width="64px"
1457- height="64px"
1458- id="svg3727"
1459- sodipodi:version="0.32"
1460- inkscape:version="0.46"
1461- sodipodi:docname="rtm_image.svg"
1462- inkscape:output_extension="org.inkscape.output.svg.inkscape"
1463- inkscape:export-filename="/home/luca/gtg/rtm-sync-plugin/GTG/plugins/rtm_sync/icons/hicolor/16x16/rtm_image.png"
1464- inkscape:export-xdpi="22.5"
1465- inkscape:export-ydpi="22.5">
1466- <defs
1467- id="defs3729">
1468- <filter
1469- inkscape:collect="always"
1470- id="filter3502"
1471- x="-0.044082869"
1472- width="1.0881657"
1473- y="-0.19633912"
1474- height="1.3926782">
1475- <feGaussianBlur
1476- inkscape:collect="always"
1477- stdDeviation="1.4852688"
1478- id="feGaussianBlur3504" />
1479- </filter>
1480- <linearGradient
1481- id="linearGradient3661">
1482- <stop
1483- style="stop-color:#3399ff;stop-opacity:1;"
1484- offset="0"
1485- id="stop3663" />
1486- <stop
1487- id="stop3675"
1488- offset="0.5"
1489- style="stop-color:#3399ff;stop-opacity:1;" />
1490- <stop
1491- style="stop-color:#3399ff;stop-opacity:0.68627451;"
1492- offset="0.75"
1493- id="stop3685" />
1494- <stop
1495- id="stop3687"
1496- offset="0.875"
1497- style="stop-color:#3399ff;stop-opacity:0.52941176;" />
1498- <stop
1499- style="stop-color:#3399ff;stop-opacity:0.37719297;"
1500- offset="1"
1501- id="stop3665" />
1502- </linearGradient>
1503- <linearGradient
1504- inkscape:collect="always"
1505- xlink:href="#linearGradient3661"
1506- id="linearGradient3725"
1507- gradientUnits="userSpaceOnUse"
1508- x1="129.75728"
1509- y1="658.44305"
1510- x2="232.12813"
1511- y2="657.89764" />
1512- <clipPath
1513- clipPathUnits="userSpaceOnUse"
1514- id="clipPath3545">
1515- <rect
1516- 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"
1517- id="rect3547"
1518- width="188.70778"
1519- height="271.60831"
1520- x="83.991325"
1521- y="571.32098" />
1522- </clipPath>
1523- <linearGradient
1524- id="linearGradient3677">
1525- <stop
1526- style="stop-color:#ececec;stop-opacity:1"
1527- offset="0"
1528- id="stop3679" />
1529- <stop
1530- id="stop3689"
1531- offset="0.5"
1532- style="stop-color:#ffffff;stop-opacity:0.49803922;" />
1533- <stop
1534- style="stop-color:#ececec;stop-opacity:1"
1535- offset="1"
1536- id="stop3681" />
1537- </linearGradient>
1538- <linearGradient
1539- inkscape:collect="always"
1540- xlink:href="#linearGradient3677"
1541- id="linearGradient3723"
1542- gradientUnits="userSpaceOnUse"
1543- x1="115.46449"
1544- y1="774.37683"
1545- x2="235.97925"
1546- y2="775.91943" />
1547- <inkscape:perspective
1548- sodipodi:type="inkscape:persp3d"
1549- inkscape:vp_x="0 : 32 : 1"
1550- inkscape:vp_y="0 : 1000 : 0"
1551- inkscape:vp_z="64 : 32 : 1"
1552- inkscape:persp3d-origin="32 : 21.333333 : 1"
1553- id="perspective3735" />
1554- </defs>
1555- <sodipodi:namedview
1556- id="base"
1557- pagecolor="#ffffff"
1558- bordercolor="#666666"
1559- borderopacity="1.0"
1560- inkscape:pageopacity="0.0"
1561- inkscape:pageshadow="2"
1562- inkscape:zoom="1.9445436"
1563- inkscape:cx="21.790111"
1564- inkscape:cy="76.61186"
1565- inkscape:current-layer="layer1"
1566- showgrid="true"
1567- inkscape:document-units="px"
1568- inkscape:grid-bbox="true"
1569- inkscape:window-width="640"
1570- inkscape:window-height="628"
1571- inkscape:window-x="506"
1572- inkscape:window-y="22" />
1573- <metadata
1574- id="metadata3732">
1575- <rdf:RDF>
1576- <cc:Work
1577- rdf:about="">
1578- <dc:format>image/svg+xml</dc:format>
1579- <dc:type
1580- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
1581- </cc:Work>
1582- </rdf:RDF>
1583- </metadata>
1584- <g
1585- id="layer1"
1586- inkscape:label="Layer 1"
1587- inkscape:groupmode="layer">
1588- <g
1589- id="g3711"
1590- transform="matrix(0.1214085,0.1214085,-0.1214085,0.1214085,61.002827,-86.966337)"
1591- inkscape:transform-center-x="-16.513266"
1592- inkscape:transform-center-y="-36.581756">
1593- <path
1594- id="path3396"
1595- 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"
1596- inkscape:transform-center-y="41.974325"
1597- inkscape:transform-center-x="-9.0994502"
1598- 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" />
1599- <path
1600- id="path3400"
1601- 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"
1602- inkscape:transform-center-y="41.974325"
1603- inkscape:transform-center-x="-9.0994502"
1604- 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" />
1605- <path
1606- id="path3402"
1607- 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"
1608- inkscape:transform-center-y="41.974325"
1609- inkscape:transform-center-x="-9.0994502"
1610- 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" />
1611- <path
1612- transform="matrix(0.9856192,0,0,1,199.34766,-2.7713095)"
1613- clip-path="url(#clipPath3545)"
1614- sodipodi:nodetypes="cccccccccccccccccccccc"
1615- id="path3534"
1616- 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"
1617- 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" />
1618- <path
1619- id="path3508"
1620- 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"
1621- 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" />
1622- <path
1623- 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"
1624- 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"
1625- id="path3659"
1626- sodipodi:nodetypes="cccccccccccccccccccccc"
1627- clip-path="url(#clipPath3545)"
1628- transform="matrix(0.9856192,0,0,1,199.34766,37.228691)" />
1629- <path
1630- id="path3563"
1631- 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"
1632- 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" />
1633- <path
1634- id="path3417"
1635- 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"
1636- 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" />
1637- <path
1638- id="path3404"
1639- 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"
1640- inkscape:transform-center-y="41.974325"
1641- inkscape:transform-center-x="-9.0994502"
1642- 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" />
1643- <path
1644- sodipodi:nodetypes="cccccccc"
1645- id="path3434"
1646- 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"
1647- 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)" />
1648- </g>
1649- </g>
1650-</svg>
1651
1652=== removed directory 'GTG/plugins/rtm_sync/pyrtm'
1653=== removed file 'GTG/plugins/rtm_sync/pyrtm/README'
1654--- GTG/plugins/rtm_sync/pyrtm/README 2009-08-07 04:13:36 +0000
1655+++ GTG/plugins/rtm_sync/pyrtm/README 1970-01-01 00:00:00 +0000
1656@@ -1,10 +0,0 @@
1657-======================================================================
1658-Python library for Remember The Milk API
1659-======================================================================
1660-
1661-Copyright (c) 2008 by Sridhar Ratnakumar <http://nearfar.org/>
1662-
1663-Contributors:
1664- - Mariano Draghi (cHagHi) <mariano at chaghi dot com dot ar>
1665-
1666-See app.py for examples
1667
1668=== removed file 'GTG/plugins/rtm_sync/pyrtm/__init__.py'
1669=== removed file 'GTG/plugins/rtm_sync/pyrtm/rtm.py'
1670--- GTG/plugins/rtm_sync/pyrtm/rtm.py 2010-03-17 03:55:32 +0000
1671+++ GTG/plugins/rtm_sync/pyrtm/rtm.py 1970-01-01 00:00:00 +0000
1672@@ -1,402 +0,0 @@
1673-# Python library for Remember The Milk API
1674-
1675-__author__ = 'Sridhar Ratnakumar <http://nearfar.org/>'
1676-__all__ = (
1677- 'API',
1678- 'createRTM',
1679- 'set_log_level',
1680- )
1681-
1682-
1683-import warnings
1684-import urllib
1685-import time
1686-from hashlib import md5
1687-from GTG import _
1688-
1689-warnings.simplefilter('default', ImportWarning)
1690-
1691-_use_simplejson = False
1692-try:
1693- import simplejson
1694- _use_simplejson = True
1695-except ImportError:
1696- try:
1697- from django.utils import simplejson
1698- _use_simplejson = True
1699- except ImportError:
1700- pass
1701-
1702-if not _use_simplejson:
1703- warnings.warn("simplejson module is not available, "
1704- "falling back to the internal JSON parser. "
1705- "Please consider installing the simplejson module from "
1706- "http://pypi.python.org/pypi/simplejson.", ImportWarning,
1707- stacklevel=2)
1708-
1709-#logging.basicConfig()
1710-#LOG = logging.getLogger(__name__)
1711-#LOG.setLevel(logging.INFO)
1712-
1713-SERVICE_URL = 'http://api.rememberthemilk.com/services/rest/'
1714-AUTH_SERVICE_URL = 'http://www.rememberthemilk.com/services/auth/'
1715-
1716-
1717-class RTMError(Exception): pass
1718-
1719-class RTMAPIError(RTMError): pass
1720-
1721-class AuthStateMachine(object):
1722-
1723- class NoData(RTMError): pass
1724-
1725- def __init__(self, states):
1726- self.states = states
1727- self.data = {}
1728-
1729- def dataReceived(self, state, datum):
1730- if state not in self.states:
1731- error_string = _("Invalid state")+" <%s>"
1732-
1733- raise RTMError, error_string % state
1734- self.data[state] = datum
1735-
1736- def get(self, state):
1737- if state in self.data:
1738- return self.data[state]
1739- else:
1740- raise AuthStateMachine.NoData, 'No data for <%s>' % state
1741-
1742-
1743-class RTM(object):
1744-
1745- def __init__(self, apiKey, secret, token=None):
1746- self.apiKey = apiKey
1747- self.secret = secret
1748- self.authInfo = AuthStateMachine(['frob', 'token'])
1749-
1750- # this enables one to do 'rtm.tasks.getList()', for example
1751- for prefix, methods in API.items():
1752- setattr(self, prefix,
1753- RTMAPICategory(self, prefix, methods))
1754-
1755- if token:
1756- self.authInfo.dataReceived('token', token)
1757-
1758- def _sign(self, params):
1759- "Sign the parameters with MD5 hash"
1760- pairs = ''.join(['%s%s' % (k,v) for k,v in sortedItems(params)])
1761- return md5(self.secret+pairs).hexdigest()
1762-
1763- def get(self, **params):
1764- "Get the XML response for the passed `params`."
1765- params['api_key'] = self.apiKey
1766- params['format'] = 'json'
1767- params['api_sig'] = self._sign(params)
1768-
1769- json = openURL(SERVICE_URL, params).read()
1770-
1771- #LOG.debug("JSON response: \n%s" % json)
1772- if _use_simplejson:
1773- data = dottedDict('ROOT', simplejson.loads(json))
1774- else:
1775- data = dottedJSON(json)
1776- rsp = data.rsp
1777-
1778- if rsp.stat == 'fail':
1779- raise RTMAPIError, 'API call failed - %s (%s)' % (
1780- rsp.err.msg, rsp.err.code)
1781- else:
1782- return rsp
1783-
1784- def getNewFrob(self):
1785- rsp = self.get(method='rtm.auth.getFrob')
1786- self.authInfo.dataReceived('frob', rsp.frob)
1787- return rsp.frob
1788-
1789- def getAuthURL(self):
1790- try:
1791- frob = self.authInfo.get('frob')
1792- except AuthStateMachine.NoData:
1793- frob = self.getNewFrob()
1794-
1795- params = {
1796- 'api_key': self.apiKey,
1797- 'perms' : 'delete',
1798- 'frob' : frob
1799- }
1800- params['api_sig'] = self._sign(params)
1801- return AUTH_SERVICE_URL + '?' + urllib.urlencode(params)
1802-
1803- def getToken(self):
1804- frob = self.authInfo.get('frob')
1805- rsp = self.get(method='rtm.auth.getToken', frob=frob)
1806- self.authInfo.dataReceived('token', rsp.auth.token)
1807- return rsp.auth.token
1808-
1809-class RTMAPICategory:
1810- "See the `API` structure and `RTM.__init__`"
1811-
1812- def __init__(self, rtm, prefix, methods):
1813- self.rtm = rtm
1814- self.prefix = prefix
1815- self.methods = methods
1816-
1817- def __getattr__(self, attr):
1818- if attr in self.methods:
1819- rargs, oargs = self.methods[attr]
1820- if self.prefix == 'tasksNotes':
1821- aname = 'rtm.tasks.notes.%s' % attr
1822- else:
1823- aname = 'rtm.%s.%s' % (self.prefix, attr)
1824- return lambda **params: self.callMethod(
1825- aname, rargs, oargs, **params)
1826- else:
1827- raise AttributeError, 'No such attribute: %s' % attr
1828-
1829- def callMethod(self, aname, rargs, oargs, **params):
1830- # Sanity checks
1831- for requiredArg in rargs:
1832- if requiredArg not in params:
1833- raise TypeError, 'Required parameter (%s) missing' % requiredArg
1834-
1835- for param in params:
1836- if param not in rargs + oargs:
1837- warnings.warn('Invalid parameter (%s)' % param)
1838-
1839- return self.rtm.get(method=aname,
1840- auth_token=self.rtm.authInfo.get('token'),
1841- **params)
1842-
1843-
1844-
1845-# Utility functions
1846-
1847-def sortedItems(dictionary):
1848- "Return a list of (key, value) sorted based on keys"
1849- keys = dictionary.keys()
1850- keys.sort()
1851- for key in keys:
1852- yield key, dictionary[key]
1853-
1854-def openURL(url, queryArgs=None):
1855- if queryArgs:
1856- url = url + '?' + urllib.urlencode(queryArgs)
1857- #LOG.debug("URL> %s", url)
1858- return urllib.urlopen(url)
1859-
1860-class dottedDict(object):
1861- """Make dictionary items accessible via the object-dot notation."""
1862-
1863- def __init__(self, name, dictionary):
1864- self._name = name
1865-
1866- if type(dictionary) is dict:
1867- for key, value in dictionary.items():
1868- if type(value) is dict:
1869- value = dottedDict(key, value)
1870- elif type(value) in (list, tuple) and key != 'tag':
1871- value = [dottedDict('%s_%d' % (key, i), item)
1872- for i, item in indexed(value)]
1873- setattr(self, key, value)
1874- else:
1875- raise ValueError, 'not a dict: %s' % dictionary
1876-
1877- def __repr__(self):
1878- children = [c for c in dir(self) if not c.startswith('_')]
1879- return 'dotted <%s> : %s' % (
1880- self._name,
1881- ', '.join(children))
1882-
1883-
1884-def safeEval(string):
1885- return eval(string, {}, {})
1886-
1887-def dottedJSON(json):
1888- return dottedDict('ROOT', safeEval(json))
1889-
1890-def indexed(seq):
1891- index = 0
1892- for item in seq:
1893- yield index, item
1894- index += 1
1895-
1896-
1897-# API spec
1898-
1899-API = {
1900- 'auth': {
1901- 'checkToken':
1902- [('auth_token',), ()],
1903- 'getFrob':
1904- [(), ()],
1905- 'getToken':
1906- [('frob',), ()]
1907- },
1908- 'contacts': {
1909- 'add':
1910- [('timeline', 'contact'), ()],
1911- 'delete':
1912- [('timeline', 'contact_id'), ()],
1913- 'getList':
1914- [(), ()]
1915- },
1916- 'groups': {
1917- 'add':
1918- [('timeline', 'group'), ()],
1919- 'addContact':
1920- [('timeline', 'group_id', 'contact_id'), ()],
1921- 'delete':
1922- [('timeline', 'group_id'), ()],
1923- 'getList':
1924- [(), ()],
1925- 'removeContact':
1926- [('timeline', 'group_id', 'contact_id'), ()],
1927- },
1928- 'lists': {
1929- 'add':
1930- [('timeline', 'name',), ('filter',)],
1931- 'archive':
1932- [('timeline', 'list_id'), ()],
1933- 'delete':
1934- [('timeline', 'list_id'), ()],
1935- 'getList':
1936- [(), ()],
1937- 'setDefaultList':
1938- [('timeline'), ('list_id')],
1939- 'setName':
1940- [('timeline', 'list_id', 'name'), ()],
1941- 'unarchive':
1942- [('timeline',), ('list_id',)]
1943- },
1944- 'locations': {
1945- 'getList':
1946- [(), ()]
1947- },
1948- 'reflection': {
1949- 'getMethodInfo':
1950- [('methodName',), ()],
1951- 'getMethods':
1952- [(), ()]
1953- },
1954- 'settings': {
1955- 'getList':
1956- [(), ()]
1957- },
1958- 'tasks': {
1959- 'add':
1960- [('timeline', 'name',), ('list_id', 'parse',)],
1961- 'addTags':
1962- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
1963- ()],
1964- 'complete':
1965- [('timeline', 'list_id', 'taskseries_id', 'task_id',), ()],
1966- 'delete':
1967- [('timeline', 'list_id', 'taskseries_id', 'task_id'), ()],
1968- 'getList':
1969- [(),
1970- ('list_id', 'filter', 'last_sync')],
1971- 'movePriority':
1972- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'direction'),
1973- ()],
1974- 'moveTo':
1975- [('timeline', 'from_list_id', 'to_list_id', 'taskseries_id', 'task_id'),
1976- ()],
1977- 'postpone':
1978- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1979- ()],
1980- 'removeTags':
1981- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'tags'),
1982- ()],
1983- 'setDueDate':
1984- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1985- ('due', 'has_due_time', 'parse')],
1986- 'setEstimate':
1987- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1988- ('estimate',)],
1989- 'setLocation':
1990- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1991- ('location_id',)],
1992- 'setName':
1993- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'name'),
1994- ()],
1995- 'setPriority':
1996- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
1997- ('priority',)],
1998- 'setRecurrence':
1999- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
2000- ('repeat',)],
2001- 'setTags':
2002- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
2003- ('tags',)],
2004- 'setURL':
2005- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
2006- ('url',)],
2007- 'uncomplete':
2008- [('timeline', 'list_id', 'taskseries_id', 'task_id'),
2009- ()],
2010- },
2011- 'tasksNotes': {
2012- 'add':
2013- [('timeline', 'list_id', 'taskseries_id', 'task_id', 'note_title', 'note_text'), ()],
2014- 'delete':
2015- [('timeline', 'note_id'), ()],
2016- 'edit':
2017- [('timeline', 'note_id', 'note_title', 'note_text'), ()]
2018- },
2019- 'test': {
2020- 'echo':
2021- [(), ()],
2022- 'login':
2023- [(), ()]
2024- },
2025- 'time': {
2026- 'convert':
2027- [('to_timezone',), ('from_timezone', 'to_timezone', 'time')],
2028- 'parse':
2029- [('text',), ('timezone', 'dateformat')]
2030- },
2031- 'timelines': {
2032- 'create':
2033- [(), ()]
2034- },
2035- 'timezones': {
2036- 'getList':
2037- [(), ()]
2038- },
2039- 'transactions': {
2040- 'undo':
2041- [('timeline', 'transaction_id'), ()]
2042- },
2043- }
2044-
2045-def createRTM(apiKey, secret, token=None):
2046- rtm = RTM(apiKey, secret, token)
2047-# if token is None:
2048-# print 'No token found'
2049-# print 'Give me access here:', rtm.getAuthURL()
2050-# raw_input('Press enter once you gave access')
2051-# print 'Note down this token for future use:', rtm.getToken()
2052-
2053- return rtm
2054-
2055-def test(apiKey, secret, token=None):
2056- rtm = createRTM(apiKey, secret, token)
2057-
2058- rspTasks = rtm.tasks.getList(filter='dueWithin:"1 week of today"')
2059- print [t.name for t in rspTasks.tasks.list.taskseries]
2060- print rspTasks.tasks.list.id
2061-
2062- rspLists = rtm.lists.getList()
2063- # print rspLists.lists.list
2064- print [(x.name, x.id) for x in rspLists.lists.list]
2065-
2066-def set_log_level(level):
2067- '''Sets the log level of the logger used by the module.
2068-
2069- >>> import rtm
2070- >>> import logging
2071- >>> rtm.set_log_level(logging.INFO)
2072- '''
2073-
2074- #LOG.setLevel(level)
2075
2076=== added file 'data/icons/hicolor/scalable/apps/backend_rtm.png'
2077Binary 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-09-03 23:47:39 +0000 differ

Subscribers

People subscribed via source and target branches

to status/vote changes: