~bryce/gtg/dbus-service-name

« back to all changes in this revision

Viewing changes to GTG/backends/genericbackend.py

  • Committer: Luca Invernizzi
  • Date: 2010-06-22 20:19:47 UTC
  • mto: This revision was merged to the branch mainline in revision 825.
  • Revision ID: invernizzi.l@gmail.com-20100622201947-mixmodtcf6qvowqi
second batch

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# -----------------------------------------------------------------------------
3
 
# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4
 
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5
 
#
6
 
# This program is free software: you can redistribute it and/or modify it under
7
 
# the terms of the GNU General Public License as published by the Free Software
8
 
# Foundation, either version 3 of the License, or (at your option) any later
9
 
# version.
10
 
#
11
 
# This program is distributed in the hope that it will be useful, but WITHOUT
12
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
 
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
 
# details.
15
 
#
16
 
# You should have received a copy of the GNU General Public License along with
17
 
# this program.  If not, see <http://www.gnu.org/licenses/>.
18
 
# -----------------------------------------------------------------------------
19
 
 
20
 
'''
21
 
FIXME: document!
22
 
'''
23
 
 
24
 
import os
25
 
import sys
26
 
import errno
27
 
import pickle
28
 
import threading
29
 
from collections import deque
30
 
 
31
 
from GTG.backends.backendsignals import BackendSignals
32
 
from GTG.tools.keyring import Keyring
33
 
from GTG.core import CoreConfig
34
 
from GTG.tools.logger import Log
35
 
 
36
 
 
37
 
 
38
 
 
39
 
class GenericBackend(object):
40
 
    '''
41
 
    Base class for every backend. It's a little more than an interface which
42
 
    methods have to be redefined in order for the backend to run.
43
 
    '''
44
 
 
45
 
 
46
 
    #BACKEND TYPE DESCRIPTION
47
 
    #"_general_description" is a dictionary that holds the values for the
48
 
    # following keys:
49
 
    BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
50
 
                          # translations, *must be unique*)
51
 
    BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
52
 
    BACKEND_DESCRIPTION = "description" #A short description of the backend
53
 
    BACKEND_AUTHORS = "authors" #a list of strings
54
 
    BACKEND_TYPE = "type"
55
 
    #BACKEND_TYPE is one of:
56
 
    TYPE_READWRITE = "readwrite"
57
 
    TYPE_READONLY = "readonly"
58
 
    TYPE_IMPORT = "import"
59
 
    TYPE_EXPORT = "export"
60
 
    _general_description = {}
61
 
 
62
 
 
63
 
    #"static_parameters" is a dictionary of dictionaries, each of which
64
 
    #representing a parameter needed to configure the backend.
65
 
    #each "sub-dictionary" is identified by this a key representing its name.
66
 
    #"static_parameters" will be part of the definition of each
67
 
    #particular backend.
68
 
    # Each dictionary contains the keys:
69
 
    #PARAM_DESCRIPTION = "description" #short description (shown to the user
70
 
                                      # during configuration)
71
 
    PARAM_DEFAULT_VALUE = "default_value" # its default value
72
 
    PARAM_TYPE = "type"  
73
 
    #PARAM_TYPE is one of the following (changing this changes the way
74
 
    # the user can configure the parameter)
75
 
    TYPE_PASSWORD = "password" #the real password is stored in the GNOME
76
 
                               # keyring
77
 
                               # This is just a key to find it there
78
 
    TYPE_STRING = "string"  #generic string, nothing fancy is done
79
 
    TYPE_INT = "int"  #edit box can contain only integers
80
 
    TYPE_BOOL = "bool" #checkbox is shown
81
 
    TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
82
 
                                        # prohibited in strings
83
 
    _static_parameters = {}
84
 
 
85
 
    def initialize(self):
86
 
        '''
87
 
        Called each time it is enabled again (including on backend creation).
88
 
        Please note that a class instance for each disabled backend *is*
89
 
        created, but it's not initialized. 
90
 
        Optional. 
91
 
        NOTE: make sure to call super().initialize()
92
 
        '''
93
 
        for module_name in self.get_required_modules():
94
 
            sys.modules[module_name]= __import__(module_name)
95
 
        self._parameters[self.KEY_ENABLED] = True
96
 
        self._is_initialized = True
97
 
        #we signal that the backend has been enabled
98
 
        self._signal_manager.backend_state_changed(self.get_id())
99
 
 
100
 
    def start_get_tasks(self):
101
 
        '''
102
 
        Once this function is launched, the backend can start pushing
103
 
        tasks to gtg parameters.
104
 
 
105
 
        @return: start_get_tasks() might not return or finish
106
 
        '''
107
 
        raise NotImplemented()
108
 
 
109
 
    def set_task(self, task):
110
 
        '''
111
 
        Save the task in the backend. If the task id is new for the 
112
 
        backend, then a new task must be created.
113
 
        '''
114
 
        pass
115
 
 
116
 
    def remove_task(self, tid):
117
 
        ''' Completely remove the task with ID = tid '''
118
 
        pass
119
 
 
120
 
    def has_task(self, tid):
121
 
        '''Returns true if the backend has an internal idea 
122
 
           of the task corresponding to the tid. False otherwise'''
123
 
        raise NotImplemented()
124
 
 
125
 
    def new_task_id(self):
126
 
        '''
127
 
        Returns an available ID for a new task so that a task with this ID
128
 
        can be saved with set_task later.
129
 
        '''
130
 
        raise NotImplemented()
131
 
 
132
 
    def this_is_the_first_run(self, xml):
133
 
        '''
134
 
        Steps to execute if it's the first time the backend is run. Optional.
135
 
        '''
136
 
        pass
137
 
 
138
 
    def purge(self):
139
 
        '''
140
 
        Called when a backend will be removed from GTG. Useful for removing
141
 
        configuration files. Optional.
142
 
        '''
143
 
        pass
144
 
 
145
 
    def get_number_of_tasks(self):
146
 
        '''
147
 
        Returns the number of tasks stored in the backend. Doesn't need to be a
148
 
        fast function, is called just for the UI
149
 
        '''
150
 
        raise NotImplemented()
151
 
 
152
 
    @staticmethod
153
 
    def get_required_modules():
154
 
        return []
155
 
 
156
 
    def quit(self, disable = False):
157
 
        '''
158
 
        Called when GTG quits or disconnects the backend. Remember to execute
159
 
        also this function when quitting. If disable is True, the backend won't
160
 
        be automatically loaded at next GTG start
161
 
        '''
162
 
        self._is_initialized = False
163
 
        if disable:
164
 
            self._parameters[self.KEY_ENABLED] = False
165
 
            #we signal that we have been disabled
166
 
            self._signal_manager.backend_state_changed(self.get_id())
167
 
            self._signal_manager.backend_sync_ended(self.get_id())
168
 
        syncing_thread = threading.Thread(target = self.sync).run()
169
 
 
170
 
    def save_state(self):
171
 
        '''
172
 
        It's the last function executed on a quitting backend, after the
173
 
        pending actions have been done.
174
 
        Useful to ensure that the state is saved in a consistent manner
175
 
        '''
176
 
        pass
177
 
 
178
 
###############################################################################
179
 
###### You don't need to reimplement the functions below this line ############
180
 
###############################################################################
181
 
 
182
 
    #These parameters are common to all backends and necessary.
183
 
    # They will be added automatically to your _static_parameters list
184
 
    #NOTE: for now I'm disabling changing the default backend. Once it's all
185
 
    #      set up, we will see about that (invernizzi)
186
 
    KEY_DEFAULT_BACKEND = "Default"
187
 
    KEY_ENABLED = "Enabled"
188
 
    KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
189
 
    KEY_ATTACHED_TAGS = "attached-tags"
190
 
    KEY_USER = "user"
191
 
    KEY_PID = "pid"
192
 
    ALLTASKS_TAG = "gtg-tags-all"  #IXME: moved here to avoid circular imports
193
 
 
194
 
    _static_parameters_obligatory = { \
195
 
                                    KEY_DEFAULT_BACKEND: { \
196
 
                                         PARAM_TYPE: TYPE_BOOL, \
197
 
                                         PARAM_DEFAULT_VALUE: False, \
198
 
                                    }, \
199
 
                                    KEY_HUMAN_NAME: { \
200
 
                                         PARAM_TYPE: TYPE_STRING, \
201
 
                                         PARAM_DEFAULT_VALUE: "", \
202
 
                                    }, \
203
 
                                    KEY_USER: { \
204
 
                                         PARAM_TYPE: TYPE_STRING, \
205
 
                                         PARAM_DEFAULT_VALUE: "", \
206
 
                                    }, \
207
 
                                    KEY_PID: { \
208
 
                                         PARAM_TYPE: TYPE_STRING, \
209
 
                                         PARAM_DEFAULT_VALUE: "", \
210
 
                                    }, \
211
 
                                    KEY_ENABLED: { \
212
 
                                         PARAM_TYPE: TYPE_BOOL, \
213
 
                                         PARAM_DEFAULT_VALUE: False, \
214
 
                                    }}
215
 
 
216
 
    _static_parameters_obligatory_for_rw = { \
217
 
                                    KEY_ATTACHED_TAGS: {\
218
 
                                         PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
219
 
                                         PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
220
 
                                    }}
221
 
    
222
 
    #Handy dictionary used in type conversion (from string to type)
223
 
    _type_converter = {TYPE_STRING: str,
224
 
                       TYPE_INT: int,
225
 
                      }
226
 
 
227
 
    @classmethod
228
 
    def _get_static_parameters(cls):
229
 
        '''
230
 
        Helper method, used to obtain the full list of the static_parameters
231
 
        (user configured and default ones)
232
 
        '''
233
 
        if hasattr(cls, "_static_parameters"):
234
 
            temp_dic = cls._static_parameters_obligatory.copy()
235
 
            if cls._general_description[cls.BACKEND_TYPE] == cls.TYPE_READWRITE:
236
 
                for key, value in \
237
 
                          cls._static_parameters_obligatory_for_rw.iteritems():
238
 
                    temp_dic[key] = value
239
 
            for key, value in cls._static_parameters.iteritems():
240
 
                temp_dic[key] = value
241
 
            return temp_dic 
242
 
        else:
243
 
            raise NotImplemented("_static_parameters not implemented for " + \
244
 
                                 "backend %s" % type(cls))
245
 
 
246
 
    def __init__(self, parameters):
247
 
        """
248
 
        Instantiates a new backend. Please note that this is called also for
249
 
        disabled backends. Those are not initialized, so you might want to check
250
 
        out the initialize() function.
251
 
        """
252
 
        if self.KEY_DEFAULT_BACKEND not in parameters:
253
 
            parameters[self.KEY_DEFAULT_BACKEND] = True
254
 
        if parameters[self.KEY_DEFAULT_BACKEND] or \
255
 
                (not self.KEY_ATTACHED_TAGS in parameters and \
256
 
                self._general_description[self.BACKEND_TYPE] \
257
 
                                        == self.TYPE_READWRITE):
258
 
            parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
259
 
        self._parameters = parameters
260
 
        self._signal_manager = BackendSignals()
261
 
        self._is_initialized = False
262
 
        if Log.is_debugging_mode():
263
 
            self.timer_timestep = 5
264
 
        else:
265
 
            self.timer_timestep = 1 
266
 
        self.to_set_timer = None
267
 
        self.please_quit = False
268
 
        self.to_set = deque()
269
 
        self.to_remove = deque()
270
 
 
271
 
    def get_attached_tags(self):
272
 
        '''
273
 
        Returns the list of tags which are handled by this backend
274
 
        '''
275
 
        if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
276
 
                   self._parameters[self.KEY_DEFAULT_BACKEND]:
277
 
            return [self.ALLTASKS_TAG]
278
 
        try:
279
 
            return self._parameters[self.KEY_ATTACHED_TAGS]
280
 
        except:
281
 
            return []
282
 
 
283
 
    def set_attached_tags(self, tags):
284
 
        '''
285
 
        Changes the set of attached tags
286
 
        '''
287
 
        self._parameters[self.KEY_ATTACHED_TAGS] = tags
288
 
 
289
 
    @classmethod
290
 
    def get_static_parameters(cls):
291
 
        """
292
 
        Returns a dictionary of parameters necessary to create a backend.
293
 
        """
294
 
        return cls._get_static_parameters()
295
 
 
296
 
    def get_parameters(self):
297
 
        """
298
 
        Returns a dictionary of the current parameters.
299
 
        """
300
 
        return self._parameters
301
 
 
302
 
    def set_parameter(self, parameter, value):
303
 
        self._parameters[parameter] = value
304
 
 
305
 
    @classmethod
306
 
    def get_name(cls):
307
 
        """
308
 
        Returns the name of the backend as it should be displayed in the UI
309
 
        """
310
 
        return cls._get_from_general_description(cls.BACKEND_NAME)
311
 
 
312
 
    @classmethod
313
 
    def get_description(cls):
314
 
        """Returns a description of the backend"""
315
 
        return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
316
 
 
317
 
    @classmethod
318
 
    def get_type(cls):
319
 
        """Returns the backend type(readonly, r/w, import, export) """
320
 
        return cls._get_from_general_description(cls.BACKEND_TYPE)
321
 
 
322
 
    @classmethod
323
 
    def get_authors(cls):
324
 
        '''
325
 
        returns the backend author(s)
326
 
        '''
327
 
        return cls._get_from_general_description(cls.BACKEND_AUTHORS)
328
 
 
329
 
    @classmethod
330
 
    def _get_from_general_description(cls, key):
331
 
        '''
332
 
        Helper method to extract values from cls._general_description.
333
 
        Raises an exception if the key is missing (helpful for developers
334
 
        adding new backends).
335
 
        '''
336
 
        if key in cls._general_description:
337
 
            return cls._general_description[key]
338
 
        else:
339
 
            raise NotImplemented("Key %s is missing from " +\
340
 
                    "'self._general_description' of a backend (%s). " +
341
 
                    "Please add the corresponding value" % (key, type(cls)))
342
 
 
343
 
    @classmethod
344
 
    def cast_param_type_from_string(cls, param_value, param_type):
345
 
        '''
346
 
        Parameters are saved in a text format, so we have to cast them to the
347
 
        appropriate type on loading. This function does exactly that.
348
 
        '''
349
 
        #FIXME: we could use pickle (dumps and loads), at least in some cases
350
 
        #       (invernizzi)
351
 
        if param_type in cls._type_converter:
352
 
            return cls._type_converter[param_type](param_value)
353
 
        elif param_type == cls.TYPE_BOOL:
354
 
            if param_value == "True":
355
 
                return True
356
 
            elif param_value == "False":
357
 
                return False
358
 
            else:
359
 
                raise Exception("Unrecognized bool value '%s'" %
360
 
                                 param_type)
361
 
        elif param_type == cls.TYPE_PASSWORD:
362
 
            if param_value == -1:
363
 
                return None
364
 
            return Keyring().get_password(int(param_value))
365
 
        elif param_type == cls.TYPE_LIST_OF_STRINGS:
366
 
            the_list = param_value.split(",")
367
 
            if not isinstance(the_list, list):
368
 
                the_list = [the_list]
369
 
            return the_list
370
 
        else:
371
 
            raise NotImplemented("I don't know what type is '%s'" %
372
 
                                 param_type)
373
 
 
374
 
    def cast_param_type_to_string(self, param_type, param_value):
375
 
        '''
376
 
        Inverse of cast_param_type_from_string
377
 
        '''
378
 
        if param_type == GenericBackend.TYPE_PASSWORD:
379
 
            if param_value == None:
380
 
                return str(-1)
381
 
            else:
382
 
                return str(Keyring().set_password(
383
 
                    "GTG stored password -" + self.get_id(), param_value))
384
 
        elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
385
 
            if param_value == []:
386
 
                return ""
387
 
            return reduce(lambda a, b: a + "," + b, param_value)
388
 
        else:
389
 
            return str(param_value)
390
 
 
391
 
    def get_id(self):
392
 
        '''
393
 
        returns the backends id, used in the datastore for indexing backends
394
 
        '''
395
 
        return self.get_name() + "@" + self._parameters["pid"]
396
 
 
397
 
    @classmethod
398
 
    def get_human_default_name(cls):
399
 
        '''
400
 
        returns the user friendly default backend name. 
401
 
        '''
402
 
        return cls._general_description[cls.BACKEND_HUMAN_NAME]
403
 
 
404
 
    def get_human_name(self):
405
 
        '''
406
 
        returns the user customized backend name. If the user hasn't
407
 
        customized it, returns the default one
408
 
        '''
409
 
        if self.KEY_HUMAN_NAME in self._parameters and \
410
 
                    self._parameters[self.KEY_HUMAN_NAME] != "":
411
 
            return self._parameters[self.KEY_HUMAN_NAME]
412
 
        else:
413
 
            return self.get_human_default_name()
414
 
 
415
 
    def set_human_name(self, name):
416
 
        '''
417
 
        sets a custom name for the backend
418
 
        '''
419
 
        self._parameters[self.KEY_HUMAN_NAME] = name
420
 
        #we signal the change
421
 
        self._signal_manager.backend_renamed(self.get_id())
422
 
 
423
 
    def is_enabled(self):
424
 
        '''
425
 
        Returns if the backend is enabled
426
 
        '''
427
 
        return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
428
 
               self.is_default()
429
 
 
430
 
    def is_default(self):
431
 
        '''
432
 
        Returns if the backend is enabled
433
 
        '''
434
 
        return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
435
 
 
436
 
    def is_initialized(self):
437
 
        '''
438
 
        Returns if the backend is up and running
439
 
        '''
440
 
        return self._is_initialized
441
 
 
442
 
    def get_parameter_type(self, param_name):
443
 
        try:
444
 
            return self.get_static_parameters()[param_name][self.PARAM_TYPE]
445
 
        except KeyError:
446
 
            return None
447
 
 
448
 
    def register_datastore(self, datastore):
449
 
        self.datastore = datastore
450
 
 
451
 
###############################################################################
452
 
### HELPER FUNCTIONS ##########################################################
453
 
###############################################################################
454
 
 
455
 
    def _store_pickled_file(self, path, data):
456
 
        '''
457
 
        A helper function to save some object in a file.
458
 
        @param path: a relative path. A good choice is
459
 
        "backend_name/object_name"
460
 
        @param data: the object
461
 
        '''
462
 
        path = os.path.join(CoreConfig().get_data_dir(), path)
463
 
        #mkdir -p
464
 
        try:
465
 
            os.makedirs(os.path.dirname(path))
466
 
        except OSError, exception:
467
 
            if exception.errno != errno.EEXIST: 
468
 
                raise
469
 
        #saving
470
 
        #try:
471
 
        with open(path, 'wb') as file:
472
 
                pickle.dump(data, file)
473
 
                #except pickle.PickleError:
474
 
                    #pass
475
 
 
476
 
    def _load_pickled_file(self, path, default_value = None):
477
 
        '''
478
 
        A helper function to load some object from a file.
479
 
        @param path: the relative path of the file
480
 
        @param default_value: the value to return if the file is missing or
481
 
        corrupt
482
 
        @returns object: the needed object, or default_value
483
 
        '''
484
 
        path = os.path.join(CoreConfig().get_data_dir(), path)
485
 
        if not os.path.exists(path):
486
 
            return default_value
487
 
        else:
488
 
            try:
489
 
                with open(path, 'r') as file:
490
 
                    return pickle.load(file)
491
 
            except pickle.PickleError:
492
 
                print "PICKLE ERROR"
493
 
                return default_value
494
 
 
495
 
###############################################################################
496
 
### THREADING #################################################################
497
 
###############################################################################
498
 
 
499
 
    def __try_launch_setting_thread(self):
500
 
        '''
501
 
        Helper function to launch the setting thread, if it's not running.
502
 
        '''
503
 
        if self.to_set_timer == None and self.is_enabled():
504
 
            self.to_set_timer = threading.Timer(self.timer_timestep, \
505
 
                                        self.launch_setting_thread)
506
 
            self.to_set_timer.start()
507
 
 
508
 
    def launch_setting_thread(self):
509
 
        '''
510
 
        This function is launched as a separate thread. Its job is to perform
511
 
        the changes that have been issued from GTG core. In particular, for
512
 
        each task in the self.to_set queue, a task has to be modified or to be
513
 
        created (if the tid is new), and for each task in the self.to_remove
514
 
        queue, a task has to be deleted
515
 
        '''
516
 
        while not self.please_quit:
517
 
            try:
518
 
                task = self.to_set.pop()
519
 
            except IndexError:
520
 
                break
521
 
            #time.sleep(4)
522
 
            tid = task.get_id()
523
 
            if tid  not in self.to_remove:
524
 
                self.set_task(task)
525
 
 
526
 
        while not self.please_quit:
527
 
            try:
528
 
                tid = self.to_remove.pop()
529
 
            except IndexError:
530
 
                break
531
 
            self.remove_task(tid)
532
 
        #we release the weak lock
533
 
        self.to_set_timer = None
534
 
 
535
 
    def queue_set_task(self, task):
536
 
        ''' Save the task in the backend. '''
537
 
        tid = task.get_id()
538
 
        print "SETTING", task.get_title()
539
 
        if task not in self.to_set and tid not in self.to_remove:
540
 
            self.to_set.appendleft(task)
541
 
            self.__try_launch_setting_thread()
542
 
 
543
 
    def queue_remove_task(self, tid):
544
 
        '''
545
 
        Queues task to be removed.
546
 
        @param tid: The Task ID of the task to be removed
547
 
        '''
548
 
        if tid not in self.to_remove:
549
 
            self.to_remove.appendleft(tid)
550
 
            self.__try_launch_setting_thread()
551
 
            return None
552
 
 
553
 
    def sync(self):
554
 
        '''
555
 
        Helper method. Forces the backend to perform all the pending changes.
556
 
        It is usually called upon quitting the backend.
557
 
        '''
558
 
        #FIXME: this function should become part of the r/w r/o generic class
559
 
        #  for backends
560
 
        if self.to_set_timer != None:
561
 
            self.please_quit = True
562
 
            try:
563
 
                self.to_set_timer.cancel()
564
 
                self.to_set_timer.join()
565
 
            except:
566
 
                pass
567
 
        self.please_quit = False
568
 
        self.launch_setting_thread()
569
 
        self.save_state()
570