@@ -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): | |||