Introducing data stores

This commit is contained in:
nemunaire 2015-02-10 00:30:04 +01:00
parent e7fd7c5ec4
commit f66d724d40
6 changed files with 181 additions and 72 deletions

View file

@ -24,6 +24,7 @@ import os
import sys import sys
import nemubot import nemubot
from nemubot import datastore
import nemubot.prompt import nemubot.prompt
from nemubot.prompt.builtins import load_file from nemubot.prompt.builtins import load_file
from nemubot.prompt.reset import PromptReset from nemubot.prompt.reset import PromptReset
@ -91,8 +92,8 @@ if __name__ == "__main__":
logger.error("%s is not a directory", path) logger.error("%s is not a directory", path)
# Create bot context # Create bot context
context = nemubot.Bot(modules_paths=modules_paths, data_path=args.data_path, context = nemubot.Bot(modules_paths=modules_paths, data_store=datastore.XML(args.data_path),
verbosity=args.verbose) verbosity=args.verbose)
if args.no_connect: if args.no_connect:
context.noautoconnect = True context.noautoconnect = True

View file

@ -32,6 +32,7 @@ __version__ = '4.0.dev0'
__author__ = 'nemunaire' __author__ = 'nemunaire'
from nemubot.consumer import Consumer, EventConsumer, MessageConsumer from nemubot.consumer import Consumer, EventConsumer, MessageConsumer
from nemubot import datastore
from nemubot.event import ModuleEvent from nemubot.event import ModuleEvent
from nemubot.hooks.messagehook import MessageHook from nemubot.hooks.messagehook import MessageHook
from nemubot.hooks.manager import HooksManager from nemubot.hooks.manager import HooksManager
@ -45,13 +46,13 @@ class Bot(threading.Thread):
"""Class containing the bot context and ensuring key goals""" """Class containing the bot context and ensuring key goals"""
def __init__(self, ip="127.0.0.1", modules_paths=list(), def __init__(self, ip="127.0.0.1", modules_paths=list(),
data_path=None, verbosity=0): data_store=datastore.Abstract(), verbosity=0):
"""Initialize the bot context """Initialize the bot context
Keyword arguments: Keyword arguments:
ip -- The external IP of the bot (default: 127.0.0.1) ip -- The external IP of the bot (default: 127.0.0.1)
modules_paths -- Paths to all directories where looking for module modules_paths -- Paths to all directories where looking for module
data_path -- Path to directory where store bot context data data_store -- An instance of the nemubot datastore for bot's modules
""" """
threading.Thread.__init__(self) threading.Thread.__init__(self)
@ -65,9 +66,8 @@ class Bot(threading.Thread):
# Context paths # Context paths
self.modules_paths = modules_paths self.modules_paths = modules_paths
self.data_path = None self.datastore = data_store
if data_path is not None: self.datastore.open()
self.set_data_path(data_path)
# Keep global context: servers and modules # Keep global context: servers and modules
self.servers = dict() self.servers = dict()
@ -180,52 +180,6 @@ class Bot(threading.Thread):
logger.exception("Uncatched exception on server read") logger.exception("Uncatched exception on server read")
# Data path
def set_data_path(self, path):
"""Check if the given path is valid and unlock,
then lock the directory and set the variable
Argument:
path -- the location
"""
lock_file = os.path.join(path, ".used_by_nemubot")
if os.path.isdir(path):
if not os.path.exists(lock_file):
if self.data_path is not None:
self.close_data_path(self.data_path)
self.data_path = path
with open(lock_file, 'w') as lf:
lf.write(str(os.getpid()))
return True
else:
with open(lock_file, 'r') as lf:
pid = lf.readline()
raise Exception("Data dir already locked, by PID %s" % pid)
return False
def close_data_path(self, path=None):
"""Release a locked path
Argument:
path -- the location, self.data_path if None
"""
if path is None:
path = self.data_path
lock_file = os.path.join(path, ".used_by_nemubot")
if os.path.isdir(path) and os.path.exists(lock_file):
os.unlink(lock_file)
return True
return False
# Events methods # Events methods
def add_event(self, evt, eid=None, module_src=None): def add_event(self, evt, eid=None, module_src=None):
@ -408,6 +362,13 @@ class Bot(threading.Thread):
if module.__name__ in self.modules: if module.__name__ in self.modules:
self.unload_module(module.__name__) self.unload_module(module.__name__)
# Overwrite print built-in
def prnt(*args):
print("[%s]" % module.__name__, *args)
if hasattr(module, "logger"):
module.logger.info(" ".join(args))
module.print = prnt
self.modules[module.__name__] = module self.modules[module.__name__] = module
return True return True
@ -456,7 +417,7 @@ class Bot(threading.Thread):
def quit(self): def quit(self):
"""Save and unload modules and disconnect servers""" """Save and unload modules and disconnect servers"""
self.close_data_path() self.datastore.close()
if self.event_timer is not None: if self.event_timer is not None:
logger.info("Stop the event timer...") logger.info("Stop the event timer...")
@ -492,9 +453,9 @@ def hotswap(bak):
bak.stop = True bak.stop = True
if bak.event_timer is not None: if bak.event_timer is not None:
bak.event_timer.cancel() bak.event_timer.cancel()
bak.close_data_path() bak.datastore.close()
new = Bot(str(bak.ip), bak.modules_paths, bak.data_path) new = Bot(str(bak.ip), bak.modules_paths, bak.datastore)
new.servers = bak.servers new.servers = bak.servers
new.modules = bak.modules new.modules = bak.modules
new.modules_configuration = bak.modules_configuration new.modules_configuration = bak.modules_configuration

View file

@ -0,0 +1,18 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.datastore.abstract import Abstract
from nemubot.datastore.xml import XML

View file

@ -0,0 +1,61 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from nemubot.tools.xmlparser import module_state
class Abstract:
"""Abstract implementation of a module data store, that always return an
empty set"""
def open(self):
return
def close(self):
return
def load(self, module):
"""Load data for the given module
Argument:
module -- the module name of data to load
Return:
The loaded data
"""
return module_state.ModuleState("nemubotstate")
def save(self, module, data):
"""Load data for the given module
Argument:
module -- the module name of data to load
data -- the new data to save
Return:
Saving status
"""
return True
def __enter__(self):
self.open()
return self
def __exit__(self, type, value, traceback):
self.close()

81
nemubot/datastore/xml.py Normal file
View file

@ -0,0 +1,81 @@
# Nemubot is a smart and modulable IM bot.
# Copyright (C) 2012-2015 Mercier Pierre-Olivier
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from nemubot.datastore.abstract import Abstract
from nemubot.tools.xmlparser import parse_file
class XML(Abstract):
"""A concrete implementation of a data store that relies on XML files"""
def __init__(self, basedir):
self.basedir = basedir
def open(self):
"""Lock the directory"""
if os.path.isdir(self.basedir):
lock_file = os.path.join(self.basedir, ".used_by_nemubot")
if not os.path.exists(lock_file):
with open(lock_file, 'w') as lf:
lf.write(str(os.getpid()))
return True
else:
with open(lock_file, 'r') as lf:
pid = lf.readline()
raise Exception("Data dir already locked, by PID %s" % pid)
return False
def close(self):
"""Release a locked path"""
lock_file = os.path.join(self.basedir, ".used_by_nemubot")
if os.path.isdir(self.basedir) and os.path.exists(lock_file):
os.unlink(lock_file)
return True
return False
def _get_data_file_path(self, module):
"""Get the path to the module data file"""
return os.path.join(self.basedir, module + ".xml")
def load(self, module):
"""Load data for the given module
Argument:
module -- the module name of data to load
"""
data_file = self._get_data_file_path(module)
if os.path.isfile(data_file):
return parse_file(data_file)
else:
return Abstract.load(self, module)
def save(self, module, data):
"""Load data for the given module
Argument:
module -- the module name of data to load
data -- the new data to save
"""
return data.save(self._get_data_file_path(module))

View file

@ -86,18 +86,13 @@ class ModuleLoader(SourceFileLoader):
module.__LOADED__ = True module.__LOADED__ = True
module.logger = logging.getLogger("nemubot.module." + fullname) module.logger = logging.getLogger("nemubot.module." + fullname)
def prnt(*args):
print("[%s]" % module.__name__, *args)
module.logger.info(" ".join(args))
def prnt_dbg(*args): def prnt_dbg(*args):
if module.DEBUG: if module.DEBUG:
print("{%s}" % module.__name__, *args) print("{%s}" % module.__name__, *args)
module.logger.debug(" ".join(args)) module.logger.debug(" ".join(args))
def mod_save(): def mod_save():
fpath = os.path.join(self.context.data_path, module.__name__ + ".xml") self.context.datastore.save(module.__name__, module.DATAS)
module.print_debug("Saving DATAS to " + fpath)
module.DATAS.save(fpath)
def send_response(server, res): def send_response(server, res):
if server in self.context.servers: if server in self.context.servers:
@ -129,7 +124,6 @@ class ModuleLoader(SourceFileLoader):
module.REGISTERED_HOOKS = list() module.REGISTERED_HOOKS = list()
module.REGISTERED_EVENTS = list() module.REGISTERED_EVENTS = list()
module.DEBUG = self.context.verbosity > 0 module.DEBUG = self.context.verbosity > 0
module.print = prnt
module.print_debug = prnt_dbg module.print_debug = prnt_dbg
module.send_response = send_response module.send_response = send_response
module.add_hook = add_hook module.add_hook = add_hook
@ -139,15 +133,8 @@ class ModuleLoader(SourceFileLoader):
module.del_event = del_event module.del_event = del_event
if not hasattr(module, "NODATA"): if not hasattr(module, "NODATA"):
module.DATAS = module_state.ModuleState("nemubotstate") module.DATAS = self.context.datastore.load(module.__name__)
if self.context.data_path is not None: module.save = mod_save
data_file = os.path.join(self.context.data_path,
module.__name__ + ".xml")
if os.path.isfile(data_file):
module.DATAS = parse_file(data_file)
module.save = mod_save
else:
module.save = lambda: False
else: else:
module.DATAS = None module.DATAS = None
module.save = lambda: False module.save = lambda: False