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