@@ -7,7 +7,6 @@ earwigbot Package | |||
.. automodule:: earwigbot.__init__ | |||
:members: | |||
:undoc-members: | |||
:show-inheritance: | |||
:mod:`bot` Module | |||
----------------- | |||
@@ -15,7 +14,6 @@ earwigbot Package | |||
.. automodule:: earwigbot.bot | |||
:members: | |||
:undoc-members: | |||
:show-inheritance: | |||
:mod:`config` Module | |||
-------------------- | |||
@@ -23,7 +21,6 @@ earwigbot Package | |||
.. automodule:: earwigbot.config | |||
:members: | |||
:undoc-members: | |||
:show-inheritance: | |||
:mod:`exceptions` Module | |||
------------------------ | |||
@@ -37,7 +34,7 @@ earwigbot Package | |||
---------------------- | |||
.. automodule:: earwigbot.managers | |||
:members: | |||
:members: _ResourceManager, CommandManager, TaskManager | |||
:undoc-members: | |||
:show-inheritance: | |||
@@ -47,7 +44,6 @@ earwigbot Package | |||
.. automodule:: earwigbot.util | |||
:members: | |||
:undoc-members: | |||
:show-inheritance: | |||
Subpackages | |||
----------- | |||
@@ -57,7 +57,7 @@ The most useful attributes are: | |||
logged and used as the quit message when disconnecting from IRC. | |||
:py:class:`earwigbot.config.BotConfig` stores configuration information for the | |||
bot. Its docstring explains what each attribute is used for, but essentially | |||
bot. Its docstrings explains what each attribute is used for, but essentially | |||
each "node" (one of :py:attr:`config.components`, :py:attr:`wiki`, | |||
:py:attr:`tasks`, :py:attr:`tasks`, or :py:attr:`metadata`) maps to a section | |||
of the bot's :file:`config.yml` file. For example, if :file:`config.yml` | |||
@@ -21,11 +21,12 @@ | |||
# SOFTWARE. | |||
""" | |||
EarwigBot is a Python robot that edits Wikipedia and interacts with people over | |||
IRC. - https://github.com/earwig/earwigbot | |||
`EarwigBot <https://github.com/earwig/earwigbot>`_ is a Python robot that edits | |||
Wikipedia and interacts with people over IRC. | |||
See README.rst for an overview, or the docs/ directory for details. This | |||
documentation is also available online at http://packages.python.org/earwigbot. | |||
See :file:`README.rst` for an overview, or the :file:`docs/` directory for | |||
details. This documentation is also available `online | |||
<http://packages.python.org/earwigbot>`_. | |||
""" | |||
__author__ = "Ben Kurtovic" | |||
@@ -40,18 +40,21 @@ class Bot(object): | |||
EarwigBot has three components that can run independently of each other: an | |||
IRC front-end, an IRC watcher, and a wiki scheduler. | |||
* The IRC front-end runs on a normal IRC server and expects users to | |||
- The IRC front-end runs on a normal IRC server and expects users to | |||
interact with it/give it commands. | |||
* The IRC watcher runs on a wiki recent-changes server and listens for | |||
- The IRC watcher runs on a wiki recent-changes server and listens for | |||
edits. Users cannot interact with this part of the bot. | |||
* The wiki scheduler runs wiki-editing bot tasks in separate threads at | |||
- The wiki scheduler runs wiki-editing bot tasks in separate threads at | |||
user-defined times through a cron-like interface. | |||
The Bot() object is accessable from within commands and tasks as self.bot. | |||
This is the primary way to access data from other components of the bot. | |||
For example, our BotConfig object is accessable from bot.config, tasks | |||
can be started with bot.tasks.start(), and sites can be loaded from the | |||
wiki toolset with bot.wiki.get_site(). | |||
The :py:class:`Bot` object is accessable from within commands and tasks as | |||
:py:attr:`self.bot`. This is the primary way to access data from other | |||
components of the bot. For example, our | |||
:py:class:`~earwigbot.config.BotConfig` object is accessable from | |||
:py:attr:`bot.config`, tasks can be started with | |||
:py:meth:`bot.tasks.start <earwigbot.managers.TaskManager.start>`, and | |||
sites can be loaded from the wiki toolset with :py:meth:`bot.wiki.get_site | |||
<earwigbot.wiki.sitesdb.SitesDB.get_site>`. | |||
""" | |||
def __init__(self, root_dir, level=logging.INFO): | |||
@@ -160,11 +163,11 @@ class Bot(object): | |||
This is thread-safe, and it will gracefully stop IRC components before | |||
reloading anything. Note that you can safely reload commands or tasks | |||
without restarting the bot with bot.commands.load() or | |||
bot.tasks.load(). These should not interfere with running components | |||
or tasks. | |||
without restarting the bot with :py:meth:`bot.commands.load` or | |||
:py:meth:`bot.tasks.load`. These should not interfere with running | |||
components or tasks. | |||
If given, 'msg' will be used as our quit message. | |||
If given, *msg* will be used as our quit message. | |||
""" | |||
if msg: | |||
self.logger.info('Restarting bot ("{0}")'.format(msg)) | |||
@@ -180,7 +183,7 @@ class Bot(object): | |||
def stop(self, msg=None): | |||
"""Gracefully stop all bot components. | |||
If given, 'msg' will be used as our quit message. | |||
If given, *msg* will be used as our quit message. | |||
""" | |||
if msg: | |||
self.logger.info('Stopping bot ("{0}")'.format(msg)) | |||
@@ -29,36 +29,34 @@ from os import mkdir, path | |||
from Crypto.Cipher import Blowfish | |||
import yaml | |||
from earwigbot.exceptions import NoConfigError | |||
__all__ = ["BotConfig"] | |||
class BotConfig(object): | |||
""" | |||
EarwigBot's YAML Config File Manager | |||
**EarwigBot's YAML Config File Manager** | |||
This handles all tasks involving reading and writing to our config file, | |||
including encrypting and decrypting passwords and making a new config file | |||
from scratch at the inital bot run. | |||
BotConfig has a few properties and functions, including the following: | |||
* config.root_dir - bot's working directory; contains config.yml, logs/ | |||
* config.path - path to the bot's config file | |||
* config.components - enabled components | |||
* config.wiki - information about wiki-editing | |||
* config.tasks - information for bot tasks | |||
* config.irc - information about IRC | |||
* config.metadata - miscellaneous information | |||
* config.schedule() - tasks scheduled to run at a given time | |||
BotConfig also has some functions used in config loading: | |||
* config.load() - loads and parses our config file, returning True if | |||
passwords are stored encrypted or False otherwise; | |||
can also be used to easily reload config | |||
* config.decrypt() - given a key, decrypts passwords inside our config | |||
variables, and remembers to decrypt the password if | |||
config is reloaded; won't do anything if passwords | |||
aren't encrypted | |||
BotConfig has a few attributes and methods, including the following: | |||
- :py:attr:`root_dir`: bot's working directory; contains | |||
:file:`config.yml`, :file:`logs/` | |||
- :py:attr:`path`: path to the bot's config file | |||
- :py:attr:`components`: enabled components | |||
- :py:attr:`wiki`: information about wiki-editing | |||
- :py:attr:`tasks`: information for bot tasks | |||
- :py:attr:`irc`: information about IRC | |||
- :py:attr:`metadata`: miscellaneous information | |||
- :py:meth:`schedule`: tasks scheduled to run at a given time | |||
BotConfig also has some methods used in config loading: | |||
- :py:meth:`load`: loads (or reloads) and parses our config file | |||
- :py:meth:`decrypt`: decrypts an object in the config tree | |||
""" | |||
def __init__(self, root_dir, level): | |||
@@ -159,10 +157,12 @@ class BotConfig(object): | |||
@property | |||
def root_dir(self): | |||
"""The bot's root directory containing its config file and more.""" | |||
return self._root_dir | |||
@property | |||
def logging_level(self): | |||
"""The minimum logging level for messages logged via stdout.""" | |||
return self._logging_level | |||
@logging_level.setter | |||
@@ -172,15 +172,17 @@ class BotConfig(object): | |||
@property | |||
def path(self): | |||
"""The path to the bot's config file.""" | |||
return self._config_path | |||
@property | |||
def log_dir(self): | |||
"""The directory containing the bot's logs.""" | |||
return self._log_dir | |||
@property | |||
def data(self): | |||
"""The entire config file.""" | |||
"""The entire config file as a decoded JSON object.""" | |||
return self._data | |||
@property | |||
@@ -209,11 +211,11 @@ class BotConfig(object): | |||
return self._metadata | |||
def is_loaded(self): | |||
"""Return True if our config file has been loaded, otherwise False.""" | |||
"""Return ``True`` if our config file has been loaded, or ``False``.""" | |||
return self._data is not None | |||
def is_encrypted(self): | |||
"""Return True if passwords are encrypted, otherwise False.""" | |||
"""Return ``True`` if passwords are encrypted, otherwise ``False``.""" | |||
return self.metadata.get("encryptPasswords", False) | |||
def load(self): | |||
@@ -223,12 +225,14 @@ class BotConfig(object): | |||
user. If there is no config file at all, offer to make one, otherwise | |||
exit. | |||
Store data from our config file in five _ConfigNodes (components, | |||
wiki, tasks, irc, metadata) for easy access (as well as the internal | |||
_data variable). | |||
If config is being reloaded, encrypted items will be automatically | |||
decrypted if they were decrypted beforehand. | |||
Data from the config file is stored in five | |||
:py:class:`~earwigbot.config._ConfigNode` s (:py:attr:`components`, | |||
:py:attr:`wiki`, :py:attr:`tasks`, :py:attr:`irc`, :py:attr:`metadata`) | |||
for easy access (as well as the lower-level :py:attr:`data` attribute). | |||
If passwords are encrypted, we'll use :py:func:`~getpass.getpass` for | |||
the key and then decrypt them. If the config is being reloaded, | |||
encrypted items will be automatically decrypted if they were decrypted | |||
earlier. | |||
""" | |||
if not path.exists(self._config_path): | |||
print "Config file not found:", self._config_path | |||
@@ -236,7 +240,7 @@ class BotConfig(object): | |||
if choice.lower().startswith("y"): | |||
self._make_new() | |||
else: | |||
exit(1) # TODO: raise an exception instead | |||
raise NoConfigError() | |||
self._load() | |||
data = self._data | |||
@@ -255,16 +259,19 @@ class BotConfig(object): | |||
self._decrypt(node, nodes) | |||
def decrypt(self, node, *nodes): | |||
"""Use self._decryption_cipher to decrypt an object in our config tree. | |||
"""Decrypt an object in our config tree. | |||
:py:attr:`_decryption_cipher` is used as our key, retrieved using | |||
:py:func:`~getpass.getpass` in :py:meth:`load` if it wasn't already | |||
specified. If this is called when passwords are not encrypted (check | |||
with :py:meth:`is_encrypted`), nothing will happen. We'll also keep | |||
track of this node if :py:meth:`load` is called again (i.e. to reload) | |||
and automatically decrypt it. | |||
If this is called when passwords are not encrypted (check with | |||
config.is_encrypted()), nothing will happen. We'll also keep track of | |||
this node if config.load() is called again (i.e. to reload) and | |||
automatically decrypt it. | |||
Example usage:: | |||
Example usage: | |||
config.decrypt(config.irc, "frontend", "nickservPassword") | |||
-> decrypts config.irc["frontend"]["nickservPassword"] | |||
>>> config.decrypt(config.irc, "frontend", "nickservPassword") | |||
# decrypts config.irc["frontend"]["nickservPassword"] | |||
""" | |||
self._decryptable_nodes.append((node, nodes)) | |||
if self.is_encrypted(): | |||
@@ -273,9 +280,8 @@ class BotConfig(object): | |||
def schedule(self, minute, hour, month_day, month, week_day): | |||
"""Return a list of tasks scheduled to run at the specified time. | |||
The schedule data comes from our config file's 'schedule' field, which | |||
is stored as self._data["schedule"]. Call this function as | |||
config.schedule(args). | |||
The schedule data comes from our config file's ``schedule`` field, | |||
which is stored as :py:attr:`self.data["schedule"] <data>`. | |||
""" | |||
# Tasks to run this turn, each as a list of either [task_name, kwargs], | |||
# or just the task_name: | |||
@@ -26,6 +26,7 @@ EarwigBot Exceptions | |||
This module contains all exceptions used by EarwigBot:: | |||
EarwigBotError | |||
+-- NoConfigError | |||
+-- IRCError | |||
| +-- BrokenSocketError | |||
| +-- KwargParseError | |||
@@ -55,6 +56,13 @@ This module contains all exceptions used by EarwigBot:: | |||
class EarwigBotError(Exception): | |||
"""Base exception class for errors in EarwigBot.""" | |||
class NoConfigError(EarwigBotError): | |||
"""The bot cannot be run without a config file. | |||
This occurs if no config file exists, and the user said they did not want | |||
one to be created. | |||
""" | |||
class IRCError(EarwigBotError): | |||
"""Base exception class for errors in IRC-relation sections of the bot.""" | |||
@@ -34,21 +34,21 @@ __all__ = ["CommandManager", "TaskManager"] | |||
class _ResourceManager(object): | |||
""" | |||
EarwigBot's Base Resource Manager | |||
Resources are essentially objects dynamically loaded by the bot, both | |||
packaged with it (built-in resources) and created by users (plugins, aka | |||
custom resources). Currently, the only two types of resources are IRC | |||
commands and bot tasks. These are both loaded from two locations: the | |||
earwigbot.commands and earwigbot.tasks packages, and the commands/ and | |||
tasks/ directories within the bot's working directory. | |||
This class handles the low-level tasks of (re)loading resources via load(), | |||
retrieving specific resources via get(), and iterating over all resources | |||
via __iter__(). If iterating over resources, it is recommended to acquire | |||
self.lock beforehand and release it afterwards (alternatively, wrap your | |||
code in a `with` statement) so an attempt at reloading resources in another | |||
thread won't disrupt your iteration. | |||
:py:mod:`earwigbot.commands` and :py:mod:`earwigbot.tasks packages`, and | |||
the :file:`commands/` and :file:`tasks/` directories within the bot's | |||
working directory. | |||
This class handles the low-level tasks of (re)loading resources via | |||
:py:meth:`load`, retrieving specific resources via :py:meth:`get`, and | |||
iterating over all resources via :py:meth:`__iter__`. If iterating over | |||
resources, it is recommended to acquire :py:attr:`self.lock <lock>` | |||
beforehand and release it afterwards (alternatively, wrap your code in a | |||
``with`` statement) so an attempt at reloading resources in another thread | |||
won't disrupt your iteration. | |||
""" | |||
def __init__(self, bot, name, attribute, base): | |||
self.bot = bot | |||
@@ -62,6 +62,7 @@ class _ResourceManager(object): | |||
@property | |||
def lock(self): | |||
"""The resource access/modify lock.""" | |||
return self._resource_access_lock | |||
def __iter__(self): | |||
@@ -116,7 +117,7 @@ class _ResourceManager(object): | |||
processed.append(modname) | |||
def load(self): | |||
"""Load (or reload) all valid resources into self._resources.""" | |||
"""Load (or reload) all valid resources into :py:attr:`_resources`.""" | |||
name = self._resource_name # e.g. "commands" or "tasks" | |||
with self.lock: | |||
self._resources.clear() | |||
@@ -132,15 +133,14 @@ class _ResourceManager(object): | |||
def get(self, key): | |||
"""Return the class instance associated with a certain resource. | |||
Will raise KeyError if the resource (command or task) is not found. | |||
Will raise :py:exc:`KeyError` if the resource (a command or task) is | |||
not found. | |||
""" | |||
return self._resources[key] | |||
class CommandManager(_ResourceManager): | |||
""" | |||
EarwigBot's IRC Command Manager | |||
Manages (i.e., loads, reloads, and calls) IRC commands. | |||
""" | |||
def __init__(self, bot): | |||
@@ -164,7 +164,7 @@ class CommandManager(_ResourceManager): | |||
self.logger.exception(e.format(command.name)) | |||
def call(self, hook, data): | |||
"""Given a hook type and a Data object, respond appropriately.""" | |||
"""Respond to a hook type and a :py:class:`Data` object.""" | |||
self.lock.acquire() | |||
for command in self._resources.itervalues(): | |||
if hook in command.hooks and self._wrap_check(command, data): | |||
@@ -176,8 +176,6 @@ class CommandManager(_ResourceManager): | |||
class TaskManager(_ResourceManager): | |||
""" | |||
EarwigBot's Bot Task Manager | |||
Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks. | |||
""" | |||
def __init__(self, bot): | |||
@@ -197,8 +195,9 @@ class TaskManager(_ResourceManager): | |||
def start(self, task_name, **kwargs): | |||
"""Start a given task in a new daemon thread, and return the thread. | |||
kwargs are passed to task.run(). If the task is not found, None will be | |||
returned. | |||
kwargs are passed to :py:meth:`task.run() <earwigbot.tasks.BaseTask>`. | |||
If the task is not found, ``None`` will be returned an an error is | |||
logged. | |||
""" | |||
msg = "Starting task '{0}' in a new thread" | |||
self.logger.info(msg.format(task_name)) | |||
@@ -22,8 +22,27 @@ | |||
# SOFTWARE. | |||
""" | |||
This is EarwigBot's command-line utility, enabling you to easily start the | |||
bot or run specific tasks. | |||
usage: :command:`earwigbot [-h] [-v] [-d] [-q] [-t NAME] [PATH]` | |||
This is EarwigBot's command-line utility, enabling you to easily start the bot | |||
or run specific tasks. | |||
.. glossary:: | |||
``PATH`` | |||
path to the bot's working directory, which will be created if it doesn't | |||
exist; current directory assumed if not specified | |||
``-h``, ``--help`` | |||
show this help message and exit | |||
``-v``, ``--version`` | |||
show program's version number and exit | |||
``-d``, ``--debug`` | |||
print all logs, including ``DEBUG``-level messages | |||
``-q``, ``--quiet`` | |||
don't print any logs except warnings and errors | |||
``-t NAME``, ``--task NAME`` | |||
given the name of a task, the bot will run it instead of the main bot and | |||
then exit | |||
""" | |||
from argparse import ArgumentParser | |||
@@ -37,17 +56,23 @@ from earwigbot.bot import Bot | |||
__all__ = ["main"] | |||
def main(): | |||
"""Main entry point for the command-line utility.""" | |||
version = "EarwigBot v{0}".format(__version__) | |||
parser = ArgumentParser(description=__doc__) | |||
desc = """This is EarwigBot's command-line utility, enabling you to easily | |||
start the bot or run specific tasks.""" | |||
parser = ArgumentParser(description=desc) | |||
parser.add_argument("path", nargs="?", metavar="PATH", default=path.curdir, | |||
help="path to the bot's working directory, which will be created if it doesn't exist; current directory assumed if not specified") | |||
help="""path to the bot's working directory, which will | |||
be created if it doesn't exist; current | |||
directory assumed if not specified""") | |||
parser.add_argument("-v", "--version", action="version", version=version) | |||
parser.add_argument("-d", "--debug", action="store_true", | |||
help="print all logs, including DEBUG-level messages") | |||
parser.add_argument("-q", "--quiet", action="store_true", | |||
help="don't print any logs except warnings and errors") | |||
parser.add_argument("-t", "--task", metavar="NAME", | |||
help="given the name of a task, the bot will run it instead of the main bot and then exit") | |||
help="""given the name of a task, the bot will run it | |||
instead of the main bot and then exit""") | |||
args = parser.parse_args() | |||
level = logging.INFO | |||
@@ -693,9 +693,8 @@ class Page(CopyrightMixin): | |||
"""Add a new section to the bottom of the page. | |||
The arguments for this are the same as those for :py:meth:`edit`, but | |||
instead of providing a summary, you provide a section title. | |||
Likewise, raised exceptions are the same as :py:meth:`edit`'s. | |||
instead of providing a summary, you provide a section title. Likewise, | |||
raised exceptions are the same as :py:meth:`edit`'s. | |||
This should create the page if it does not already exist, with just the | |||
new section as content. | |||
@@ -646,11 +646,10 @@ class Site(object): | |||
If *all* is ``False`` (default), we'll return the first name in the | |||
list, which is usually the localized version. Otherwise, we'll return | |||
the entire list, which includes the canonical name. | |||
For example, this returns ``u"Wikipedia"`` if *ns_id* = ``4`` and | |||
*all* = ``False`` on ``enwiki``; returns ``[u"Wikipedia", u"Project", | |||
u"WP"]`` if *ns_id* = ``4`` and *all* is ``True``. | |||
the entire list, which includes the canonical name. For example, this | |||
returns ``u"Wikipedia"`` if *ns_id* = ``4`` and *all* is ``False`` on | |||
``enwiki``; returns ``[u"Wikipedia", u"Project", u"WP"]`` if *ns_id* = | |||
``4`` and *all* is ``True``. | |||
Raises :py:exc:`~earwigbot.exceptions.NamespaceNotFoundError` if the ID | |||
is not found. | |||