| @@ -48,4 +48,5 @@ if not __release__: | |||
| finally: | |||
| del _add_git_commit_id_to_version | |||
| from earwigbot import blowfish, bot, commands, config, irc, tasks, util, wiki | |||
| from earwigbot import (blowfish, bot, commands, config, irc, managers, tasks, | |||
| util, wiki) | |||
| @@ -24,11 +24,10 @@ import logging | |||
| from threading import Lock, Thread | |||
| from time import sleep, time | |||
| from earwigbot.commands import CommandManager | |||
| from earwigbot.config import BotConfig | |||
| from earwigbot.irc import Frontend, Watcher | |||
| from earwigbot.tasks import TaskManager | |||
| from earwigbot.wiki import SitesDBManager | |||
| from earwigbot.managers import CommandManager, TaskManager | |||
| from earwigbot.wiki import SitesDB | |||
| __all__ = ["Bot"] | |||
| @@ -58,7 +57,7 @@ class Bot(object): | |||
| self.logger = logging.getLogger("earwigbot") | |||
| self.commands = CommandManager(self) | |||
| self.tasks = TaskManager(self) | |||
| self.wiki = SitesDBManager(self.config) | |||
| self.wiki = SitesDB(self.config) | |||
| self.frontend = None | |||
| self.watcher = None | |||
| @@ -73,12 +72,12 @@ class Bot(object): | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| self.frontend = Frontend(self) | |||
| Thread(name=name, target=self.frontend.loop).start() | |||
| Thread(name="irc_frontend", target=self.frontend.loop).start() | |||
| if self.config.components.get("irc_watcher"): | |||
| self.logger.info("Starting IRC watcher") | |||
| self.watcher = Watcher(self) | |||
| Thread(name=name, target=self.watcher.loop).start() | |||
| Thread(name="irc_watcher", target=self.watcher.loop).start() | |||
| def _start_wiki_scheduler(self): | |||
| def wiki_scheduler(): | |||
| @@ -92,7 +91,7 @@ class Bot(object): | |||
| if self.config.components.get("wiki_scheduler"): | |||
| self.logger.info("Starting wiki scheduler") | |||
| Thread(name=name, target=wiki_scheduler).start() | |||
| Thread(name="wiki_scheduler", target=wiki_scheduler).start() | |||
| def _stop_irc_components(self): | |||
| if self.frontend: | |||
| @@ -21,20 +21,16 @@ | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's IRC Command Manager | |||
| EarwigBot's IRC Commands | |||
| This package provides the IRC "commands" used by the bot's front-end component. | |||
| This module contains the BaseCommand class (import with | |||
| `from earwigbot.commands import BaseCommand`) and an internal CommandManager | |||
| class. This can be accessed through `bot.commands`. | |||
| `from earwigbot.commands import BaseCommand`), whereas the package contains | |||
| various built-in commands. Additional commands can be installed as plugins in | |||
| the bot's working directory. | |||
| """ | |||
| import imp | |||
| from os import listdir, path | |||
| from re import sub | |||
| from threading import Lock | |||
| __all__ = ["BaseCommand", "CommandManager"] | |||
| __all__ = ["BaseCommand"] | |||
| class BaseCommand(object): | |||
| """A base class for commands on IRC. | |||
| @@ -90,95 +86,3 @@ class BaseCommand(object): | |||
| Note that | |||
| """ | |||
| pass | |||
| class CommandManager(object): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = bot.logger.getChild("commands") | |||
| self._commands = {} | |||
| self._command_access_lock = Lock() | |||
| def __iter__(self): | |||
| for name in self._commands: | |||
| yield name | |||
| def _load_command(self, name, path): | |||
| """Load a specific command from a module, identified by name and path. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Command' class inside (assuming it is an instance | |||
| of BaseCommand), add it to self._commands, and log the addition. Any | |||
| problems along the way will either be ignored or logged. | |||
| """ | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| module = imp.load_module(name, f, path, desc) | |||
| except Exception: | |||
| e = "Couldn't load module {0} from {1}" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| finally: | |||
| f.close() | |||
| try: | |||
| command_class = module.Command | |||
| except AttributeError: | |||
| return # No command in this module | |||
| try: | |||
| command = command_class(self.bot) | |||
| except Exception: | |||
| e = "Error initializing Command() class in {0} (from {1})" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| if not isinstance(command, BaseCommand): | |||
| return | |||
| self._commands[command.name] = command | |||
| self.logger.debug("Loaded command {0}".format(command.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid commands in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_command(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid commands into self._commands.""" | |||
| with self._command_access_lock: | |||
| self._commands.clear() | |||
| builtin_dir = path.dirname(__file__) | |||
| plugins_dir = path.join(self.bot.config.root_dir, "commands") | |||
| self._load_directory(builtin_dir) # Built-in commands | |||
| self._load_directory(plugins_dir) # Custom commands, aka plugins | |||
| msg = "Loaded {0} commands: {1}" | |||
| commands = ", ".join(self._commands.keys()) | |||
| self.logger.info(msg.format(len(self._commands), commands)) | |||
| def check(self, hook, data): | |||
| """Given an IRC event, check if there's anything we can respond to.""" | |||
| with self._command_access_lock: | |||
| for command in self._commands.values(): | |||
| if hook in command.hooks: | |||
| if command.check(data): | |||
| try: | |||
| command._wrap_process(data) | |||
| except Exception: | |||
| e = "Error executing command '{0}':" | |||
| self.logger.exception(e.format(data.command)) | |||
| break | |||
| def get(self, command_name): | |||
| """Return the class instance associated with a certain command name. | |||
| Will raise KeyError if the command is not found. | |||
| """ | |||
| return self._command[command_name] | |||
| @@ -0,0 +1,250 @@ | |||
| #! /usr/bin/env python | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import imp | |||
| from os import listdir, path | |||
| from re import sub | |||
| from threading import Lock, Thread | |||
| from time import gmtime, strftime | |||
| from earwigbot.commands import BaseCommand | |||
| from earwigbot.tasks import BaseTask | |||
| __all__ = ["CommandManager", "TaskManager"] | |||
| class _BaseManager(object): | |||
| pass | |||
| class CommandManager(_BaseManager): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = bot.logger.getChild("commands") | |||
| self._commands = {} | |||
| self._command_access_lock = Lock() | |||
| def __iter__(self): | |||
| for name in self._commands: | |||
| yield name | |||
| def _load_command(self, name, path): | |||
| """Load a specific command from a module, identified by name and path. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Command' class inside (assuming it is an instance | |||
| of BaseCommand), add it to self._commands, and log the addition. Any | |||
| problems along the way will either be ignored or logged. | |||
| """ | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| module = imp.load_module(name, f, path, desc) | |||
| except Exception: | |||
| e = "Couldn't load module {0} from {1}" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| finally: | |||
| f.close() | |||
| try: | |||
| command_class = module.Command | |||
| except AttributeError: | |||
| return # No command in this module | |||
| try: | |||
| command = command_class(self.bot) | |||
| except Exception: | |||
| e = "Error initializing Command() class in {0} (from {1})" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| if not isinstance(command, BaseCommand): | |||
| return | |||
| self._commands[command.name] = command | |||
| self.logger.debug("Loaded command {0}".format(command.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid commands in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_command(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid commands into self._commands.""" | |||
| with self._command_access_lock: | |||
| self._commands.clear() | |||
| builtin_dir = path.join(path.dirname(__file__), "commands") | |||
| plugins_dir = path.join(self.bot.config.root_dir, "commands") | |||
| self._load_directory(builtin_dir) # Built-in commands | |||
| self._load_directory(plugins_dir) # Custom commands, aka plugins | |||
| msg = "Loaded {0} commands: {1}" | |||
| commands = ", ".join(self._commands.keys()) | |||
| self.logger.info(msg.format(len(self._commands), commands)) | |||
| def check(self, hook, data): | |||
| """Given an IRC event, check if there's anything we can respond to.""" | |||
| with self._command_access_lock: | |||
| for command in self._commands.values(): | |||
| if hook in command.hooks: | |||
| if command.check(data): | |||
| try: | |||
| command._wrap_process(data) | |||
| except Exception: | |||
| e = "Error executing command '{0}':" | |||
| self.logger.exception(e.format(data.command)) | |||
| break | |||
| def get(self, command_name): | |||
| """Return the class instance associated with a certain command name. | |||
| Will raise KeyError if the command is not found. | |||
| """ | |||
| return self._command[command_name] | |||
| class TaskManager(_BaseManager): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = bot.logger.getChild("tasks") | |||
| self._tasks = {} | |||
| self._task_access_lock = Lock() | |||
| def __iter__(self): | |||
| for name in self._tasks: | |||
| yield name | |||
| def _wrapper(self, task, **kwargs): | |||
| """Wrapper for task classes: run the task and catch any errors.""" | |||
| try: | |||
| task.run(**kwargs) | |||
| except Exception: | |||
| msg = "Task '{0}' raised an exception and had to stop:" | |||
| self.logger.exception(msg.format(task.name)) | |||
| else: | |||
| msg = "Task '{0}' finished without error" | |||
| self.logger.info(msg.format(task.name)) | |||
| def _load_task(self, name, path): | |||
| """Load a specific task from a module, identified by name and path. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Task' class inside (assuming it is an instance of | |||
| BaseTask), add it to self._tasks, and log the addition. Any problems | |||
| along the way will either be ignored or logged. | |||
| """ | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| module = imp.load_module(name, f, path, desc) | |||
| except Exception: | |||
| e = "Couldn't load module {0} from {1}" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| finally: | |||
| f.close() | |||
| try: | |||
| task_class = module.Task | |||
| except AttributeError: | |||
| return # No task in this module | |||
| try: | |||
| task = task_class(self.bot) | |||
| except Exception: | |||
| e = "Error initializing Task() class in {0} (from {1})" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| if not isinstance(task, BaseTask): | |||
| return | |||
| self._tasks[task.name] = task | |||
| self.logger.debug("Loaded task {0}".format(task.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid tasks in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_task(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid tasks into self._tasks.""" | |||
| with self._task_access_lock: | |||
| self._tasks.clear() | |||
| builtin_dir = path.join(path.dirname(__file__), "tasks") | |||
| plugins_dir = path.join(self.bot.config.root_dir, "tasks") | |||
| self._load_directory(builtin_dir) # Built-in tasks | |||
| self._load_directory(plugins_dir) # Custom tasks, aka plugins | |||
| msg = "Loaded {0} tasks: {1}" | |||
| tasks = ', '.join(self._tasks.keys()) | |||
| self.logger.info(msg.format(len(self._tasks), tasks)) | |||
| def start(self, task_name, **kwargs): | |||
| """Start a given task in a new thread. kwargs are passed to task.run""" | |||
| msg = "Starting task '{0}' in a new thread" | |||
| self.logger.info(msg.format(task_name)) | |||
| with self._task_access_lock: | |||
| try: | |||
| task = self._tasks[task_name] | |||
| except KeyError: | |||
| e = "Couldn't find task '{0}':" | |||
| self.logger.error(e.format(task_name)) | |||
| return | |||
| task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) | |||
| start_time = strftime("%b %d %H:%M:%S") | |||
| task_thread.name = "{0} ({1})".format(task_name, start_time) | |||
| task_thread.start() | |||
| def schedule(self, now=None): | |||
| """Start all tasks that are supposed to be run at a given time.""" | |||
| if not now: | |||
| now = gmtime() | |||
| # Get list of tasks to run this turn: | |||
| tasks = self.bot.config.schedule(now.tm_min, now.tm_hour, now.tm_mday, | |||
| now.tm_mon, now.tm_wday) | |||
| for task in tasks: | |||
| if isinstance(task, list): # They've specified kwargs, | |||
| self.start(task[0], **task[1]) # so pass those to start | |||
| else: # Otherwise, just pass task_name | |||
| self.start(task) | |||
| def get(self, task_name): | |||
| """Return the class instance associated with a certain task name. | |||
| Will raise KeyError if the task is not found. | |||
| """ | |||
| return self._tasks[task_name] | |||
| @@ -21,22 +21,20 @@ | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's Wiki Task Manager | |||
| EarwigBot's Bot Tasks | |||
| This package provides the wiki bot "tasks" EarwigBot runs. This module contains | |||
| the BaseTask class (import with `from earwigbot.tasks import BaseTask`) and an | |||
| internal TaskManager class. This can be accessed through `bot.tasks`. | |||
| """ | |||
| the BaseTask class (import with `from earwigbot.tasks import BaseTask`), | |||
| whereas the package contains various built-in tasks. Additional tasks can be | |||
| installed as plugins in the bot's working directory. | |||
| import imp | |||
| from os import listdir, path | |||
| from re import sub | |||
| from threading import Lock, Thread | |||
| from time import gmtime, strftime | |||
| To run a task, use bot.tasks.start(name, **kwargs). **kwargs get passed to the | |||
| Task's run() function. | |||
| """ | |||
| from earwigbot import wiki | |||
| __all__ = ["BaseTask", "TaskManager"] | |||
| __all__ = ["BaseTask"] | |||
| class BaseTask(object): | |||
| """A base class for bot tasks that edit Wikipedia.""" | |||
| @@ -131,125 +129,3 @@ class BaseTask(object): | |||
| self.logger.warn("Emergency task shutoff has been enabled!") | |||
| return True | |||
| class TaskManager(object): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = bot.logger.getChild("tasks") | |||
| self._tasks = {} | |||
| self._task_access_lock = Lock() | |||
| def __iter__(self): | |||
| for name in self._tasks: | |||
| yield name | |||
| def _wrapper(self, task, **kwargs): | |||
| """Wrapper for task classes: run the task and catch any errors.""" | |||
| try: | |||
| task.run(**kwargs) | |||
| except Exception: | |||
| msg = "Task '{0}' raised an exception and had to stop:" | |||
| self.logger.exception(msg.format(task.name)) | |||
| else: | |||
| msg = "Task '{0}' finished without error" | |||
| self.logger.info(msg.format(task.name)) | |||
| def _load_task(self, name, path): | |||
| """Load a specific task from a module, identified by name and path. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Task' class inside (assuming it is an instance of | |||
| BaseTask), add it to self._tasks, and log the addition. Any problems | |||
| along the way will either be ignored or logged. | |||
| """ | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| module = imp.load_module(name, f, path, desc) | |||
| except Exception: | |||
| e = "Couldn't load module {0} from {1}" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| finally: | |||
| f.close() | |||
| try: | |||
| task_class = module.Task | |||
| except AttributeError: | |||
| return # No task in this module | |||
| try: | |||
| task = task_class(self.bot) | |||
| except Exception: | |||
| e = "Error initializing Task() class in {0} (from {1})" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| if not isinstance(task, BaseTask): | |||
| return | |||
| self._tasks[task.name] = task | |||
| self.logger.debug("Loaded task {0}".format(task.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid tasks in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_task(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid tasks into self._tasks.""" | |||
| with self._task_access_lock: | |||
| self._tasks.clear() | |||
| builtin_dir = path.dirname(__file__) | |||
| plugins_dir = path.join(self.bot.config.root_dir, "tasks") | |||
| self._load_directory(builtin_dir) # Built-in tasks | |||
| self._load_directory(plugins_dir) # Custom tasks, aka plugins | |||
| msg = "Loaded {0} tasks: {1}" | |||
| tasks = ', '.join(self._tasks.keys()) | |||
| self.logger.info(msg.format(len(self._tasks), tasks)) | |||
| def start(self, task_name, **kwargs): | |||
| """Start a given task in a new thread. kwargs are passed to task.run""" | |||
| msg = "Starting task '{0}' in a new thread" | |||
| self.logger.info(msg.format(task_name)) | |||
| with self._task_access_lock: | |||
| try: | |||
| task = self._tasks[task_name] | |||
| except KeyError: | |||
| e = "Couldn't find task '{0}':" | |||
| self.logger.error(e.format(task_name)) | |||
| return | |||
| task_thread = Thread(target=self._wrapper, args=(task,), kwargs=kwargs) | |||
| start_time = strftime("%b %d %H:%M:%S") | |||
| task_thread.name = "{0} ({1})".format(task_name, start_time) | |||
| task_thread.start() | |||
| def schedule(self, now=None): | |||
| """Start all tasks that are supposed to be run at a given time.""" | |||
| if not now: | |||
| now = gmtime() | |||
| # Get list of tasks to run this turn: | |||
| tasks = self.bot.config.schedule(now.tm_min, now.tm_hour, now.tm_mday, | |||
| now.tm_mon, now.tm_wday) | |||
| for task in tasks: | |||
| if isinstance(task, list): # They've specified kwargs, | |||
| self.start(task[0], **task[1]) # so pass those to start | |||
| else: # Otherwise, just pass task_name | |||
| self.start(task) | |||
| def get(self, task_name): | |||
| """Return the class instance associated with a certain task name. | |||
| Will raise KeyError if the task is not found. | |||
| """ | |||
| return self._tasks[task_name] | |||
| @@ -29,10 +29,10 @@ written by Mr.Z-man, other than a similar purpose. We share no code. | |||
| Import the toolset directly with `from earwigbot import wiki`. If using the | |||
| built-in integration with the rest of the bot, Bot() objects contain a `wiki` | |||
| attribute, which is a SitesDBManager object tied to the sites.db file located | |||
| in the same directory as config.yml. That object has the principal methods | |||
| get_site, add_site, and remove_site that should handle all of your Site (and | |||
| thus, Page, Category, and User) needs. | |||
| attribute, which is a SitesDB object tied to the sites.db file located in the | |||
| same directory as config.yml. That object has the principal methods get_site, | |||
| add_site, and remove_site that should handle all of your Site (and thus, Page, | |||
| Category, and User) needs. | |||
| """ | |||
| import logging as _log | |||
| @@ -32,9 +32,9 @@ from earwigbot import __version__ | |||
| from earwigbot.wiki.exceptions import SiteNotFoundError | |||
| from earwigbot.wiki.site import Site | |||
| __all__ = ["SitesDBManager"] | |||
| __all__ = ["SitesDB"] | |||
| class SitesDBManager(object): | |||
| class SitesDB(object): | |||
| """ | |||
| EarwigBot's Wiki Toolset: Sites Database Manager | |||
| @@ -49,8 +49,7 @@ class SitesDBManager(object): | |||
| here are available as bot.wiki.get_site(), bot.wiki.add_site(), and | |||
| bot.wiki.remove_site(), which use a sites.db file located in the same | |||
| directory as our config.yml file. Lower-level access can be achieved | |||
| by importing the manager class | |||
| (`from earwigbot.wiki import SitesDBManager`). | |||
| by importing the manager class (`from earwigbot.wiki import SitesDB`). | |||
| """ | |||
| def __init__(self, config): | |||