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
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
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
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
# -----------------------------------------------------------------------------
21
This file contains the most generic representation of a backend, the
30
from collections import deque
32
from GTG.backends.backendsignals import BackendSignals
33
from GTG.tools.keyring import Keyring
34
from GTG.core import CoreConfig
35
from GTG.tools.logger import Log
36
from GTG.tools.interruptible import _cancellation_point
40
class GenericBackend(object):
42
Base class for every backend.
43
It defines the interface a backend must have and takes care of all the
44
operations common to all backends.
45
A particular backend should redefine all the methods marked as such.
49
###########################################################################
50
### BACKEND INTERFACE #####################################################
51
###########################################################################
53
#General description of the backend: these parameters are used
54
#to show a description of the backend to the user when s/he is
55
#considering adding it.
56
# For an example, see the GTG/backends/backend_localfile.py file
57
#_general_description has this format:
58
#_general_description = {
59
# GenericBackend.BACKEND_NAME: "backend_unique_identifier", \
60
# GenericBackend.BACKEND_HUMAN_NAME: _("Human friendly name"), \
61
# GenericBackend.BACKEND_AUTHORS: ["First author", \
63
# GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_READWRITE, \
64
# GenericBackend.BACKEND_DESCRIPTION: \
65
# _("Short description of the backend"),\
67
# The complete list of constants and their meaning is given below.
68
_general_description = {}
70
#These are the parameters to configure a new backend of this type. A
71
# parameter has a name, a type and a default value.
72
# For an example, see the GTG/backends/backend_localfile.py file
73
#_static_parameters has this format:
74
#_static_parameters = { \
76
# GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
77
# GenericBackend.PARAM_DEFAULT_VALUE: "my default value",
80
# GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
81
# GenericBackend.PARAM_DEFAULT_VALUE: 42,
83
# The complete list of constants and their meaning is given below.
84
_static_parameters = {}
88
Called each time it is enabled (including on backend creation).
89
Please note that a class instance for each disabled backend *is*
90
created, but it's not initialized.
92
NOTE: make sure to call super().initialize()
94
#NOTE: I'm disabling this since support for runtime checking of the
95
# presence of the necessary modules is disabled. (invernizzi)
96
# for module_name in self.get_required_modules():
97
# sys.modules[module_name]= __import__(module_name)
98
self._parameters[self.KEY_ENABLED] = True
99
self._is_initialized = True
100
#we signal that the backend has been enabled
101
self._signal_manager.backend_state_changed(self.get_id())
103
def start_get_tasks(self):
105
This function starts submitting the tasks from the backend into GTG
107
It's run as a separate thread.
109
@return: start_get_tasks() might not return or finish
113
def set_task(self, task):
115
This function is called from GTG core whenever a task should be
116
saved, either because it's a new one or it has been modified.
117
If the task id is new for the backend, then a new task must be
118
created. No special notification that the task is a new one is given.
120
@param task: the task object to save
124
def remove_task(self, tid):
125
''' This function is called from GTG core whenever a task must be
126
removed from the backend. Note that the task could be not present here.
128
@param tid: the id of the task to delete
132
def this_is_the_first_run(self, xml):
134
Optional, and almost surely not needed.
135
Called upon the very first GTG startup.
136
This function is needed only in the default backend (XML localfile,
138
The xml parameter is an object containing GTG default tasks.
140
@param xml: an xml object containing the default tasks.
144
#NOTE: task counting is disabled in the UI, so I've disabled it here
146
# def get_number_of_tasks(self):
148
# Returns the number of tasks stored in the backend. Doesn't need
149
# to be a fast function, is called just for the UI
151
# raise NotImplemented()
153
#NOTE: I'm disabling this since support for runtime checking of the
154
# presence of the necessary modules is disabled. (invernizzi)
156
# def get_required_modules():
159
def quit(self, disable = False):
161
Called when GTG quits or the user wants to disable the backend.
163
@param disable: If disable is True, the backend won't
164
be automatically loaded when GTG starts
166
self._is_initialized = False
168
self._parameters[self.KEY_ENABLED] = False
169
#we signal that we have been disabled
170
self._signal_manager.backend_state_changed(self.get_id())
171
self._signal_manager.backend_sync_ended(self.get_id())
172
syncing_thread = threading.Thread(target = self.sync).run()
174
def save_state(self):
176
It's the last function executed on a quitting backend, after the
177
pending actions have been done.
178
Useful to ensure that the state is saved in a consistent manner
182
###############################################################################
183
###### You don't need to reimplement the functions below this line ############
184
###############################################################################
186
###########################################################################
187
### CONSTANTS #############################################################
188
###########################################################################
189
#BACKEND TYPE DESCRIPTION
190
# Each backend must have a "_general_description" attribute, which
191
# is a dictionary that holds the values for the following keys.
192
BACKEND_NAME = "name" #the backend gtg internal name (doesn't change in
193
# translations, *must be unique*)
194
BACKEND_HUMAN_NAME = "human-friendly-name" #The name shown to the user
195
BACKEND_DESCRIPTION = "description" #A short description of the backend
196
BACKEND_AUTHORS = "authors" #a list of strings
197
BACKEND_TYPE = "type"
198
#BACKEND_TYPE is one of:
199
TYPE_READWRITE = "readwrite"
200
TYPE_READONLY = "readonly"
201
TYPE_IMPORT = "import"
202
TYPE_EXPORT = "export"
205
#"static_parameters" is a dictionary of dictionaries, each of which
206
# are a description of a parameter needed to configure the backend and
207
# is identified in the outer dictionary by a key which is the name of the
209
# For an example, see the GTG/backends/backend_localfile.py file
210
# Each dictionary contains the keys:
211
PARAM_DEFAULT_VALUE = "default_value" # its default value
213
#PARAM_TYPE is one of the following (changing this changes the way
214
# the user can configure the parameter)
215
TYPE_PASSWORD = "password" #the real password is stored in the GNOME
217
# This is just a key to find it there
218
TYPE_STRING = "string" #generic string, nothing fancy is done
219
TYPE_INT = "int" #edit box can contain only integers
220
TYPE_BOOL = "bool" #checkbox is shown
221
TYPE_LIST_OF_STRINGS = "liststring" #list of strings. the "," character is
222
# prohibited in strings
224
#These parameters are common to all backends and necessary.
225
# They will be added automatically to your _static_parameters list
226
#NOTE: for now I'm disabling changing the default backend. Once it's all
227
# set up, we will see about that (invernizzi)
228
KEY_DEFAULT_BACKEND = "Default"
229
KEY_ENABLED = "Enabled"
230
KEY_HUMAN_NAME = BACKEND_HUMAN_NAME
231
KEY_ATTACHED_TAGS = "attached-tags"
234
ALLTASKS_TAG = "gtg-tags-all" #NOTE: this has been moved here to avoid
235
# circular imports. It's the same as in
236
# the CoreConfig class, because it's the
237
# same thing conceptually. It doesn't
238
# matter it the naming diverges.
240
_static_parameters_obligatory = { \
241
KEY_DEFAULT_BACKEND: { \
242
PARAM_TYPE: TYPE_BOOL, \
243
PARAM_DEFAULT_VALUE: False, \
246
PARAM_TYPE: TYPE_STRING, \
247
PARAM_DEFAULT_VALUE: "", \
250
PARAM_TYPE: TYPE_STRING, \
251
PARAM_DEFAULT_VALUE: "", \
254
PARAM_TYPE: TYPE_STRING, \
255
PARAM_DEFAULT_VALUE: "", \
258
PARAM_TYPE: TYPE_BOOL, \
259
PARAM_DEFAULT_VALUE: False, \
262
_static_parameters_obligatory_for_rw = { \
263
KEY_ATTACHED_TAGS: {\
264
PARAM_TYPE: TYPE_LIST_OF_STRINGS, \
265
PARAM_DEFAULT_VALUE: [ALLTASKS_TAG], \
268
#Handy dictionary used in type conversion (from string to type)
269
_type_converter = {TYPE_STRING: str,
274
def _get_static_parameters(cls):
276
Helper method, used to obtain the full list of the static_parameters
277
(user configured and default ones)
279
@returns dict: the dict containing all the static parameters
281
temp_dic = cls._static_parameters_obligatory.copy()
282
if cls._general_description[cls.BACKEND_TYPE] == \
285
cls._static_parameters_obligatory_for_rw.iteritems():
286
temp_dic[key] = value
287
for key, value in cls._static_parameters.iteritems():
288
temp_dic[key] = value
291
def __init__(self, parameters):
293
Instantiates a new backend. Please note that this is called also
294
for disabled backends. Those are not initialized, so you might
295
want to check out the initialize() function.
297
if self.KEY_DEFAULT_BACKEND not in parameters:
298
#if it's not specified, then this is the default backend
299
#(for retro-compatibility with the GTG 0.2 series)
300
parameters[self.KEY_DEFAULT_BACKEND] = True
301
#default backends should get all the tasks
302
if parameters[self.KEY_DEFAULT_BACKEND] or \
303
(not self.KEY_ATTACHED_TAGS in parameters and \
304
self._general_description[self.BACKEND_TYPE] \
305
== self.TYPE_READWRITE):
306
parameters[self.KEY_ATTACHED_TAGS] = [self.ALLTASKS_TAG]
307
self._parameters = parameters
308
self._signal_manager = BackendSignals()
309
self._is_initialized = False
310
#if debugging mode is enabled, tasks should be saved as soon as they're
311
# marked as modified. If in normal mode, we prefer speed over easier
313
if Log.is_debugging_mode():
314
self.timer_timestep = 5
316
self.timer_timestep = 1
317
self.to_set_timer = None
318
self.please_quit = False
319
self.cancellation_point = lambda: _cancellation_point(\
320
lambda: self.please_quit)
321
self.to_set = deque()
322
self.to_remove = deque()
324
def get_attached_tags(self):
326
Returns the list of tags which are handled by this backend
328
if hasattr(self._parameters, self.KEY_DEFAULT_BACKEND) and \
329
self._parameters[self.KEY_DEFAULT_BACKEND]:
330
#default backends should get all the tasks
331
#NOTE: this shouldn't be needed, but it doesn't cost anything and it
332
# could avoid potential tasks losses.
333
return [self.ALLTASKS_TAG]
335
return self._parameters[self.KEY_ATTACHED_TAGS]
339
def set_attached_tags(self, tags):
341
Changes the set of attached tags
343
@param tags: the new attached_tags set
345
self._parameters[self.KEY_ATTACHED_TAGS] = tags
348
def get_static_parameters(cls):
350
Returns a dictionary of parameters necessary to create a backend.
352
return cls._get_static_parameters()
354
def get_parameters(self):
356
Returns a dictionary of the current parameters.
358
return self._parameters
360
def set_parameter(self, parameter, value):
362
Change a parameter for this backend
364
@param parameter: the parameter name
365
@param value: the new value
367
self._parameters[parameter] = value
372
Returns the name of the backend as it should be displayed in the UI
374
return cls._get_from_general_description(cls.BACKEND_NAME)
377
def get_description(cls):
378
"""Returns a description of the backend"""
379
return cls._get_from_general_description(cls.BACKEND_DESCRIPTION)
383
"""Returns the backend type(readonly, r/w, import, export) """
384
return cls._get_from_general_description(cls.BACKEND_TYPE)
387
def get_authors(cls):
389
returns the backend author(s)
391
return cls._get_from_general_description(cls.BACKEND_AUTHORS)
394
def _get_from_general_description(cls, key):
396
Helper method to extract values from cls._general_description.
398
@param key: the key to extract
400
return cls._general_description[key]
403
def cast_param_type_from_string(cls, param_value, param_type):
405
Parameters are saved in a text format, so we have to cast them to the
406
appropriate type on loading. This function does exactly that.
408
@param param_value: the actual value of the parameter, in a string
410
@param param_type: the wanted type
411
@returns something: the casted param_value
413
if param_type in cls._type_converter:
414
return cls._type_converter[param_type](param_value)
415
elif param_type == cls.TYPE_BOOL:
416
if param_value == "True":
418
elif param_value == "False":
421
raise Exception("Unrecognized bool value '%s'" %
423
elif param_type == cls.TYPE_PASSWORD:
424
if param_value == -1:
426
return Keyring().get_password(int(param_value))
427
elif param_type == cls.TYPE_LIST_OF_STRINGS:
428
the_list = param_value.split(",")
429
if not isinstance(the_list, list):
430
the_list = [the_list]
433
raise NotImplemented("I don't know what type is '%s'" %
436
def cast_param_type_to_string(self, param_type, param_value):
438
Inverse of cast_param_type_from_string
440
@param param_value: the actual value of the parameter
441
@param param_type: the type of the parameter (password...)
442
@returns something: param_value casted to string
444
if param_type == GenericBackend.TYPE_PASSWORD:
445
if param_value == None:
448
return str(Keyring().set_password(
449
"GTG stored password -" + self.get_id(), param_value))
450
elif param_type == GenericBackend.TYPE_LIST_OF_STRINGS:
451
if param_value == []:
453
return reduce(lambda a, b: a + "," + b, param_value)
455
return str(param_value)
459
returns the backends id, used in the datastore for indexing backends
461
@returns string: the backend id
463
return self.get_name() + "@" + self._parameters["pid"]
466
def get_human_default_name(cls):
468
returns the user friendly default backend name, without eventual user
471
@returns string: the default "human name"
473
return cls._general_description[cls.BACKEND_HUMAN_NAME]
475
def get_human_name(self):
477
returns the user customized backend name. If the user hasn't
478
customized it, returns the default one.
480
@returns string: the "human name" of this backend
482
if self.KEY_HUMAN_NAME in self._parameters and \
483
self._parameters[self.KEY_HUMAN_NAME] != "":
484
return self._parameters[self.KEY_HUMAN_NAME]
486
return self.get_human_default_name()
488
def set_human_name(self, name):
490
sets a custom name for the backend
492
@param name: the new name
494
self._parameters[self.KEY_HUMAN_NAME] = name
495
#we signal the change
496
self._signal_manager.backend_renamed(self.get_id())
498
def is_enabled(self):
500
Returns if the backend is enabled
504
return self.get_parameters()[GenericBackend.KEY_ENABLED] or \
507
def is_default(self):
509
Returns if the backend is enabled
513
return self.get_parameters()[GenericBackend.KEY_DEFAULT_BACKEND]
515
def is_initialized(self):
517
Returns if the backend is up and running
519
@returns is_initialized
521
return self._is_initialized
523
def get_parameter_type(self, param_name):
525
Given the name of a parameter, returns its type. If the parameter is one
526
of the default ones, it does not have a type: in that case, it returns
529
@param param_name: the name of the parameter
530
@returns string: the type, or None
533
return self.get_static_parameters()[param_name][self.PARAM_TYPE]
537
def register_datastore(self, datastore):
539
Setter function to inform the backend about the datastore that's loading
542
@param datastore: a Datastore
544
self.datastore = datastore
546
###############################################################################
547
### HELPER FUNCTIONS ##########################################################
548
###############################################################################
550
def _store_pickled_file(self, path, data):
552
A helper function to save some object in a file.
554
@param path: a relative path. A good choice is
555
"backend_name/object_name"
556
@param data: the object
558
path = os.path.join(CoreConfig().get_data_dir(), path)
561
os.makedirs(os.path.dirname(path))
562
except OSError, exception:
563
if exception.errno != errno.EEXIST:
566
with open(path, 'wb') as file:
567
pickle.dump(data, file)
569
def _load_pickled_file(self, path, default_value = None):
571
A helper function to load some object from a file.
573
@param path: the relative path of the file
574
@param default_value: the value to return if the file is missing or
576
@returns object: the needed object, or default_value
578
path = os.path.join(CoreConfig().get_data_dir(), path)
579
if not os.path.exists(path):
582
with open(path, 'r') as file:
584
return pickle.load(file)
585
except pickle.PickleError:
586
Log.error("PICKLE ERROR")
589
def _gtg_task_is_syncable_per_attached_tags(self, task):
591
Helper function which checks if the given task satisfies the filtering
592
imposed by the tags attached to the backend.
593
That means, if a user wants a backend to sync only tasks tagged @works,
594
this function should be used to check if that is verified.
596
@returns bool: True if the task should be synced
598
attached_tags = self.get_attached_tags()
599
if GenericBackend.ALLTASKS_TAG in attached_tags:
601
for tag in task.get_tags_name():
602
if tag in attached_tags:
606
###############################################################################
607
### THREADING #################################################################
608
###############################################################################
610
def __try_launch_setting_thread(self):
612
Helper function to launch the setting thread, if it's not running.
614
if self.to_set_timer == None and self.is_enabled():
615
self.to_set_timer = threading.Timer(self.timer_timestep, \
616
self.launch_setting_thread)
617
self.to_set_timer.start()
619
def launch_setting_thread(self, bypass_quit_request = False):
621
This function is launched as a separate thread. Its job is to perform
622
the changes that have been issued from GTG core.
623
In particular, for each task in the self.to_set queue, a task
624
has to be modified or to be created (if the tid is new), and for
625
each task in the self.to_remove queue, a task has to be deleted
627
@param bypass_quit_request: if True, the thread should not be stopped
628
even if asked by self.please_quit = True.
629
It's used when the backend quits, to finish
630
syncing all pending tasks
632
while not self.please_quit or bypass_quit_request:
634
task = self.to_set.pop()
638
if tid not in self.to_remove:
641
while not self.please_quit or bypass_quit_request:
643
tid = self.to_remove.pop()
646
self.remove_task(tid)
647
#we release the weak lock
648
self.to_set_timer = None
650
def queue_set_task(self, task):
651
''' Save the task in the backend. In particular, it just enqueues the
652
task in the self.to_set queue. A thread will shortly run to apply the
655
@param task: the task that should be saved
658
if task not in self.to_set and tid not in self.to_remove:
659
self.to_set.appendleft(task)
660
self.__try_launch_setting_thread()
662
def queue_remove_task(self, tid):
664
Queues task to be removed. In particular, it just enqueues the
665
task in the self.to_remove queue. A thread will shortly run to apply the
668
@param tid: The Task ID of the task to be removed
670
if tid not in self.to_remove:
671
self.to_remove.appendleft(tid)
672
self.__try_launch_setting_thread()
677
Helper method. Forces the backend to perform all the pending changes.
678
It is usually called upon quitting the backend.
680
if self.to_set_timer != None:
681
self.please_quit = True
683
self.to_set_timer.cancel()
687
self.to_set_timer.join()
690
self.launch_setting_thread(bypass_quit_request = True)