Merge lp:~gtg-user/gtg/rtm-backend into lp:~gtg/gtg/old-trunk
- rtm-backend
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gtg developers | Pending | ||
Review via email: mp+33610@code.launchpad.net |
Commit message
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' |
2077 | 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-09-03 23:47:39 +0000 differ |