Browse Source

Move Command+TaskManagers to a common earwigbot.managers module

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
0c6f627e43
7 changed files with 278 additions and 249 deletions
  1. +2
    -1
      earwigbot/__init__.py
  2. +6
    -7
      earwigbot/bot.py
  3. +5
    -101
      earwigbot/commands/__init__.py
  4. +250
    -0
      earwigbot/managers.py
  5. +8
    -132
      earwigbot/tasks/__init__.py
  6. +4
    -4
      earwigbot/wiki/__init__.py
  7. +3
    -4
      earwigbot/wiki/sitesdb.py

+ 2
- 1
earwigbot/__init__.py View File

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

+ 6
- 7
earwigbot/bot.py View File

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


+ 5
- 101
earwigbot/commands/__init__.py View File

@@ -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]

+ 250
- 0
earwigbot/managers.py View File

@@ -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]

+ 8
- 132
earwigbot/tasks/__init__.py View File

@@ -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]

+ 4
- 4
earwigbot/wiki/__init__.py View File

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


+ 3
- 4
earwigbot/wiki/sitesdb.py View File

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


Loading…
Cancel
Save