database integration: first draft

- using SQLite as backend

/!\ Not finished yet, break execution. SàT CAN'T LAUNCH PROPERLY IN THE CURRENT STATE
This commit is contained in:
Goffi 2011-10-30 23:13:40 +01:00
commit 1982eb79d8
4 changed files with 235 additions and 72 deletions

View file

@ -112,7 +112,6 @@ class SAT(service.Service):
self.trigger = TriggerManager() #trigger are used to change SàT behaviour
self.bridge=DBusBridge()
self.bridge.register("getVersion", lambda: self.get_const('client_version'))
self.bridge.register("getProfileName", self.memory.getProfileName)
@ -148,7 +147,12 @@ class SAT(service.Service):
self.bridge.register("getMenus", self.getMenus)
self.bridge.register("getMenuHelp", self.getMenuHelp)
self.bridge.register("callMenu", self.callMenu)
self.memory.initialized.addCallback(self._postMemoryInit)
def _postMemoryInit(self, ignore):
"""Method called after memory initialization is done"""
info(_("Memory initialised"))
self._import_plugins()
@ -156,8 +160,8 @@ class SAT(service.Service):
"""Import all plugins found in plugins directory"""
import sat.plugins
plugins_path = os.path.dirname(sat.plugins.__file__)
plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob (os.path.join(plugins_path,"plugin*.py")))]
__plugins_to_import = {} #plugins will still have to import
plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob(os.path.join(plugins_path,"plugin*.py")))]
__plugins_to_import = {} #plugins we still have to import
for plug in plug_lst:
plugin_path = 'sat.plugins.'+plug
__import__(plugin_path)
@ -215,37 +219,44 @@ class SAT(service.Service):
if callback:
callback()
return
current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile),
self.memory.getParamA("Password", "Connection", profile_key = profile),
self.memory.getParamA("Server", "Connection", profile_key = profile), 5222)
if callback and errback:
current.getConnectionDeferred().addCallbacks(lambda x:callback(), errback)
def afterMemoryInit(ignore):
"""This part must be called when we have loaded individual parameters from memory"""
current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key = profile), profile),
self.memory.getParamA("Password", "Connection", profile_key = profile),
self.memory.getParamA("Server", "Connection", profile_key = profile), 5222)
current.messageProt = xmpp.SatMessageProtocol(self)
current.messageProt.setHandlerParent(current)
current.roster = xmpp.SatRosterProtocol(self)
current.roster.setHandlerParent(current)
if callback and errback:
current.getConnectionDeferred().addCallbacks(lambda x:callback(), errback)
current.presence = xmpp.SatPresenceProtocol(self)
current.presence.setHandlerParent(current)
current.messageProt = xmpp.SatMessageProtocol(self)
current.messageProt.setHandlerParent(current)
current.roster = xmpp.SatRosterProtocol(self)
current.roster.setHandlerParent(current)
current.fallBack = xmpp.SatFallbackHandler(self)
current.fallBack.setHandlerParent(current)
current.presence = xmpp.SatPresenceProtocol(self)
current.presence.setHandlerParent(current)
current.versionHandler = xmpp.SatVersionHandler(self.get_const('client_name'),
self.get_const('client_version'))
current.versionHandler.setHandlerParent(current)
current.fallBack = xmpp.SatFallbackHandler(self)
current.fallBack.setHandlerParent(current)
debug (_("setting plugins parents"))
for plugin in self.plugins.iteritems():
if plugin[1].is_handler:
plugin[1].getHandler(profile).setHandlerParent(current)
current.versionHandler = xmpp.SatVersionHandler(self.get_const('client_name'),
self.get_const('client_version'))
current.versionHandler.setHandlerParent(current)
current.startService()
debug (_("setting plugins parents"))
for plugin in self.plugins.iteritems():
if plugin[1].is_handler:
plugin[1].getHandler(profile).setHandlerParent(current)
current.startService()
params_defer = self.memory.loadIndividualParams(profile).addCallback(afterMemoryInit)
if errback:
params_defer.addErrback(errback)
def disconnect(self, profile_key):
"""disconnect from jabber server"""

View file

@ -444,7 +444,7 @@ class ImapServerFactory(protocol.ServerFactory):
debug (_("IMAP server connection lost (reason: %s)"), reason)
def buildProtocol(self, addr):
debug ("Building protocole")
debug ("Building protocol")
prot = protocol.ServerFactory.buildProtocol(self, addr)
prot.portal = portal.Portal(ImapRealm(self.host))
prot.portal.registerChecker(SatProfileCredentialChecker(self.host))

View file

@ -27,16 +27,17 @@ import cPickle as pickle
from ConfigParser import SafeConfigParser, NoOptionError, NoSectionError
from xml.dom import minidom
from logging import debug, info, warning, error
import pdb
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from sat.tools.xml_tools import paramsXml2xmlUI
from sat.core.default_config import default_config
from sat.tools.sqlite import SqliteStorage
SAVEFILE_PARAM_XML="/param" #xml parameters template
SAVEFILE_PARAM_DATA="/param" #individual & general parameters; _ind and _gen suffixes will be added
SAVEFILE_HISTORY="/history"
SAVEFILE_PRIVATE="/private" #file used to store misc values (mainly for plugins)
SAVEFILE_DATABASE="/sat.db"
class Param():
"""This class manage parameters with xml"""
@ -75,26 +76,20 @@ class Param():
"""Load parameters template from file"""
self.dom = minidom.parse(file)
def load_data(self, file):
"""Load parameters data from file"""
file_ind = file + '_ind'
file_gen = file + '_gen'
if os.path.exists(file_gen):
try:
with open(file_gen, 'r') as file_gen_pickle:
self.params_gen=pickle.load(file_gen_pickle)
debug(_("general params data loaded"))
except:
error (_("Can't load general params data !"))
def loadGenData(self, storage):
"""Load general parameters data from storage
@param storage: xxxStorage object instance
@return: deferred triggered once params are loaded"""
return storage.loadGenParams(self.params_gen)
if os.path.exists(file_ind):
try:
with open(file_ind, 'r') as file_ind_pickle:
self.params=pickle.load(file_ind_pickle)
debug(_("individual params data loaded"))
except:
error (_("Can't load individual params data !"))
def loadIndData(self, storage, profile):
"""Load individual parameters
set self.params cache
@param storage: xxxStorage object instance
@param profile: profile to load (*must exist*)
@return: deferred triggered once params are loaded"""
self.params[profile] = {}
return storage.loadIndParams(self.params, profile)
def save_xml(self, file):
"""Save parameters template to xml file"""
@ -124,28 +119,25 @@ class Param():
host.set_const('savefile_param_data', SAVEFILE_PARAM_DATA)
host.registerGeneralCB("registerNewAccount", host.registerNewAccountCB)
def getProfilesList(self):
return self.params.keys()
def createProfile(self, name):
"""Create a new profile
@param name: Name of the profile"""
if self.params.has_key(name):
if self.storage.hasProfile(name):
info (_('The profile name already exists'))
return 1
return True
if not self.host.trigger.point("ProfileCreation", name):
return 0
return False
self.params[name]={}
return 0
return False
def deleteProfile(self, name):
"""Delete an existing profile
@param name: Name of the profile"""
if not self.params.has_key(name):
if not self.storage.hasProfile(name):
error (_('Trying to delete an unknown profile'))
return 1
return True
del self.params[name]
return 0
return False
def getProfileName(self, profile_key):
"""return profile according to profile_key
@ -159,10 +151,10 @@ class Param():
default = self.host.memory.getPrivate('Profile_default')
if not default or not default in self.params:
info(_('No default profile, returning first one')) #TODO: manage real default profile
default = self.params.keys()[0]
default = self.getProfilesList()[0]
self.host.memory.setPrivate('Profile_default', default)
return default #FIXME: temporary, must use real default value, and fallback to first one if it doesn't exists
if not self.params.has_key(profile_key):
if not self.storage.hasProfile(profile_key):
info (_('Trying to access an unknown profile'))
return ""
return profile_key
@ -396,6 +388,7 @@ class Param():
if node[0] == 'general':
self.params_gen[(category, name)] = value
self.storage.setGenParam(category, name, value)
for profile in self.getProfilesList():
if self.host.isConnected(profile):
self.host.bridge.paramUpdate(name, value, category, profile)
@ -408,14 +401,18 @@ class Param():
if type=="button":
print "clique",node.toxml()
else:
self.params[profile][(category, name)] = value
self.host.bridge.paramUpdate(name, value, category, profile) #TODO: add profile in signal
if self.host.isConnected(profile): #key can not exists if profile is not connected
self.params[profile][(category, name)] = value
self.host.bridge.paramUpdate(name, value, category, profile)
self.storage.setIndParam(category, name, value, profile)
class Memory:
"""This class manage all persistent informations"""
def __init__(self, host):
info (_("Memory manager init"))
self.initialized = defer.Deferred()
init_defers = [] #list of deferred to wait before initialization is finished
self.host = host
self.contacts={}
self.presenceStatus={}
@ -429,7 +426,14 @@ class Memory:
self.config = self.parseMainConf()
host.set_const('savefile_history', SAVEFILE_HISTORY)
host.set_const('savefile_private', SAVEFILE_PRIVATE)
self.load()
host.set_const('savefile_database', SAVEFILE_DATABASE)
database_file = os.path.expanduser(self.getConfig('','local_dir')+
self.host.get_const('savefile_database'))
self.loadFiles()
self.storage = SqliteStorage(database_file)
self.storage.initialized.addCallback(lambda ignore: self.load(init_defers))
defer.DeferredList(init_defers).chainDeferred(self.initialized)
def parseMainConf(self):
"""look for main .ini configuration file, and parse it"""
@ -455,7 +459,8 @@ class Memory:
return os.path.expanduser(_value) if name.endswith('_path') or name.endswith('_dir') else _value
def load(self):
def loadFiles(self):
"""Load parameters and all memory things from file/db"""
param_file_xml = os.path.expanduser(self.getConfig('','local_dir')+
self.host.get_const('savefile_param_xml'))
@ -466,7 +471,7 @@ class Memory:
private_file = os.path.expanduser(self.getConfig('','local_dir')+
self.host.get_const('savefile_private'))
#parameters
#parameters template
if os.path.exists(param_file_xml):
try:
self.params.load_xml(param_file_xml)
@ -478,12 +483,7 @@ class Memory:
info (_("No params template, using default template"))
self.params.load_default_params()
try:
self.params.load_data(param_file_data)
debug(_("params loaded"))
except:
error (_("Can't load params !"))
#history
if os.path.exists(history_file):
try:
@ -502,6 +502,22 @@ class Memory:
except:
error (_("Can't load private values !"))
def load(self, init_defers):
"""Load parameters and all memory things from db
@param init_defers: list of deferred to wait before parameters are loaded"""
#parameters data
init_defers.append(self.params.loadGenData(self.storage))
def loadIndividualParams(self, profile_key):
"""Load individual parameters for a profile
@param profile_key: %(doc_profile_key)s"""
profile = self.getProfileName(profile_key)
if not profile:
error (_('Trying to load parameters for a non-existant profile'))
raise Exception("Profile doesn't exist")
return self.params.loadIndParams(profile)
def save(self):
"""Save parameters and all memory things to file/db"""
#TODO: need to encrypt files (at least passwords !) and set permissions
@ -525,7 +541,7 @@ class Memory:
debug(_("private values saved"))
def getProfilesList(self):
return self.params.getProfilesList()
return self.storage.getProfilesList()
def getProfileName(self, profile_key):

136
src/tools/sqlite.py Normal file
View file

@ -0,0 +1,136 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
SAT: a jabber client
Copyright (C) 2009, 2010, 2011 Jérôme Poisson (goffi@goffi.org)
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/>.
"""
from logging import debug, info, warning, error
from twisted.enterprise import adbapi
from twisted.internet import defer
import os.path
class SqliteStorage():
"""This class manage storage with Sqlite database"""
def __init__(self, db_filename):
"""Connect to the given database
@param db_filename: full path to the Sqlite database"""
self.initialized = defer.Deferred() #triggered when memory is fully initialised and ready
init_defers = [] #list of deferred we have to wait to before initialisation is complete
self.profiles={} #we keep cache for the profiles (key: profile name, value: profile id)
info(_("Connecting database"))
new_base = not os.path.exists(db_filename) #do we have to create the database ?
self.dbpool = adbapi.ConnectionPool("sqlite3", db_filename, check_same_thread=False)
init_defers.append(self.dbpool.runOperation("PRAGMA foreign_keys = ON").addErrback(lambda x: error(_("Can't activate foreign keys"))))
if new_base:
info(_("The database is new, creating the tables"))
database_creation = [
"CREATE TABLE profiles (id INTEGER PRIMARY KEY ASC, name TEXT, UNIQUE (name))",
"CREATE TABLE historic (id INTEGER PRIMARY KEY ASC, profile_id INTEGER, source TEXT, dest TEXT, source_res TEXT, dest_res TEXT, timestamp DATETIME, message TEXT, FOREIGN KEY(profile_id) REFERENCES profiles(id))",
"CREATE TABLE param_gen (category TEXT, name TEXT, value TEXT, PRIMARY KEY (category,name))",
"CREATE TABLE param_ind (category TEXT, name TEXT, profile_id INTEGER, value TEXT, PRIMARY KEY (category,name,profile_id), FOREIGN KEY(profile_id) REFERENCES profiles(id))"]
for op in database_creation:
d = self.dbpool.runOperation(op)
d.addErrback(lambda x: error(_("Error while creating tables in database [QUERY: %s]") % op ))
init_defers.append(d)
def fillProfileCache(ignore):
d = self.dbpool.runQuery("SELECT name,id FROM profiles").addCallback(self._profilesCache)
d.chainDeferred(self.initialized)
defer.DeferredList(init_defers).addCallback(fillProfileCache)
#Profiles
def _profilesCache(self, profiles_result):
"""Fill the profiles cache
@param profiles_result: result of the sql profiles query"""
for profile in profiles_result:
name, id = profile
self.profiles[name] = id
def getProfilesList(self):
""""Return list of all registered profiles"""
return self.profiles.keys()
def hasProfile(self, profile_name):
"""return True if profile_name exists
@param profile_name: name of the profile to check"""
return self.profiles.has_key(profile_name)
def createProfile(self, name):
"""Create a new profile
@param name: name of the profile
@return: deferred triggered once profile is actually created"""
def getProfileId(ignore):
return self.dbpool.runQuery("SELECT (id) FROM profiles WHERE name = ?", (name,))
def profile_created(profile_id):
_id = profile_id[0][0]
self.profiles[name] = _id #we synchronise the cache
d = self.dbpool.runQuery("INSERT INTO profiles(name) VALUES (?)", (name,))
d.addCallback(getProfileId)
d.addCallback(profile_created)
d.addErrback(lambda ignore: error(_("Can't create profile %(name)s" % {"name":name})))
return d
#Params
def loadGenParams(self, params_gen):
"""Load general parameters
@param params_gen: dictionary to fill
@return: deferred"""
def fillParams(result):
params_gen[(category, name)] = value
debug(_("loading general parameters from database"))
return self.dbpool.runQuery("SELECT category,name,value FROM param_gen").addCallback(fillParams)
def loadIndParams(self, params_ind, profile):
"""Load general parameters
@param params_ind: dictionary to fill
@param profile: a profile which *must* exist
@return: deferred"""
def fillParams(result):
params_ind[profile][(category, name)] = value
debug(_("loading individual parameters from database"))
d = self.dbpool.runQuery("SELECT category,name,value FROM param_gen WHERE profile_id=?", self.profiles[profile])
d.addCallback(fillParams)
return d
def setGenParam(self, category, name, value):
"""Save the general parameters in database
@param category: category of the parameter
@param name: name of the parameter
@param value: value to set
@return: deferred"""
d = self.dbpool.runQuery("REPLACE INTO param_gen(category,name,value) VALUES (?,?,?)", (category, name, value))
d.addErrback(lambda ignore: error(_("Can't set general parameter (%(category)s/%(name)s) in database" % {"category":category, "name":name})))
return d
def setIndParam(self, category, name, value, profile):
"""Save the general parameters in database
@param category: category of the parameter
@param name: name of the parameter
@param value: value to set
@param profile: a profile which *must* exist
@return: deferred"""
d = self.dbpool.runQuery("REPLACE INTO param_ind(category,name,profile_id,value) VALUES (?,?,?,?)", (category, name, self.profiles[profile], value))
d.addErrback(lambda ignore: error(_("Can't set individual parameter (%(category)s/%(name)s) for [%(profile)s] in database" % {"category":category, "name":name, "profile":profile})))
return d