Merge lp:~gtg-user/gtg/backends-utils into lp:~gtg/gtg/old-trunk
- backends-utils
- Merge into old-trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~gtg-user/gtg/backends-utils |
Merge into: | lp:~gtg/gtg/old-trunk |
Prerequisite: | lp:~gtg-user/gtg/backends-window |
Diff against target: |
1207 lines (+1147/-0) 11 files modified
CHANGELOG (+1/-0) GTG/backends/periodicimportbackend.py (+90/-0) GTG/backends/syncengine.py (+288/-0) GTG/tests/test_bidict.py (+79/-0) GTG/tests/test_dates.py (+43/-0) GTG/tests/test_syncengine.py (+189/-0) GTG/tests/test_syncmeme.py (+59/-0) GTG/tests/test_twokeydict.py (+98/-0) GTG/tools/bidict.py (+112/-0) GTG/tools/twokeydict.py (+135/-0) GTG/tools/watchdog.py (+53/-0) |
To merge this branch: | bzr merge lp:~gtg-user/gtg/backends-utils |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gtg developers | Pending | ||
Review via email: mp+32278@code.launchpad.net |
This proposal has been superseded by a proposal from 2010-08-13.
Commit message
Description of the change
This branch contains all the common utils used by the backends that are not the default one.
Mainly, it contains code for:
- telling if a remote task is new, has to be updated or removed (it's a standalone library)
- getting remote tasks in polling
- a watchdog for stalling functions
The code contained in this merge isn't used by "Trunk" GTG, but it will be as backends are merged: this is why you won't see any difference in GTG's behavior now.
Tests and documentation for these parts is here too.
(lp:~gtg-user/gtg/backends-window should be merged before this one. Some file needed by both are just there to review).
- 877. By Luca Invernizzi
-
merge with trunk
- 878. By Luca Invernizzi
-
handy initializer for SyncMeme
- 879. By Luca Invernizzi
-
updated changelog
- 880. By Luca Invernizzi
-
merge with trunk
- 881. By Luca Invernizzi
-
disallowing concurrent importing
- 882. By Luca Invernizzi
-
cherrypicking from my development branch
- 883. By Luca Invernizzi
-
more responsiveness in periodic imports
- 884. By Luca Invernizzi
-
merge w/ trunk
Unmerged revisions
Preview Diff
1 | === modified file 'CHANGELOG' |
2 | --- CHANGELOG 2010-08-04 00:30:22 +0000 |
3 | +++ CHANGELOG 2010-08-13 23:43:06 +0000 |
4 | @@ -4,6 +4,7 @@ |
5 | * Fixed bug with data consistency #579189, by Marko Kevac |
6 | * Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij |
7 | * Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul |
8 | + * Added utilities for complex backends by Luca Invernizzi |
9 | |
10 | 2010-03-01 Getting Things GNOME! 0.2.2 |
11 | * Autostart on login, by Luca Invernizzi |
12 | |
13 | === added file 'GTG/backends/periodicimportbackend.py' |
14 | --- GTG/backends/periodicimportbackend.py 1970-01-01 00:00:00 +0000 |
15 | +++ GTG/backends/periodicimportbackend.py 2010-08-13 23:43:06 +0000 |
16 | @@ -0,0 +1,90 @@ |
17 | +# -*- coding: utf-8 -*- |
18 | +# ----------------------------------------------------------------------------- |
19 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
20 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
21 | +# |
22 | +# This program is free software: you can redistribute it and/or modify it under |
23 | +# the terms of the GNU General Public License as published by the Free Software |
24 | +# Foundation, either version 3 of the License, or (at your option) any later |
25 | +# version. |
26 | +# |
27 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
28 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
29 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
30 | +# details. |
31 | +# |
32 | +# You should have received a copy of the GNU General Public License along with |
33 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
34 | +# ----------------------------------------------------------------------------- |
35 | + |
36 | +''' |
37 | +Contains PeriodicImportBackend, a GenericBackend specialized for checking the |
38 | +remote backend in polling. |
39 | +''' |
40 | + |
41 | +import threading |
42 | + |
43 | +from GTG.backends.genericbackend import GenericBackend |
44 | +from GTG.backends.backendsignals import BackendSignals |
45 | +from GTG.tools.interruptible import interruptible |
46 | + |
47 | + |
48 | + |
49 | +class PeriodicImportBackend(GenericBackend): |
50 | + ''' |
51 | + This class can be used in place of GenericBackend when a periodic import is |
52 | + necessary, as the remote service providing tasks does not signals the |
53 | + changes. |
54 | + To use this, only two things are necessary: |
55 | + - using do_periodic_import instead of start_get_tasks |
56 | + - having in _static_parameters a "period" key, as in |
57 | + "period": { \ |
58 | + GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \ |
59 | + GenericBackend.PARAM_DEFAULT_VALUE: 2, }, |
60 | + This specifies the time that must pass between consecutive imports |
61 | + (in minutes) |
62 | + ''' |
63 | + |
64 | + @interruptible |
65 | + def start_get_tasks(self): |
66 | + ''' |
67 | + This function launches the first periodic import, and schedules the |
68 | + next ones. |
69 | + ''' |
70 | + try: |
71 | + if self.import_timer: |
72 | + self.import_timer.cancel() |
73 | + except: |
74 | + pass |
75 | + self._start_get_tasks() |
76 | + self.cancellation_point() |
77 | + if self.is_enabled() == False: |
78 | + return |
79 | + self.import_timer = threading.Timer( \ |
80 | + self._parameters['period'] * 60.0, \ |
81 | + self.start_get_tasks) |
82 | + self.import_timer.start() |
83 | + |
84 | + def _start_get_tasks(self): |
85 | + ''' |
86 | + This function executes an imports and schedules the next |
87 | + ''' |
88 | + self.cancellation_point() |
89 | + BackendSignals().backend_sync_started(self.get_id()) |
90 | + self.do_periodic_import() |
91 | + BackendSignals().backend_sync_ended(self.get_id()) |
92 | + |
93 | + def quit(self, disable = False): |
94 | + ''' |
95 | + Called when GTG quits or disconnects the backend. |
96 | + ''' |
97 | + super(PeriodicImportBackend, self).quit(disable) |
98 | + try: |
99 | + self.import_timer.cancel() |
100 | + except Exception: |
101 | + pass |
102 | + try: |
103 | + self.import_timer.join() |
104 | + except Exception: |
105 | + pass |
106 | + |
107 | |
108 | === added file 'GTG/backends/syncengine.py' |
109 | --- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000 |
110 | +++ GTG/backends/syncengine.py 2010-08-13 23:43:06 +0000 |
111 | @@ -0,0 +1,288 @@ |
112 | +# -*- coding: utf-8 -*- |
113 | +# ----------------------------------------------------------------------------- |
114 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
115 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
116 | +# |
117 | +# This program is free software: you can redistribute it and/or modify it under |
118 | +# the terms of the GNU General Public License as published by the Free Software |
119 | +# Foundation, either version 3 of the License, or (at your option) any later |
120 | +# version. |
121 | +# |
122 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
123 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
124 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
125 | +# details. |
126 | +# |
127 | +# You should have received a copy of the GNU General Public License along with |
128 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
129 | +# ----------------------------------------------------------------------------- |
130 | + |
131 | +''' |
132 | +This library deals with synchronizing two sets of objects. |
133 | +It works like this: |
134 | + - We have two sets of generic objects (local and remote) |
135 | + - We present one object of either one of the sets and ask the library what's |
136 | + the state of its synchronization |
137 | + - the library will tell us if we need to add a clone object in the other set, |
138 | + update it or, if the other one has been removed, remove also this one |
139 | +''' |
140 | +from GTG.tools.twokeydict import TwoKeyDict |
141 | + |
142 | + |
143 | +TYPE_LOCAL = "local" |
144 | +TYPE_REMOTE = "remote" |
145 | + |
146 | + |
147 | + |
148 | +class SyncMeme(object): |
149 | + ''' |
150 | + A SyncMeme is the object storing the data needed to keep track of the state |
151 | + of two objects synchronization. |
152 | + This basic version, that can be expanded as needed by the code using the |
153 | + SyncEngine, just stores the modified date and time of the last |
154 | + synchronization for both objects (local and remote) |
155 | + ''' |
156 | + #NOTE: Checking objects CRCs would make this check nicer, as we could know |
157 | + # if the object was really changed, or it has just updated its |
158 | + # modified time (invernizzi) |
159 | + |
160 | + def __init__(self, |
161 | + local_modified = None, |
162 | + remote_modified = None, |
163 | + origin = None): |
164 | + ''' |
165 | + Creates a new SyncMeme, updating the modified times for both the |
166 | + local and remote objects, and sets the given origin. |
167 | + If any of the parameters is set to None, it's ignored. |
168 | + |
169 | + @param local_modified: the modified time for the local object |
170 | + @param remote_modified: the modified time for the remote object |
171 | + @param origin: an object that identifies whether the local or the remote is |
172 | + the original object, the other one being a copy. |
173 | + ''' |
174 | + if local_modified != None: |
175 | + self.set_local_last_modified(local_modified) |
176 | + if remote_modified != None: |
177 | + self.set_remote_last_modified(remote_modified) |
178 | + if origin != None: |
179 | + self.set_origin(origin) |
180 | + |
181 | + def set_local_last_modified(self, modified_datetime): |
182 | + ''' |
183 | + Setter function for the local object modified datetime. |
184 | + |
185 | + @param modified_datetime: the local object modified datetime |
186 | + ''' |
187 | + self.local_last_modified = modified_datetime |
188 | + |
189 | + def get_local_last_modified(self): |
190 | + ''' |
191 | + Getter function for the local object modified datetime. |
192 | + ''' |
193 | + return self.local_last_modified |
194 | + |
195 | + def set_remote_last_modified(self, modified_datetime): |
196 | + ''' |
197 | + Setter function for the remote object modified datetime. |
198 | + |
199 | + @param modified_datetime: the remote object modified datetime |
200 | + ''' |
201 | + self.remote_last_modified = modified_datetime |
202 | + |
203 | + def get_remote_last_modified(self): |
204 | + ''' |
205 | + Getter function for the remote object modified datetime. |
206 | + ''' |
207 | + return self.remote_last_modified |
208 | + |
209 | + def which_is_newest(self, local_modified, remote_modified): |
210 | + ''' |
211 | + Given the updated modified time for both the local and the remote |
212 | + objects, it checks them against the stored modified times and |
213 | + then against each other. |
214 | + |
215 | + @returns string: "local"- if the local object has been modified and its |
216 | + the newest |
217 | + "remote" - the same for the remote object |
218 | + None - if no object modified time is newer than the |
219 | + stored one (the objects have not been modified) |
220 | + ''' |
221 | + if local_modified <= self.local_last_modified and \ |
222 | + remote_modified <= self.remote_last_modified: |
223 | + return None |
224 | + if local_modified > remote_modified: |
225 | + return "local" |
226 | + else: |
227 | + return "remote" |
228 | + |
229 | + def get_origin(self): |
230 | + ''' |
231 | + Returns the name of the source that firstly presented the object |
232 | + ''' |
233 | + return self.origin |
234 | + |
235 | + def set_origin(self, origin): |
236 | + ''' |
237 | + Sets the source that presented the object for the first time. This |
238 | + source holds the original object, while the other holds the copy. |
239 | + This can be useful in the case of "lost syncability" (see the SyncEngine |
240 | + for an explaination). |
241 | + |
242 | + @param origin: object representing the source |
243 | + ''' |
244 | + self.origin = origin |
245 | + |
246 | + |
247 | + |
248 | +class SyncMemes(TwoKeyDict): |
249 | + ''' |
250 | + A TwoKeyDict, with just the names changed to be better understandable. |
251 | + The meaning of these names is explained in the SyncEngine class description. |
252 | + It's used to store a set of SyncMeme objects, each one keeping storing all |
253 | + the data needed to keep track of a single relationship. |
254 | + ''' |
255 | + |
256 | + |
257 | + get_remote_id = TwoKeyDict._get_secondary_key |
258 | + get_local_id = TwoKeyDict._get_primary_key |
259 | + remove_local_id = TwoKeyDict._remove_by_primary |
260 | + remove_remote_id = TwoKeyDict._remove_by_secondary |
261 | + get_meme_from_local_id = TwoKeyDict._get_by_primary |
262 | + get_meme_from_remote_id = TwoKeyDict._get_by_secondary |
263 | + get_all_local = TwoKeyDict._get_all_primary_keys |
264 | + get_all_remote = TwoKeyDict._get_all_secondary_keys |
265 | + |
266 | + |
267 | + |
268 | +class SyncEngine(object): |
269 | + ''' |
270 | + The SyncEngine is an object useful in keeping two sets of objects |
271 | + synchronized. |
272 | + One set is called the Local set, the other is the Remote one. |
273 | + It stores the state of the synchronization and the latest state of each |
274 | + object. |
275 | + When asked, it can tell if a couple of related objects are up to date in the |
276 | + sync and, if not, which one must be updated. |
277 | + |
278 | + It stores the state of each relationship in a series of SyncMeme. |
279 | + ''' |
280 | + |
281 | + |
282 | + UPDATE = "update" |
283 | + REMOVE = "remove" |
284 | + ADD = "add" |
285 | + LOST_SYNCABILITY = "lost syncability" |
286 | + |
287 | + def __init__(self): |
288 | + ''' |
289 | + Initializes the storage of object relationships. |
290 | + ''' |
291 | + self.sync_memes = SyncMemes() |
292 | + |
293 | + def _analyze_element(self, |
294 | + element_id, |
295 | + is_local, |
296 | + has_local, |
297 | + has_remote, |
298 | + is_syncable = True): |
299 | + ''' |
300 | + Given an object that should be synced with another one, |
301 | + it finds out about the related object, and decides whether: |
302 | + - the other object hasn't been created yet (thus must be added) |
303 | + - the other object has been deleted (thus this one must be deleted) |
304 | + - the other object is present, but either one has been changed |
305 | + |
306 | + A particular case happens if the other object is present, but the |
307 | + "is_syncable" parameter (which tells that we intend to keep these two |
308 | + objects in sync) is set to False. In this case, this function returns |
309 | + that the Syncability property has been lost. This case is interesting if |
310 | + we want to delete one of the two objects (the one that has been cloned |
311 | + from the original). |
312 | + |
313 | + @param element_id: the id of the element we're analysing. |
314 | + @param is_local: True if the element analysed is the local one (not the |
315 | + remote) |
316 | + @param has_local: function that accepts an id of the local set and |
317 | + returns True if the element is present |
318 | + @param has_remote: function that accepts an id of the remote set and |
319 | + returns True if the element is present |
320 | + @param is_syncable: explained above |
321 | + @returns string: one of self.UPDATE, self.ADD, self.REMOVE, |
322 | + self.LOST_SYNCABILITY |
323 | + ''' |
324 | + if is_local: |
325 | + get_other_id = self.sync_memes.get_remote_id |
326 | + is_task_present = has_remote |
327 | + else: |
328 | + get_other_id = self.sync_memes.get_local_id |
329 | + is_task_present = has_local |
330 | + |
331 | + try: |
332 | + other_id = get_other_id(element_id) |
333 | + if is_task_present(other_id): |
334 | + if is_syncable: |
335 | + return self.UPDATE, other_id |
336 | + else: |
337 | + return self.LOST_SYNCABILITY, other_id |
338 | + else: |
339 | + return self.REMOVE, None |
340 | + except KeyError: |
341 | + if is_syncable: |
342 | + return self.ADD, None |
343 | + return None, None |
344 | + |
345 | + def analyze_local_id(self, element_id, *other_args): |
346 | + ''' |
347 | + Shortcut to call _analyze_element for a local element |
348 | + ''' |
349 | + return self._analyze_element(element_id, True, *other_args) |
350 | + |
351 | + def analyze_remote_id(self, element_id, *other_args): |
352 | + ''' |
353 | + Shortcut to call _analyze_element for a remote element |
354 | + ''' |
355 | + return self._analyze_element(element_id, False, *other_args) |
356 | + |
357 | + def record_relationship(self, local_id, remote_id, meme): |
358 | + ''' |
359 | + Records that an object from the local set is related with one a remote |
360 | + set. |
361 | + |
362 | + @param local_id: the id of the local task |
363 | + @param remote_id: the id of the remote task |
364 | + @param meme: the SyncMeme that keeps track of the relationship |
365 | + ''' |
366 | + triplet = (local_id, remote_id, meme) |
367 | + self.sync_memes.add(triplet) |
368 | + |
369 | + def break_relationship(self, local_id = None, remote_id = None): |
370 | + ''' |
371 | + breaks a relationship between two objects. |
372 | + Only one of the two parameters is necessary to identify the |
373 | + relationship. |
374 | + |
375 | + @param local_id: the id of the local task |
376 | + @param remote_id: the id of the remote task |
377 | + ''' |
378 | + if local_id: |
379 | + self.sync_memes.remove_local_id(local_id) |
380 | + elif remote_id: |
381 | + self.sync_memes.remove_remote_id(remote_id) |
382 | + |
383 | + def __getattr__(self, attr): |
384 | + ''' |
385 | + The functions listed here are passed directly to the SyncMeme object |
386 | + |
387 | + @param attr: a function name among the ones listed here |
388 | + @returns object: the function return object. |
389 | + ''' |
390 | + if attr in ['get_remote_id', |
391 | + 'get_local_id', |
392 | + 'get_meme_from_local_id', |
393 | + 'get_meme_from_remote_id', |
394 | + 'get_all_local', |
395 | + 'get_all_remote']: |
396 | + return getattr(self.sync_memes, attr) |
397 | + else: |
398 | + raise AttributeError |
399 | + |
400 | |
401 | === added file 'GTG/tests/test_bidict.py' |
402 | --- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000 |
403 | +++ GTG/tests/test_bidict.py 2010-08-13 23:43:06 +0000 |
404 | @@ -0,0 +1,79 @@ |
405 | +# -*- coding: utf-8 -*- |
406 | +# ----------------------------------------------------------------------------- |
407 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
408 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
409 | +# |
410 | +# This program is free software: you can redistribute it and/or modify it under |
411 | +# the terms of the GNU General Public License as published by the Free Software |
412 | +# Foundation, either version 3 of the License, or (at your option) any later |
413 | +# version. |
414 | +# |
415 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
416 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
417 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
418 | +# details. |
419 | +# |
420 | +# You should have received a copy of the GNU General Public License along with |
421 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
422 | +# ----------------------------------------------------------------------------- |
423 | + |
424 | +''' |
425 | +Tests for the diDict class |
426 | +''' |
427 | + |
428 | +import unittest |
429 | +import uuid |
430 | + |
431 | +from GTG.tools.bidict import BiDict |
432 | + |
433 | + |
434 | + |
435 | +class TestBiDict(unittest.TestCase): |
436 | + ''' |
437 | + Tests for the BiDict object. |
438 | + ''' |
439 | + |
440 | + |
441 | + def test_add_and_gets(self): |
442 | + ''' |
443 | + Test for the __init__, _get_by_first, _get_by_second function |
444 | + ''' |
445 | + pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)] |
446 | + bidict = BiDict(*pairs) |
447 | + for pair in pairs: |
448 | + self.assertEqual(bidict._get_by_first(pair[0]), pair[1]) |
449 | + self.assertEqual(bidict._get_by_second(pair[1]), pair[0]) |
450 | + |
451 | + def test_remove_by_first_or_second(self): |
452 | + ''' |
453 | + Tests for removing elements from the biDict |
454 | + ''' |
455 | + pair_first = (1, 'one') |
456 | + pair_second = (2, 'two') |
457 | + bidict = BiDict(pair_first, pair_second) |
458 | + bidict._remove_by_first(pair_first[0]) |
459 | + bidict._remove_by_second(pair_second[1]) |
460 | + missing_first = 0 |
461 | + missing_second = 0 |
462 | + try: |
463 | + bidict._get_by_first(pair_first[0]) |
464 | + except KeyError: |
465 | + missing_first += 1 |
466 | + try: |
467 | + bidict._get_by_first(pair_second[0]) |
468 | + except KeyError: |
469 | + missing_first += 1 |
470 | + try: |
471 | + bidict._get_by_second(pair_first[1]) |
472 | + except KeyError: |
473 | + missing_second += 1 |
474 | + try: |
475 | + bidict._get_by_second(pair_second[1]) |
476 | + except KeyError: |
477 | + missing_second += 1 |
478 | + self.assertEqual(missing_first, 2) |
479 | + self.assertEqual(missing_second, 2) |
480 | + |
481 | +def test_suite(): |
482 | + return unittest.TestLoader().loadTestsFromTestCase(TestBiDict) |
483 | + |
484 | |
485 | === added file 'GTG/tests/test_dates.py' |
486 | --- GTG/tests/test_dates.py 1970-01-01 00:00:00 +0000 |
487 | +++ GTG/tests/test_dates.py 2010-08-13 23:43:06 +0000 |
488 | @@ -0,0 +1,43 @@ |
489 | +# -*- coding: utf-8 -*- |
490 | +# ----------------------------------------------------------------------------- |
491 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
492 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
493 | +# |
494 | +# This program is free software: you can redistribute it and/or modify it under |
495 | +# the terms of the GNU General Public License as published by the Free Software |
496 | +# Foundation, either version 3 of the License, or (at your option) any later |
497 | +# version. |
498 | +# |
499 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
500 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
501 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
502 | +# details. |
503 | +# |
504 | +# You should have received a copy of the GNU General Public License along with |
505 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
506 | +# ----------------------------------------------------------------------------- |
507 | + |
508 | +''' |
509 | +Tests for the various Date classes |
510 | +''' |
511 | + |
512 | +import unittest |
513 | + |
514 | +from GTG.tools.dates import get_canonical_date |
515 | + |
516 | +class TestDates(unittest.TestCase): |
517 | + ''' |
518 | + Tests for the various Date classes |
519 | + ''' |
520 | + |
521 | + def test_get_canonical_date(self): |
522 | + ''' |
523 | + Tests for "get_canonical_date" |
524 | + ''' |
525 | + for str in ["1985-03-29", "now", "soon", "later", ""]: |
526 | + date = get_canonical_date(str) |
527 | + self.assertEqual(date.__str__(), str) |
528 | + |
529 | +def test_suite(): |
530 | + return unittest.TestLoader().loadTestsFromTestCase(TestDates) |
531 | + |
532 | |
533 | === added file 'GTG/tests/test_syncengine.py' |
534 | --- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000 |
535 | +++ GTG/tests/test_syncengine.py 2010-08-13 23:43:06 +0000 |
536 | @@ -0,0 +1,189 @@ |
537 | +# -*- coding: utf-8 -*- |
538 | +# ----------------------------------------------------------------------------- |
539 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
540 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
541 | +# |
542 | +# This program is free software: you can redistribute it and/or modify it under |
543 | +# the terms of the GNU General Public License as published by the Free Software |
544 | +# Foundation, either version 3 of the License, or (at your option) any later |
545 | +# version. |
546 | +# |
547 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
548 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
549 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
550 | +# details. |
551 | +# |
552 | +# You should have received a copy of the GNU General Public License along with |
553 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
554 | +# ----------------------------------------------------------------------------- |
555 | + |
556 | +''' |
557 | +Tests for the SyncEngine class |
558 | +''' |
559 | + |
560 | +import unittest |
561 | +import uuid |
562 | + |
563 | +from GTG.backends.syncengine import SyncEngine |
564 | + |
565 | + |
566 | + |
567 | +class TestSyncEngine(unittest.TestCase): |
568 | + ''' |
569 | + Tests for the SyncEngine object. |
570 | + ''' |
571 | + |
572 | + def setUp(self): |
573 | + self.ftp_local = FakeTaskProvider() |
574 | + self.ftp_remote = FakeTaskProvider() |
575 | + self.sync_engine = SyncEngine() |
576 | + |
577 | + def test_analyze_element_and_record_and_break_relationship(self): |
578 | + ''' |
579 | + Test for the _analyze_element, analyze_remote_id, analyze_local_id, |
580 | + record_relationship, break_relationship |
581 | + ''' |
582 | + #adding a new local task |
583 | + local_id = uuid.uuid4() |
584 | + self.ftp_local.fake_add_task(local_id) |
585 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
586 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
587 | + (SyncEngine.ADD, None)) |
588 | + #creating the related remote task |
589 | + remote_id = uuid.uuid4() |
590 | + self.ftp_remote.fake_add_task(remote_id) |
591 | + #informing the sync_engine about that |
592 | + self.sync_engine.record_relationship(local_id, remote_id, object()) |
593 | + #verifying that it understood that |
594 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
595 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
596 | + (SyncEngine.UPDATE, remote_id)) |
597 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
598 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
599 | + (SyncEngine.UPDATE, local_id)) |
600 | + #and not the reverse |
601 | + self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \ |
602 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
603 | + (SyncEngine.ADD, None)) |
604 | + self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \ |
605 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
606 | + (SyncEngine.ADD, None)) |
607 | + #now we remove the remote task |
608 | + self.ftp_remote.fake_remove_task(remote_id) |
609 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
610 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
611 | + (SyncEngine.REMOVE, None)) |
612 | + self.sync_engine.break_relationship(local_id = local_id) |
613 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
614 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
615 | + (SyncEngine.ADD, None)) |
616 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
617 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
618 | + (SyncEngine.ADD, None)) |
619 | + #we add them back and remove giving the remote id as key to find what to |
620 | + #delete |
621 | + self.ftp_local.fake_add_task(local_id) |
622 | + self.ftp_remote.fake_add_task(remote_id) |
623 | + self.ftp_remote.fake_remove_task(remote_id) |
624 | + self.sync_engine.record_relationship(local_id, remote_id, object) |
625 | + self.sync_engine.break_relationship(remote_id = remote_id) |
626 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
627 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
628 | + (SyncEngine.ADD, None)) |
629 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
630 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
631 | + (SyncEngine.ADD, None)) |
632 | + |
633 | + def test_syncability(self): |
634 | + ''' |
635 | + Test for the _analyze_element, analyze_remote_id, analyze_local_id. |
636 | + Checks that the is_syncable parameter is used correctly |
637 | + ''' |
638 | + #adding a new local task unsyncable |
639 | + local_id = uuid.uuid4() |
640 | + self.ftp_local.fake_add_task(local_id) |
641 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
642 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
643 | + False), \ |
644 | + (None, None)) |
645 | + #adding a new local task, syncable |
646 | + local_id = uuid.uuid4() |
647 | + self.ftp_local.fake_add_task(local_id) |
648 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
649 | + self.ftp_local.has_task, self.ftp_remote.has_task), \ |
650 | + (SyncEngine.ADD, None)) |
651 | + #creating the related remote task |
652 | + remote_id = uuid.uuid4() |
653 | + self.ftp_remote.fake_add_task(remote_id) |
654 | + #informing the sync_engine about that |
655 | + self.sync_engine.record_relationship(local_id, remote_id, object()) |
656 | + #checking that it behaves correctly with established relationships |
657 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
658 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
659 | + True), \ |
660 | + (SyncEngine.UPDATE, remote_id)) |
661 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
662 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
663 | + False), \ |
664 | + (SyncEngine.LOST_SYNCABILITY, remote_id)) |
665 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
666 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
667 | + True), \ |
668 | + (SyncEngine.UPDATE, local_id)) |
669 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
670 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
671 | + False), \ |
672 | + (SyncEngine.LOST_SYNCABILITY, local_id)) |
673 | + #now we remove the remote task |
674 | + self.ftp_remote.fake_remove_task(remote_id) |
675 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
676 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
677 | + True), \ |
678 | + (SyncEngine.REMOVE, None)) |
679 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
680 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
681 | + False), \ |
682 | + (SyncEngine.REMOVE, None)) |
683 | + self.sync_engine.break_relationship(local_id = local_id) |
684 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
685 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
686 | + True), \ |
687 | + (SyncEngine.ADD, None)) |
688 | + self.assertEqual(self.sync_engine.analyze_local_id(local_id, \ |
689 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
690 | + False), \ |
691 | + (None, None)) |
692 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
693 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
694 | + True), \ |
695 | + (SyncEngine.ADD, None)) |
696 | + self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \ |
697 | + self.ftp_local.has_task, self.ftp_remote.has_task, |
698 | + False), \ |
699 | + (None, None)) |
700 | + |
701 | +def test_suite(): |
702 | + return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine) |
703 | + |
704 | + |
705 | +class FakeTaskProvider(object): |
706 | + |
707 | + def __init__(self): |
708 | + self.dic = {} |
709 | + |
710 | + def has_task(self, tid): |
711 | + return self.dic.has_key(tid) |
712 | + |
713 | +############################################################################### |
714 | +### Function with the fake_ prefix are here to assist in testing, they do not |
715 | +### need to be present in the real class |
716 | +############################################################################### |
717 | + |
718 | + def fake_add_task(self, tid): |
719 | + self.dic[tid] = "something" |
720 | + |
721 | + def fake_get_task(self, tid): |
722 | + return self.dic[tid] |
723 | + |
724 | + def fake_remove_task(self, tid): |
725 | + del self.dic[tid] |
726 | |
727 | === added file 'GTG/tests/test_syncmeme.py' |
728 | --- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000 |
729 | +++ GTG/tests/test_syncmeme.py 2010-08-13 23:43:06 +0000 |
730 | @@ -0,0 +1,59 @@ |
731 | +# -*- coding: utf-8 -*- |
732 | +# ----------------------------------------------------------------------------- |
733 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
734 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
735 | +# |
736 | +# This program is free software: you can redistribute it and/or modify it under |
737 | +# the terms of the GNU General Public License as published by the Free Software |
738 | +# Foundation, either version 3 of the License, or (at your option) any later |
739 | +# version. |
740 | +# |
741 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
742 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
743 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
744 | +# details. |
745 | +# |
746 | +# You should have received a copy of the GNU General Public License along with |
747 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
748 | +# ----------------------------------------------------------------------------- |
749 | + |
750 | +''' |
751 | +Tests for the SyncMeme class |
752 | +''' |
753 | + |
754 | +import unittest |
755 | +import datetime |
756 | + |
757 | +from GTG.backends.syncengine import SyncMeme |
758 | + |
759 | + |
760 | + |
761 | +class TestSyncMeme(unittest.TestCase): |
762 | + ''' |
763 | + Tests for the SyncEngine object. |
764 | + ''' |
765 | + |
766 | + def test_which_is_newest(self): |
767 | + ''' |
768 | + test the which_is_newest function |
769 | + |
770 | + ''' |
771 | + meme = SyncMeme() |
772 | + #tasks have not changed |
773 | + local_modified = datetime.datetime.now() |
774 | + remote_modified = datetime.datetime.now() |
775 | + meme.set_local_last_modified(local_modified) |
776 | + meme.set_remote_last_modified(remote_modified) |
777 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
778 | + remote_modified), None) |
779 | + #we update the local |
780 | + local_modified = datetime.datetime.now() |
781 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
782 | + remote_modified), 'local') |
783 | + #we update the remote |
784 | + remote_modified = datetime.datetime.now() |
785 | + self.assertEqual(meme.which_is_newest(local_modified, \ |
786 | + remote_modified), 'remote') |
787 | +def test_suite(): |
788 | + return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme) |
789 | + |
790 | |
791 | === added file 'GTG/tests/test_twokeydict.py' |
792 | --- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000 |
793 | +++ GTG/tests/test_twokeydict.py 2010-08-13 23:43:06 +0000 |
794 | @@ -0,0 +1,98 @@ |
795 | +# -*- coding: utf-8 -*- |
796 | +# ----------------------------------------------------------------------------- |
797 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
798 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
799 | +# |
800 | +# This program is free software: you can redistribute it and/or modify it under |
801 | +# the terms of the GNU General Public License as published by the Free Software |
802 | +# Foundation, either version 3 of the License, or (at your option) any later |
803 | +# version. |
804 | +# |
805 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
806 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
807 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
808 | +# details. |
809 | +# |
810 | +# You should have received a copy of the GNU General Public License along with |
811 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
812 | +# ----------------------------------------------------------------------------- |
813 | + |
814 | +''' |
815 | +Tests for the TwoKeyDict class |
816 | +''' |
817 | + |
818 | +import unittest |
819 | +import uuid |
820 | + |
821 | +from GTG.tools.twokeydict import TwoKeyDict |
822 | + |
823 | + |
824 | + |
825 | +class TestTwoKeyDict(unittest.TestCase): |
826 | + ''' |
827 | + Tests for the TwoKeyDict object. |
828 | + ''' |
829 | + |
830 | + |
831 | + def test_add_and_gets(self): |
832 | + ''' |
833 | + Test for the __init__, _get_by_first, _get_by_second function |
834 | + ''' |
835 | + triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \ |
836 | + for a in xrange(10)] |
837 | + tw_dict = TwoKeyDict(*triplets) |
838 | + for triplet in triplets: |
839 | + self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2]) |
840 | + self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2]) |
841 | + |
842 | + def test_remove_by_first_or_second(self): |
843 | + ''' |
844 | + Test for removing triplets form the TwoKeyDict |
845 | + ''' |
846 | + triplet_first = (1, 'I', 'one') |
847 | + triplet_second = (2, 'II', 'two') |
848 | + tw_dict = TwoKeyDict(triplet_first, triplet_second) |
849 | + tw_dict._remove_by_primary(triplet_first[0]) |
850 | + tw_dict._remove_by_secondary(triplet_second[1]) |
851 | + missing_first = 0 |
852 | + missing_second = 0 |
853 | + try: |
854 | + tw_dict._get_by_primary(triplet_first[0]) |
855 | + except KeyError: |
856 | + missing_first += 1 |
857 | + try: |
858 | + tw_dict._get_by_secondary(triplet_second[0]) |
859 | + except KeyError: |
860 | + missing_first += 1 |
861 | + try: |
862 | + tw_dict._get_by_secondary(triplet_first[1]) |
863 | + except KeyError: |
864 | + missing_second += 1 |
865 | + try: |
866 | + tw_dict._get_by_secondary(triplet_second[1]) |
867 | + except KeyError: |
868 | + missing_second += 1 |
869 | + self.assertEqual(missing_first, 2) |
870 | + self.assertEqual(missing_second, 2) |
871 | + #check for memory leaks |
872 | + dict_len = 0 |
873 | + for key in tw_dict._primary_to_value.iterkeys(): |
874 | + dict_len += 1 |
875 | + self.assertEqual(dict_len, 0) |
876 | + |
877 | + def test_get_primary_and_secondary_key(self): |
878 | + ''' |
879 | + Test for fetching the objects stored in the TwoKeyDict |
880 | + ''' |
881 | + triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \ |
882 | + for a in xrange(10)] |
883 | + tw_dict = TwoKeyDict(*triplets) |
884 | + for triplet in triplets: |
885 | + self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \ |
886 | + triplet[1]) |
887 | + self.assertEqual(tw_dict._get_primary_key(triplet[1]), \ |
888 | + triplet[0]) |
889 | + |
890 | +def test_suite(): |
891 | + return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict) |
892 | + |
893 | |
894 | === added file 'GTG/tools/bidict.py' |
895 | --- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000 |
896 | +++ GTG/tools/bidict.py 2010-08-13 23:43:06 +0000 |
897 | @@ -0,0 +1,112 @@ |
898 | +# -*- coding: utf-8 -*- |
899 | +# ----------------------------------------------------------------------------- |
900 | +# Getting Things Gnome! - a personal organizer for the GNOME desktop |
901 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
902 | +# |
903 | +# This program is free software: you can redistribute it and/or modify it under |
904 | +# the terms of the GNU General Public License as published by the Free Software |
905 | +# Foundation, either version 3 of the License, or (at your option) any later |
906 | +# version. |
907 | +# |
908 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
909 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
910 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
911 | +# details. |
912 | +# |
913 | +# You should have received a copy of the GNU General Public License along with |
914 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
915 | +# ----------------------------------------------------------------------------- |
916 | + |
917 | + |
918 | + |
919 | +class BiDict(object): |
920 | + ''' |
921 | + Bidirectional dictionary: the pairs stored can be accessed using either the |
922 | + first or the second element as key (named key1 and key2). |
923 | + You don't need this if there is no clash between the domains of the first |
924 | + and second element of the pairs. |
925 | + ''' |
926 | + |
927 | + def __init__(self, *pairs): |
928 | + ''' |
929 | + Initialization of the bidirectional dictionary |
930 | + |
931 | + @param pairs: optional. A list of pairs to add to the dictionary |
932 | + ''' |
933 | + super(BiDict, self).__init__() |
934 | + self._first_to_second = {} |
935 | + self._second_to_first = {} |
936 | + for pair in pairs: |
937 | + self.add(pair) |
938 | + |
939 | + def add(self, pair): |
940 | + ''' |
941 | + Adds a pair (key1, key2) to the dictionary |
942 | + |
943 | + @param pair: the pair formatted as (key1, key2) |
944 | + ''' |
945 | + self._first_to_second[pair[0]] = pair[1] |
946 | + self._second_to_first[pair[1]] = pair[0] |
947 | + |
948 | + def _get_by_first(self, key): |
949 | + ''' |
950 | + Gets the key2 given key1 |
951 | + |
952 | + @param key: the first key |
953 | + ''' |
954 | + return self._first_to_second[key] |
955 | + |
956 | + def _get_by_second(self, key): |
957 | + ''' |
958 | + Gets the key1 given key2 |
959 | + |
960 | + @param key: the second key |
961 | + ''' |
962 | + return self._second_to_first[key] |
963 | + |
964 | + def _remove_by_first(self, first): |
965 | + ''' |
966 | + Removes a pair given the first key |
967 | + |
968 | + @param key: the first key |
969 | + ''' |
970 | + second = self._first_to_second[first] |
971 | + del self._second_to_first[second] |
972 | + del self._first_to_second[first] |
973 | + |
974 | + def _remove_by_second(self, second): |
975 | + ''' |
976 | + Removes a pair given the second key |
977 | + |
978 | + @param key: the second key |
979 | + ''' |
980 | + first = self._second_to_first[second] |
981 | + del self._first_to_second[first] |
982 | + del self._second_to_first[second] |
983 | + |
984 | + def _get_all_first(self): |
985 | + ''' |
986 | + Returns the list of all first keys |
987 | + |
988 | + @returns list |
989 | + ''' |
990 | + return list(self._first_to_second) |
991 | + |
992 | + def _get_all_second(self): |
993 | + ''' |
994 | + Returns the list of all second keys |
995 | + |
996 | + @returns list |
997 | + ''' |
998 | + return list(self._second_to_first) |
999 | + |
1000 | + def __str__(self): |
1001 | + ''' |
1002 | + returns a string representing the content of this BiDict |
1003 | + |
1004 | + @returns string |
1005 | + ''' |
1006 | + return reduce(lambda text, keys: \ |
1007 | + str(text) + str(keys), |
1008 | + self._first_to_second.iteritems()) |
1009 | + |
1010 | |
1011 | === added file 'GTG/tools/twokeydict.py' |
1012 | --- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000 |
1013 | +++ GTG/tools/twokeydict.py 2010-08-13 23:43:06 +0000 |
1014 | @@ -0,0 +1,135 @@ |
1015 | +# -*- coding: utf-8 -*- |
1016 | +# ----------------------------------------------------------------------------- |
1017 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
1018 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1019 | +# |
1020 | +# This program is free software: you can redistribute it and/or modify it under |
1021 | +# the terms of the GNU General Public License as published by the Free Software |
1022 | +# Foundation, either version 3 of the License, or (at your option) any later |
1023 | +# version. |
1024 | +# |
1025 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1026 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1027 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1028 | +# details. |
1029 | +# |
1030 | +# You should have received a copy of the GNU General Public License along with |
1031 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1032 | +# ----------------------------------------------------------------------------- |
1033 | + |
1034 | +''' |
1035 | +Contains TwoKeyDict, a Dictionary which also has a secondary key |
1036 | +''' |
1037 | + |
1038 | +from GTG.tools.bidict import BiDict |
1039 | + |
1040 | + |
1041 | + |
1042 | +class TwoKeyDict(object): |
1043 | + ''' |
1044 | + It's a standard Dictionary with a secondary key. |
1045 | + For example, you can add an element ('2', 'II', two'), where the |
1046 | + first two arguments are keys and the third is the stored object, and access |
1047 | + it as: |
1048 | + twokey['2'] ==> 'two' |
1049 | + twokey['II'] ==> 'two' |
1050 | + You can also request the other key, given one. |
1051 | + Function calls start with _ because you'll probably want to rename them when |
1052 | + you use this dictionary, for the sake of clarity. |
1053 | + ''' |
1054 | + |
1055 | + |
1056 | + def __init__(self, *triplets): |
1057 | + ''' |
1058 | + Creates the TwoKeyDict and optionally populates it with some data |
1059 | + |
1060 | + @oaram triplets: tuples for populating the TwoKeyDict. Format: |
1061 | + ((key1, key2, data_to_store), ...) |
1062 | + ''' |
1063 | + super(TwoKeyDict, self).__init__() |
1064 | + self._key_to_key_bidict = BiDict() |
1065 | + self._primary_to_value = {} |
1066 | + for triplet in triplets: |
1067 | + self.add(triplet) |
1068 | + |
1069 | + def add(self, triplet): |
1070 | + ''' |
1071 | + Adds a new triplet to the TwoKeyDict |
1072 | + |
1073 | + @param triplet: a tuple formatted like this: |
1074 | + (key1, key2, data_to_store) |
1075 | + ''' |
1076 | + self._key_to_key_bidict.add((triplet[0], triplet[1])) |
1077 | + self._primary_to_value[triplet[0]] = triplet[2] |
1078 | + |
1079 | + def _get_by_primary(self, primary): |
1080 | + ''' |
1081 | + Gets the stored data given the primary key |
1082 | + |
1083 | + @param primary: the primary key |
1084 | + @returns object: the stored object |
1085 | + ''' |
1086 | + return self._primary_to_value[primary] |
1087 | + |
1088 | + def _get_by_secondary(self, secondary): |
1089 | + ''' |
1090 | + Gets the stored data given the secondary key |
1091 | + |
1092 | + @param secondary: the primary key |
1093 | + @returns object: the stored object |
1094 | + ''' |
1095 | + primary = self._key_to_key_bidict._get_by_second(secondary) |
1096 | + return self._get_by_primary(primary) |
1097 | + |
1098 | + def _remove_by_primary(self, primary): |
1099 | + ''' |
1100 | + Removes a triplet given the rpimary key. |
1101 | + |
1102 | + @param primary: the primary key |
1103 | + ''' |
1104 | + del self._primary_to_value[primary] |
1105 | + self._key_to_key_bidict._remove_by_first(primary) |
1106 | + |
1107 | + def _remove_by_secondary(self, secondary): |
1108 | + ''' |
1109 | + Removes a triplet given the rpimary key. |
1110 | + |
1111 | + @param secondary: the primary key |
1112 | + ''' |
1113 | + primary = self._key_to_key_bidict._get_by_second(secondary) |
1114 | + self._remove_by_primary(primary) |
1115 | + |
1116 | + def _get_secondary_key(self, primary): |
1117 | + ''' |
1118 | + Gets the secondary key given the primary |
1119 | + |
1120 | + @param primary: the primary key |
1121 | + @returns object: the secondary key |
1122 | + ''' |
1123 | + return self._key_to_key_bidict._get_by_first(primary) |
1124 | + |
1125 | + def _get_primary_key(self, secondary): |
1126 | + ''' |
1127 | + Gets the primary key given the secondary |
1128 | + |
1129 | + @param secondary: the secondary key |
1130 | + @returns object: the primary key |
1131 | + ''' |
1132 | + return self._key_to_key_bidict._get_by_second(secondary) |
1133 | + |
1134 | + def _get_all_primary_keys(self): |
1135 | + ''' |
1136 | + Returns all primary keys |
1137 | + |
1138 | + @returns list: list of all primary keys |
1139 | + ''' |
1140 | + return self._key_to_key_bidict._get_all_first() |
1141 | + |
1142 | + def _get_all_secondary_keys(self): |
1143 | + ''' |
1144 | + Returns all secondary keys |
1145 | + |
1146 | + @returns list: list of all secondary keys |
1147 | + ''' |
1148 | + return self._key_to_key_bidict._get_all_second() |
1149 | + |
1150 | |
1151 | === added file 'GTG/tools/watchdog.py' |
1152 | --- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000 |
1153 | +++ GTG/tools/watchdog.py 2010-08-13 23:43:06 +0000 |
1154 | @@ -0,0 +1,53 @@ |
1155 | +# -*- coding: utf-8 -*- |
1156 | +# ----------------------------------------------------------------------------- |
1157 | +# Gettings Things Gnome! - a personal organizer for the GNOME desktop |
1158 | +# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau |
1159 | +# |
1160 | +# This program is free software: you can redistribute it and/or modify it under |
1161 | +# the terms of the GNU General Public License as published by the Free Software |
1162 | +# Foundation, either version 3 of the License, or (at your option) any later |
1163 | +# version. |
1164 | +# |
1165 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1166 | +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1167 | +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
1168 | +# details. |
1169 | +# |
1170 | +# You should have received a copy of the GNU General Public License along with |
1171 | +# this program. If not, see <http://www.gnu.org/licenses/>. |
1172 | +# ----------------------------------------------------------------------------- |
1173 | +import threading |
1174 | + |
1175 | +class Watchdog(object): |
1176 | + ''' |
1177 | + a simple thread-safe watchdog. |
1178 | + usage: |
1179 | + with Watchdod(timeout, error_function): |
1180 | + #do something |
1181 | + ''' |
1182 | + |
1183 | + def __init__(self, timeout, error_function): |
1184 | + ''' |
1185 | + Just sets the timeout and the function to execute when an error occours |
1186 | + |
1187 | + @param timeout: timeout in seconds |
1188 | + @param error_function: what to execute in case the watchdog timer |
1189 | + triggers |
1190 | + ''' |
1191 | + self.timeout = timeout |
1192 | + self.error_function = error_function |
1193 | + |
1194 | + def __enter__(self): |
1195 | + '''Starts the countdown''' |
1196 | + self.timer = threading.Timer(self.timeout, self.error_function) |
1197 | + self.timer.start() |
1198 | + |
1199 | + def __exit__(self, type, value, traceback): |
1200 | + '''Aborts the countdown''' |
1201 | + try: |
1202 | + self.timer.cancel() |
1203 | + except: |
1204 | + pass |
1205 | + if value == None: |
1206 | + return True |
1207 | + return False |