gtg team mailing list archive
-
gtg team
-
Mailing list archive
-
Message #03035
[Merge] lp:~gtg-user/gtg/backends-utils into lp:gtg
Luca Invernizzi has proposed merging lp:~gtg-user/gtg/backends-utils into lp:gtg with lp:~gtg-user/gtg/backends-window as a prerequisite.
Requested reviews:
Gtg developers (gtg)
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).
--
https://code.launchpad.net/~gtg-user/gtg/backends-utils/+merge/32644
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/backends-utils into lp:gtg.
=== modified file 'CHANGELOG'
--- CHANGELOG 2010-08-04 00:30:22 +0000
+++ CHANGELOG 2010-08-13 23:45:00 +0000
@@ -4,6 +4,7 @@
* Fixed bug with data consistency #579189, by Marko Kevac
* Added samba bugzilla to the bugzilla plugin, by Jelmer Vernoij
* Fixed bug #532392, a start date is later than a due date, by Volodymyr Floreskul
+ * Added utilities for complex backends by Luca Invernizzi
2010-03-01 Getting Things GNOME! 0.2.2
* Autostart on login, by Luca Invernizzi
=== added file 'GTG/backends/periodicimportbackend.py'
--- GTG/backends/periodicimportbackend.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/periodicimportbackend.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Contains PeriodicImportBackend, a GenericBackend specialized for checking the
+remote backend in polling.
+'''
+
+import threading
+
+from GTG.backends.genericbackend import GenericBackend
+from GTG.backends.backendsignals import BackendSignals
+from GTG.tools.interruptible import interruptible
+
+
+
+class PeriodicImportBackend(GenericBackend):
+ '''
+ This class can be used in place of GenericBackend when a periodic import is
+ necessary, as the remote service providing tasks does not signals the
+ changes.
+ To use this, only two things are necessary:
+ - using do_periodic_import instead of start_get_tasks
+ - having in _static_parameters a "period" key, as in
+ "period": { \
+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+ GenericBackend.PARAM_DEFAULT_VALUE: 2, },
+ This specifies the time that must pass between consecutive imports
+ (in minutes)
+ '''
+
+ @interruptible
+ def start_get_tasks(self):
+ '''
+ This function launches the first periodic import, and schedules the
+ next ones.
+ '''
+ try:
+ if self.import_timer:
+ self.import_timer.cancel()
+ except:
+ pass
+ self._start_get_tasks()
+ self.cancellation_point()
+ if self.is_enabled() == False:
+ return
+ self.import_timer = threading.Timer( \
+ self._parameters['period'] * 60.0, \
+ self.start_get_tasks)
+ self.import_timer.start()
+
+ def _start_get_tasks(self):
+ '''
+ This function executes an imports and schedules the next
+ '''
+ self.cancellation_point()
+ BackendSignals().backend_sync_started(self.get_id())
+ self.do_periodic_import()
+ BackendSignals().backend_sync_ended(self.get_id())
+
+ def quit(self, disable = False):
+ '''
+ Called when GTG quits or disconnects the backend.
+ '''
+ super(PeriodicImportBackend, self).quit(disable)
+ try:
+ self.import_timer.cancel()
+ except Exception:
+ pass
+ try:
+ self.import_timer.join()
+ except Exception:
+ pass
+
=== added file 'GTG/backends/syncengine.py'
--- GTG/backends/syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/syncengine.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,288 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+This library deals with synchronizing two sets of objects.
+It works like this:
+ - We have two sets of generic objects (local and remote)
+ - We present one object of either one of the sets and ask the library what's
+ the state of its synchronization
+ - the library will tell us if we need to add a clone object in the other set,
+ update it or, if the other one has been removed, remove also this one
+'''
+from GTG.tools.twokeydict import TwoKeyDict
+
+
+TYPE_LOCAL = "local"
+TYPE_REMOTE = "remote"
+
+
+
+class SyncMeme(object):
+ '''
+ A SyncMeme is the object storing the data needed to keep track of the state
+ of two objects synchronization.
+ This basic version, that can be expanded as needed by the code using the
+ SyncEngine, just stores the modified date and time of the last
+ synchronization for both objects (local and remote)
+ '''
+ #NOTE: Checking objects CRCs would make this check nicer, as we could know
+ # if the object was really changed, or it has just updated its
+ # modified time (invernizzi)
+
+ def __init__(self,
+ local_modified = None,
+ remote_modified = None,
+ origin = None):
+ '''
+ Creates a new SyncMeme, updating the modified times for both the
+ local and remote objects, and sets the given origin.
+ If any of the parameters is set to None, it's ignored.
+
+ @param local_modified: the modified time for the local object
+ @param remote_modified: the modified time for the remote object
+ @param origin: an object that identifies whether the local or the remote is
+ the original object, the other one being a copy.
+ '''
+ if local_modified != None:
+ self.set_local_last_modified(local_modified)
+ if remote_modified != None:
+ self.set_remote_last_modified(remote_modified)
+ if origin != None:
+ self.set_origin(origin)
+
+ def set_local_last_modified(self, modified_datetime):
+ '''
+ Setter function for the local object modified datetime.
+
+ @param modified_datetime: the local object modified datetime
+ '''
+ self.local_last_modified = modified_datetime
+
+ def get_local_last_modified(self):
+ '''
+ Getter function for the local object modified datetime.
+ '''
+ return self.local_last_modified
+
+ def set_remote_last_modified(self, modified_datetime):
+ '''
+ Setter function for the remote object modified datetime.
+
+ @param modified_datetime: the remote object modified datetime
+ '''
+ self.remote_last_modified = modified_datetime
+
+ def get_remote_last_modified(self):
+ '''
+ Getter function for the remote object modified datetime.
+ '''
+ return self.remote_last_modified
+
+ def which_is_newest(self, local_modified, remote_modified):
+ '''
+ Given the updated modified time for both the local and the remote
+ objects, it checks them against the stored modified times and
+ then against each other.
+
+ @returns string: "local"- if the local object has been modified and its
+ the newest
+ "remote" - the same for the remote object
+ None - if no object modified time is newer than the
+ stored one (the objects have not been modified)
+ '''
+ if local_modified <= self.local_last_modified and \
+ remote_modified <= self.remote_last_modified:
+ return None
+ if local_modified > remote_modified:
+ return "local"
+ else:
+ return "remote"
+
+ def get_origin(self):
+ '''
+ Returns the name of the source that firstly presented the object
+ '''
+ return self.origin
+
+ def set_origin(self, origin):
+ '''
+ Sets the source that presented the object for the first time. This
+ source holds the original object, while the other holds the copy.
+ This can be useful in the case of "lost syncability" (see the SyncEngine
+ for an explaination).
+
+ @param origin: object representing the source
+ '''
+ self.origin = origin
+
+
+
+class SyncMemes(TwoKeyDict):
+ '''
+ A TwoKeyDict, with just the names changed to be better understandable.
+ The meaning of these names is explained in the SyncEngine class description.
+ It's used to store a set of SyncMeme objects, each one keeping storing all
+ the data needed to keep track of a single relationship.
+ '''
+
+
+ get_remote_id = TwoKeyDict._get_secondary_key
+ get_local_id = TwoKeyDict._get_primary_key
+ remove_local_id = TwoKeyDict._remove_by_primary
+ remove_remote_id = TwoKeyDict._remove_by_secondary
+ get_meme_from_local_id = TwoKeyDict._get_by_primary
+ get_meme_from_remote_id = TwoKeyDict._get_by_secondary
+ get_all_local = TwoKeyDict._get_all_primary_keys
+ get_all_remote = TwoKeyDict._get_all_secondary_keys
+
+
+
+class SyncEngine(object):
+ '''
+ The SyncEngine is an object useful in keeping two sets of objects
+ synchronized.
+ One set is called the Local set, the other is the Remote one.
+ It stores the state of the synchronization and the latest state of each
+ object.
+ When asked, it can tell if a couple of related objects are up to date in the
+ sync and, if not, which one must be updated.
+
+ It stores the state of each relationship in a series of SyncMeme.
+ '''
+
+
+ UPDATE = "update"
+ REMOVE = "remove"
+ ADD = "add"
+ LOST_SYNCABILITY = "lost syncability"
+
+ def __init__(self):
+ '''
+ Initializes the storage of object relationships.
+ '''
+ self.sync_memes = SyncMemes()
+
+ def _analyze_element(self,
+ element_id,
+ is_local,
+ has_local,
+ has_remote,
+ is_syncable = True):
+ '''
+ Given an object that should be synced with another one,
+ it finds out about the related object, and decides whether:
+ - the other object hasn't been created yet (thus must be added)
+ - the other object has been deleted (thus this one must be deleted)
+ - the other object is present, but either one has been changed
+
+ A particular case happens if the other object is present, but the
+ "is_syncable" parameter (which tells that we intend to keep these two
+ objects in sync) is set to False. In this case, this function returns
+ that the Syncability property has been lost. This case is interesting if
+ we want to delete one of the two objects (the one that has been cloned
+ from the original).
+
+ @param element_id: the id of the element we're analysing.
+ @param is_local: True if the element analysed is the local one (not the
+ remote)
+ @param has_local: function that accepts an id of the local set and
+ returns True if the element is present
+ @param has_remote: function that accepts an id of the remote set and
+ returns True if the element is present
+ @param is_syncable: explained above
+ @returns string: one of self.UPDATE, self.ADD, self.REMOVE,
+ self.LOST_SYNCABILITY
+ '''
+ if is_local:
+ get_other_id = self.sync_memes.get_remote_id
+ is_task_present = has_remote
+ else:
+ get_other_id = self.sync_memes.get_local_id
+ is_task_present = has_local
+
+ try:
+ other_id = get_other_id(element_id)
+ if is_task_present(other_id):
+ if is_syncable:
+ return self.UPDATE, other_id
+ else:
+ return self.LOST_SYNCABILITY, other_id
+ else:
+ return self.REMOVE, None
+ except KeyError:
+ if is_syncable:
+ return self.ADD, None
+ return None, None
+
+ def analyze_local_id(self, element_id, *other_args):
+ '''
+ Shortcut to call _analyze_element for a local element
+ '''
+ return self._analyze_element(element_id, True, *other_args)
+
+ def analyze_remote_id(self, element_id, *other_args):
+ '''
+ Shortcut to call _analyze_element for a remote element
+ '''
+ return self._analyze_element(element_id, False, *other_args)
+
+ def record_relationship(self, local_id, remote_id, meme):
+ '''
+ Records that an object from the local set is related with one a remote
+ set.
+
+ @param local_id: the id of the local task
+ @param remote_id: the id of the remote task
+ @param meme: the SyncMeme that keeps track of the relationship
+ '''
+ triplet = (local_id, remote_id, meme)
+ self.sync_memes.add(triplet)
+
+ def break_relationship(self, local_id = None, remote_id = None):
+ '''
+ breaks a relationship between two objects.
+ Only one of the two parameters is necessary to identify the
+ relationship.
+
+ @param local_id: the id of the local task
+ @param remote_id: the id of the remote task
+ '''
+ if local_id:
+ self.sync_memes.remove_local_id(local_id)
+ elif remote_id:
+ self.sync_memes.remove_remote_id(remote_id)
+
+ def __getattr__(self, attr):
+ '''
+ The functions listed here are passed directly to the SyncMeme object
+
+ @param attr: a function name among the ones listed here
+ @returns object: the function return object.
+ '''
+ if attr in ['get_remote_id',
+ 'get_local_id',
+ 'get_meme_from_local_id',
+ 'get_meme_from_remote_id',
+ 'get_all_local',
+ 'get_all_remote']:
+ return getattr(self.sync_memes, attr)
+ else:
+ raise AttributeError
+
=== added file 'GTG/tests/test_bidict.py'
--- GTG/tests/test_bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_bidict.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the diDict class
+'''
+
+import unittest
+import uuid
+
+from GTG.tools.bidict import BiDict
+
+
+
+class TestBiDict(unittest.TestCase):
+ '''
+ Tests for the BiDict object.
+ '''
+
+
+ def test_add_and_gets(self):
+ '''
+ Test for the __init__, _get_by_first, _get_by_second function
+ '''
+ pairs = [(uuid.uuid4(), uuid.uuid4()) for a in xrange(10)]
+ bidict = BiDict(*pairs)
+ for pair in pairs:
+ self.assertEqual(bidict._get_by_first(pair[0]), pair[1])
+ self.assertEqual(bidict._get_by_second(pair[1]), pair[0])
+
+ def test_remove_by_first_or_second(self):
+ '''
+ Tests for removing elements from the biDict
+ '''
+ pair_first = (1, 'one')
+ pair_second = (2, 'two')
+ bidict = BiDict(pair_first, pair_second)
+ bidict._remove_by_first(pair_first[0])
+ bidict._remove_by_second(pair_second[1])
+ missing_first = 0
+ missing_second = 0
+ try:
+ bidict._get_by_first(pair_first[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ bidict._get_by_first(pair_second[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ bidict._get_by_second(pair_first[1])
+ except KeyError:
+ missing_second += 1
+ try:
+ bidict._get_by_second(pair_second[1])
+ except KeyError:
+ missing_second += 1
+ self.assertEqual(missing_first, 2)
+ self.assertEqual(missing_second, 2)
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestBiDict)
+
=== added file 'GTG/tests/test_dates.py'
--- GTG/tests/test_dates.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_dates.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the various Date classes
+'''
+
+import unittest
+
+from GTG.tools.dates import get_canonical_date
+
+class TestDates(unittest.TestCase):
+ '''
+ Tests for the various Date classes
+ '''
+
+ def test_get_canonical_date(self):
+ '''
+ Tests for "get_canonical_date"
+ '''
+ for str in ["1985-03-29", "now", "soon", "later", ""]:
+ date = get_canonical_date(str)
+ self.assertEqual(date.__str__(), str)
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestDates)
+
=== added file 'GTG/tests/test_syncengine.py'
--- GTG/tests/test_syncengine.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncengine.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the SyncEngine class
+'''
+
+import unittest
+import uuid
+
+from GTG.backends.syncengine import SyncEngine
+
+
+
+class TestSyncEngine(unittest.TestCase):
+ '''
+ Tests for the SyncEngine object.
+ '''
+
+ def setUp(self):
+ self.ftp_local = FakeTaskProvider()
+ self.ftp_remote = FakeTaskProvider()
+ self.sync_engine = SyncEngine()
+
+ def test_analyze_element_and_record_and_break_relationship(self):
+ '''
+ Test for the _analyze_element, analyze_remote_id, analyze_local_id,
+ record_relationship, break_relationship
+ '''
+ #adding a new local task
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #creating the related remote task
+ remote_id = uuid.uuid4()
+ self.ftp_remote.fake_add_task(remote_id)
+ #informing the sync_engine about that
+ self.sync_engine.record_relationship(local_id, remote_id, object())
+ #verifying that it understood that
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.UPDATE, remote_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.UPDATE, local_id))
+ #and not the reverse
+ self.assertEqual(self.sync_engine.analyze_remote_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #now we remove the remote task
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.REMOVE, None))
+ self.sync_engine.break_relationship(local_id = local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #we add them back and remove giving the remote id as key to find what to
+ #delete
+ self.ftp_local.fake_add_task(local_id)
+ self.ftp_remote.fake_add_task(remote_id)
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.sync_engine.record_relationship(local_id, remote_id, object)
+ self.sync_engine.break_relationship(remote_id = remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+
+ def test_syncability(self):
+ '''
+ Test for the _analyze_element, analyze_remote_id, analyze_local_id.
+ Checks that the is_syncable parameter is used correctly
+ '''
+ #adding a new local task unsyncable
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+ #adding a new local task, syncable
+ local_id = uuid.uuid4()
+ self.ftp_local.fake_add_task(local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task), \
+ (SyncEngine.ADD, None))
+ #creating the related remote task
+ remote_id = uuid.uuid4()
+ self.ftp_remote.fake_add_task(remote_id)
+ #informing the sync_engine about that
+ self.sync_engine.record_relationship(local_id, remote_id, object())
+ #checking that it behaves correctly with established relationships
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.UPDATE, remote_id))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.LOST_SYNCABILITY, remote_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.UPDATE, local_id))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.LOST_SYNCABILITY, local_id))
+ #now we remove the remote task
+ self.ftp_remote.fake_remove_task(remote_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.REMOVE, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (SyncEngine.REMOVE, None))
+ self.sync_engine.break_relationship(local_id = local_id)
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_local_id(local_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ True), \
+ (SyncEngine.ADD, None))
+ self.assertEqual(self.sync_engine.analyze_remote_id(remote_id, \
+ self.ftp_local.has_task, self.ftp_remote.has_task,
+ False), \
+ (None, None))
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncEngine)
+
+
+class FakeTaskProvider(object):
+
+ def __init__(self):
+ self.dic = {}
+
+ def has_task(self, tid):
+ return self.dic.has_key(tid)
+
+###############################################################################
+### Function with the fake_ prefix are here to assist in testing, they do not
+### need to be present in the real class
+###############################################################################
+
+ def fake_add_task(self, tid):
+ self.dic[tid] = "something"
+
+ def fake_get_task(self, tid):
+ return self.dic[tid]
+
+ def fake_remove_task(self, tid):
+ del self.dic[tid]
=== added file 'GTG/tests/test_syncmeme.py'
--- GTG/tests/test_syncmeme.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_syncmeme.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the SyncMeme class
+'''
+
+import unittest
+import datetime
+
+from GTG.backends.syncengine import SyncMeme
+
+
+
+class TestSyncMeme(unittest.TestCase):
+ '''
+ Tests for the SyncEngine object.
+ '''
+
+ def test_which_is_newest(self):
+ '''
+ test the which_is_newest function
+
+ '''
+ meme = SyncMeme()
+ #tasks have not changed
+ local_modified = datetime.datetime.now()
+ remote_modified = datetime.datetime.now()
+ meme.set_local_last_modified(local_modified)
+ meme.set_remote_last_modified(remote_modified)
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), None)
+ #we update the local
+ local_modified = datetime.datetime.now()
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), 'local')
+ #we update the remote
+ remote_modified = datetime.datetime.now()
+ self.assertEqual(meme.which_is_newest(local_modified, \
+ remote_modified), 'remote')
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestSyncMeme)
+
=== added file 'GTG/tests/test_twokeydict.py'
--- GTG/tests/test_twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tests/test_twokeydict.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Tests for the TwoKeyDict class
+'''
+
+import unittest
+import uuid
+
+from GTG.tools.twokeydict import TwoKeyDict
+
+
+
+class TestTwoKeyDict(unittest.TestCase):
+ '''
+ Tests for the TwoKeyDict object.
+ '''
+
+
+ def test_add_and_gets(self):
+ '''
+ Test for the __init__, _get_by_first, _get_by_second function
+ '''
+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
+ for a in xrange(10)]
+ tw_dict = TwoKeyDict(*triplets)
+ for triplet in triplets:
+ self.assertEqual(tw_dict._get_by_primary(triplet[0]), triplet[2])
+ self.assertEqual(tw_dict._get_by_secondary(triplet[1]), triplet[2])
+
+ def test_remove_by_first_or_second(self):
+ '''
+ Test for removing triplets form the TwoKeyDict
+ '''
+ triplet_first = (1, 'I', 'one')
+ triplet_second = (2, 'II', 'two')
+ tw_dict = TwoKeyDict(triplet_first, triplet_second)
+ tw_dict._remove_by_primary(triplet_first[0])
+ tw_dict._remove_by_secondary(triplet_second[1])
+ missing_first = 0
+ missing_second = 0
+ try:
+ tw_dict._get_by_primary(triplet_first[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ tw_dict._get_by_secondary(triplet_second[0])
+ except KeyError:
+ missing_first += 1
+ try:
+ tw_dict._get_by_secondary(triplet_first[1])
+ except KeyError:
+ missing_second += 1
+ try:
+ tw_dict._get_by_secondary(triplet_second[1])
+ except KeyError:
+ missing_second += 1
+ self.assertEqual(missing_first, 2)
+ self.assertEqual(missing_second, 2)
+ #check for memory leaks
+ dict_len = 0
+ for key in tw_dict._primary_to_value.iterkeys():
+ dict_len += 1
+ self.assertEqual(dict_len, 0)
+
+ def test_get_primary_and_secondary_key(self):
+ '''
+ Test for fetching the objects stored in the TwoKeyDict
+ '''
+ triplets = [(uuid.uuid4(), uuid.uuid4(), uuid.uuid4()) \
+ for a in xrange(10)]
+ tw_dict = TwoKeyDict(*triplets)
+ for triplet in triplets:
+ self.assertEqual(tw_dict._get_secondary_key(triplet[0]), \
+ triplet[1])
+ self.assertEqual(tw_dict._get_primary_key(triplet[1]), \
+ triplet[0])
+
+def test_suite():
+ return unittest.TestLoader().loadTestsFromTestCase(TestTwoKeyDict)
+
=== added file 'GTG/tools/bidict.py'
--- GTG/tools/bidict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/bidict.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+
+
+class BiDict(object):
+ '''
+ Bidirectional dictionary: the pairs stored can be accessed using either the
+ first or the second element as key (named key1 and key2).
+ You don't need this if there is no clash between the domains of the first
+ and second element of the pairs.
+ '''
+
+ def __init__(self, *pairs):
+ '''
+ Initialization of the bidirectional dictionary
+
+ @param pairs: optional. A list of pairs to add to the dictionary
+ '''
+ super(BiDict, self).__init__()
+ self._first_to_second = {}
+ self._second_to_first = {}
+ for pair in pairs:
+ self.add(pair)
+
+ def add(self, pair):
+ '''
+ Adds a pair (key1, key2) to the dictionary
+
+ @param pair: the pair formatted as (key1, key2)
+ '''
+ self._first_to_second[pair[0]] = pair[1]
+ self._second_to_first[pair[1]] = pair[0]
+
+ def _get_by_first(self, key):
+ '''
+ Gets the key2 given key1
+
+ @param key: the first key
+ '''
+ return self._first_to_second[key]
+
+ def _get_by_second(self, key):
+ '''
+ Gets the key1 given key2
+
+ @param key: the second key
+ '''
+ return self._second_to_first[key]
+
+ def _remove_by_first(self, first):
+ '''
+ Removes a pair given the first key
+
+ @param key: the first key
+ '''
+ second = self._first_to_second[first]
+ del self._second_to_first[second]
+ del self._first_to_second[first]
+
+ def _remove_by_second(self, second):
+ '''
+ Removes a pair given the second key
+
+ @param key: the second key
+ '''
+ first = self._second_to_first[second]
+ del self._first_to_second[first]
+ del self._second_to_first[second]
+
+ def _get_all_first(self):
+ '''
+ Returns the list of all first keys
+
+ @returns list
+ '''
+ return list(self._first_to_second)
+
+ def _get_all_second(self):
+ '''
+ Returns the list of all second keys
+
+ @returns list
+ '''
+ return list(self._second_to_first)
+
+ def __str__(self):
+ '''
+ returns a string representing the content of this BiDict
+
+ @returns string
+ '''
+ return reduce(lambda text, keys: \
+ str(text) + str(keys),
+ self._first_to_second.iteritems())
+
=== added file 'GTG/tools/twokeydict.py'
--- GTG/tools/twokeydict.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/twokeydict.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Contains TwoKeyDict, a Dictionary which also has a secondary key
+'''
+
+from GTG.tools.bidict import BiDict
+
+
+
+class TwoKeyDict(object):
+ '''
+ It's a standard Dictionary with a secondary key.
+ For example, you can add an element ('2', 'II', two'), where the
+ first two arguments are keys and the third is the stored object, and access
+ it as:
+ twokey['2'] ==> 'two'
+ twokey['II'] ==> 'two'
+ You can also request the other key, given one.
+ Function calls start with _ because you'll probably want to rename them when
+ you use this dictionary, for the sake of clarity.
+ '''
+
+
+ def __init__(self, *triplets):
+ '''
+ Creates the TwoKeyDict and optionally populates it with some data
+
+ @oaram triplets: tuples for populating the TwoKeyDict. Format:
+ ((key1, key2, data_to_store), ...)
+ '''
+ super(TwoKeyDict, self).__init__()
+ self._key_to_key_bidict = BiDict()
+ self._primary_to_value = {}
+ for triplet in triplets:
+ self.add(triplet)
+
+ def add(self, triplet):
+ '''
+ Adds a new triplet to the TwoKeyDict
+
+ @param triplet: a tuple formatted like this:
+ (key1, key2, data_to_store)
+ '''
+ self._key_to_key_bidict.add((triplet[0], triplet[1]))
+ self._primary_to_value[triplet[0]] = triplet[2]
+
+ def _get_by_primary(self, primary):
+ '''
+ Gets the stored data given the primary key
+
+ @param primary: the primary key
+ @returns object: the stored object
+ '''
+ return self._primary_to_value[primary]
+
+ def _get_by_secondary(self, secondary):
+ '''
+ Gets the stored data given the secondary key
+
+ @param secondary: the primary key
+ @returns object: the stored object
+ '''
+ primary = self._key_to_key_bidict._get_by_second(secondary)
+ return self._get_by_primary(primary)
+
+ def _remove_by_primary(self, primary):
+ '''
+ Removes a triplet given the rpimary key.
+
+ @param primary: the primary key
+ '''
+ del self._primary_to_value[primary]
+ self._key_to_key_bidict._remove_by_first(primary)
+
+ def _remove_by_secondary(self, secondary):
+ '''
+ Removes a triplet given the rpimary key.
+
+ @param secondary: the primary key
+ '''
+ primary = self._key_to_key_bidict._get_by_second(secondary)
+ self._remove_by_primary(primary)
+
+ def _get_secondary_key(self, primary):
+ '''
+ Gets the secondary key given the primary
+
+ @param primary: the primary key
+ @returns object: the secondary key
+ '''
+ return self._key_to_key_bidict._get_by_first(primary)
+
+ def _get_primary_key(self, secondary):
+ '''
+ Gets the primary key given the secondary
+
+ @param secondary: the secondary key
+ @returns object: the primary key
+ '''
+ return self._key_to_key_bidict._get_by_second(secondary)
+
+ def _get_all_primary_keys(self):
+ '''
+ Returns all primary keys
+
+ @returns list: list of all primary keys
+ '''
+ return self._key_to_key_bidict._get_all_first()
+
+ def _get_all_secondary_keys(self):
+ '''
+ Returns all secondary keys
+
+ @returns list: list of all secondary keys
+ '''
+ return self._key_to_key_bidict._get_all_second()
+
=== added file 'GTG/tools/watchdog.py'
--- GTG/tools/watchdog.py 1970-01-01 00:00:00 +0000
+++ GTG/tools/watchdog.py 2010-08-13 23:45:00 +0000
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Gettings Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+import threading
+
+class Watchdog(object):
+ '''
+ a simple thread-safe watchdog.
+ usage:
+ with Watchdod(timeout, error_function):
+ #do something
+ '''
+
+ def __init__(self, timeout, error_function):
+ '''
+ Just sets the timeout and the function to execute when an error occours
+
+ @param timeout: timeout in seconds
+ @param error_function: what to execute in case the watchdog timer
+ triggers
+ '''
+ self.timeout = timeout
+ self.error_function = error_function
+
+ def __enter__(self):
+ '''Starts the countdown'''
+ self.timer = threading.Timer(self.timeout, self.error_function)
+ self.timer.start()
+
+ def __exit__(self, type, value, traceback):
+ '''Aborts the countdown'''
+ try:
+ self.timer.cancel()
+ except:
+ pass
+ if value == None:
+ return True
+ return False
Follow ups