diff --git a/earwigbot/__init__.py b/earwigbot/__init__.py index 50da3f6..d77092a 100644 --- a/earwigbot/__init__.py +++ b/earwigbot/__init__.py @@ -32,6 +32,5 @@ __version__ = "0.1.dev" __email__ = "ben.kurtovic@verizon.net" from earwigbot import ( - blowfish, config, classes, commands, config, irc, main, rules, runner, - tasks, tests, wiki + blowfish, commands, config, irc, main, rules, runner, tasks, tests, wiki ) diff --git a/earwigbot/classes/__init__.py b/earwigbot/classes/__init__.py deleted file mode 100644 index 96079e1..0000000 --- a/earwigbot/classes/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2012 by Ben Kurtovic -# -# 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. - -from earwigbot.classes.base_task import * diff --git a/earwigbot/classes/base_task.py b/earwigbot/classes/base_task.py deleted file mode 100644 index 85229f2..0000000 --- a/earwigbot/classes/base_task.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2012 by Ben Kurtovic -# -# 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 logging - -from earwigbot.config import config -from earwigbot import wiki - -__all__ = ["BaseTask"] - -class BaseTask(object): - """A base class for bot tasks that edit Wikipedia.""" - name = None - number = 0 - - def __init__(self): - """Constructor for new tasks. - - This is called once immediately after the task class is loaded by - the task manager (in tasks._load_task()). - """ - pass - - def _setup_logger(self): - """Set up a basic module-level logger.""" - logger_name = ".".join(("earwigbot", "tasks", self.name)) - self.logger = logging.getLogger(logger_name) - self.logger.setLevel(logging.DEBUG) - - def run(self, **kwargs): - """Main entry point to run a given task. - - This is called directly by tasks.start() and is the main way to make a - task do stuff. kwargs will be any keyword arguments passed to start() - which are entirely optional. - - The same task instance is preserved between runs, so you can - theoretically store data in self (e.g. - start('mytask', action='store', data='foo')) and then use it later - (e.g. start('mytask', action='save')). - """ - pass - - def make_summary(self, comment): - """Makes an edit summary by filling in variables in a config value. - - config.wiki["summary"] is used, where $2 is replaced by the main - summary body, given as a method arg, and $1 is replaced by the task - number. - - If the config value is not found, we just return the arg as-is. - """ - try: - summary = config.wiki["summary"] - except KeyError: - return comment - return summary.replace("$1", str(self.number)).replace("$2", comment) - - def shutoff_enabled(self, site=None): - """Returns whether on-wiki shutoff is enabled for this task. - - We check a certain page for certain content. This is determined by - our config file: config.wiki["shutoff"]["page"] is used as the title, - with $1 replaced by our username and $2 replaced by the task number, - and config.wiki["shutoff"]["disabled"] is used as the content. - - If the page has that content or the page does not exist, then shutoff - is "disabled", meaning the bot is supposed to run normally, and we - return False. If the page's content is something other than what we - expect, shutoff is enabled, and we return True. - - If a site is not provided, we'll try to use self.site if it's set. - Otherwise, we'll use our default site. - """ - if not site: - try: - site = self.site - except AttributeError: - site = wiki.get_site() - - try: - cfg = config.wiki["shutoff"] - except KeyError: - return False - title = cfg.get("page", "User:$1/Shutoff/Task $2") - username = site.get_user().name() - title = title.replace("$1", username).replace("$2", str(self.number)) - page = site.get_page(title) - - try: - content = page.get() - except wiki.PageNotFoundError: - return False - if content == cfg.get("disabled", "run"): - return False - - self.logger.warn("Emergency task shutoff has been enabled!") - return True diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index 8af0ce8..d5543bb 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -26,7 +26,7 @@ EarwigBot's IRC Command Manager 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 the singleton `command_manager`. +class. This can be accessed through the `command_manager` singleton. """ import logging diff --git a/earwigbot/commands/afc_report.py b/earwigbot/commands/afc_report.py index 3c3bc24..c6a5840 100644 --- a/earwigbot/commands/afc_report.py +++ b/earwigbot/commands/afc_report.py @@ -22,9 +22,9 @@ import re -from earwigbot import tasks from earwigbot import wiki from earwigbot.commands import BaseCommand +from earwigbot.tasks import task_manager class Command(BaseCommand): """Get information about an AFC submission by name.""" @@ -36,7 +36,7 @@ class Command(BaseCommand): self.data = data try: - self.statistics = tasks.get("afc_statistics") + self.statistics = task_manager.get("afc_statistics") except KeyError: e = "Cannot run command: requires afc_statistics task." self.logger.error(e) diff --git a/earwigbot/commands/threads.py b/earwigbot/commands/threads.py index 5b5ce0f..33f686d 100644 --- a/earwigbot/commands/threads.py +++ b/earwigbot/commands/threads.py @@ -23,10 +23,10 @@ import threading import re -from earwigbot import tasks from earwigbot.commands import BaseCommand from earwigbot.config import config from earwigbot.irc import KwargParseException +from earwigbot.tasks import task_manager class Command(BaseCommand): """Manage wiki tasks from IRC, and check on thread status.""" @@ -106,7 +106,7 @@ class Command(BaseCommand): def do_listall(self): """With !tasks listall or !tasks all, list all loaded tasks, and report whether they are currently running or idle.""" - all_tasks = tasks.get_all().keys() + all_tasks = task_manager.get_all().keys() threads = threading.enumerate() tasklist = [] @@ -147,14 +147,14 @@ class Command(BaseCommand): self.connection.reply(data, msg) return - if task_name not in tasks.get_all().keys(): + if task_name not in task_manager.get_all().keys(): # This task does not exist or hasn't been loaded: - msg = "task could not be found; either bot/tasks/{0}.py doesn't exist, or it wasn't loaded correctly." + msg = "task could not be found; either tasks/{0}.py doesn't exist, or it wasn't loaded correctly." self.connection.reply(data, msg.format(task_name)) return data.kwargs["fromIRC"] = True - tasks.start(task_name, **data.kwargs) + task_manager.start(task_name, **data.kwargs) msg = "task \x0302{0}\x0301 started.".format(task_name) self.connection.reply(data, msg) diff --git a/earwigbot/main.py b/earwigbot/main.py index 59b3cfd..656633c 100644 --- a/earwigbot/main.py +++ b/earwigbot/main.py @@ -49,9 +49,9 @@ import logging import threading import time -from earwigbot import tasks -from earwigbot.irc import Frontend, Watcher from earwigbot.config import config +from earwigbot.irc import Frontend, Watcher +from earwigbot.tasks import task_manager logger = logging.getLogger("earwigbot") @@ -72,10 +72,7 @@ def wiki_scheduler(): primary thread if the IRC frontend is not enabled.""" while 1: time_start = time.time() - now = time.gmtime(time_start) - - tasks.schedule(now) - + task_manager.schedule() time_end = time.time() time_diff = time_start - time_end if time_diff < 60: # Sleep until the next minute @@ -90,7 +87,7 @@ def irc_frontend(): if config.components.get("wiki_schedule"): logger.info("Starting wiki scheduler") - tasks.load() + task_manager.load() t_scheduler = threading.Thread(target=wiki_scheduler) t_scheduler.name = "wiki-scheduler" t_scheduler.daemon = True @@ -119,7 +116,7 @@ def main(): # Run the scheduler on the main thread, but also run the IRC watcher on # another thread iff it is enabled: logger.info("Starting wiki scheduler") - tasks.load() + task_manager.load() if "irc_watcher" in enabled: logger.info("Starting IRC watcher") t_watcher = threading.Thread(target=irc_watcher) diff --git a/earwigbot/rules.py b/earwigbot/rules.py index 9e5190b..8b58b3b 100644 --- a/earwigbot/rules.py +++ b/earwigbot/rules.py @@ -29,7 +29,7 @@ recieves an event from IRC. import re -from earwigbot import tasks +from earwigbot.tasks import task_manager afc_prefix = "wikipedia( talk)?:(wikiproject )?articles for creation" @@ -56,7 +56,7 @@ def process(rc): chans.update(("##earwigbot", "#wikipedia-en-afc-feed")) if r_page.search(page_name): - #tasks.start("afc_copyvios", page=rc.page) + #task_manager.start("afc_copyvios", page=rc.page) chans.add("#wikipedia-en-afc-feed") elif r_ffu.match(page_name): @@ -76,7 +76,7 @@ def process(rc): elif rc.flags == "restore" and r_restore.match(comment): p = r_restored_page.findall(rc.comment)[0] - #tasks.start("afc_copyvios", page=p) + #task_manager.start("afc_copyvios", page=p) chans.add("#wikipedia-en-afc-feed") elif rc.flags == "protect" and r_protect.match(comment): diff --git a/earwigbot/tasks/__init__.py b/earwigbot/tasks/__init__.py index 75115e9..4af79af 100644 --- a/earwigbot/tasks/__init__.py +++ b/earwigbot/tasks/__init__.py @@ -23,8 +23,10 @@ """ EarwigBot's Wiki Task Manager -This package provides the wiki bot "tasks" EarwigBot runs. Here in __init__, -you can find some functions used to load and run these 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 the `task_manager` +singleton. """ import logging @@ -33,106 +35,199 @@ import sys import threading import time -from earwigbot.classes import BaseTask +from earwigbot import wiki from earwigbot.config import config -__all__ = ["load", "schedule", "start", "get", "get_all"] - -# Base directory when searching for tasks: -base_dir = os.path.dirname(os.path.abspath(__file__)) - -# Store loaded tasks as a dict where the key is the task name and the value is -# an instance of the task class: -_tasks = {} - -# Logger for this module: -logger = logging.getLogger("earwigbot.commands") - -def _load_task(filename): - """Try to load a specific task from a module, identified by file name.""" - global _tasks - - # Strip .py from the end of the filename and join with our package name: - name = ".".join(("tasks", filename[:-3])) - try: - __import__(name) - except: - logger.exception("Couldn't load file {0}:".format(filename)) - return - - task = sys.modules[name].Task() - task._setup_logger() - if not isinstance(task, BaseTask): - return - - _tasks[task.name] = task - logger.debug("Added task {0}".format(task.name)) - -def _wrapper(task, **kwargs): - """Wrapper for task classes: run the task and catch any errors.""" - try: - task.run(**kwargs) - except: - error = "Task '{0}' raised an exception and had to stop" - logger.exception(error.format(task.name)) - else: - logger.info("Task '{0}' finished without error".format(task.name)) - -def load(): - """Load all valid tasks from bot/tasks/, into the _tasks variable.""" - files = os.listdir(base_dir) - files.sort() - - for filename in files: - if filename.startswith("_") or not filename.endswith(".py"): - continue +__all__ = ["BaseTask", "task_manager"] + +class BaseTask(object): + """A base class for bot tasks that edit Wikipedia.""" + name = None + number = 0 + + def __init__(self): + """Constructor for new tasks. + + This is called once immediately after the task class is loaded by + the task manager (in tasks._load_task()). + """ + pass + + def _setup_logger(self): + """Set up a basic module-level logger.""" + logger_name = ".".join(("earwigbot", "tasks", self.name)) + self.logger = logging.getLogger(logger_name) + self.logger.setLevel(logging.DEBUG) + + def run(self, **kwargs): + """Main entry point to run a given task. + + This is called directly by tasks.start() and is the main way to make a + task do stuff. kwargs will be any keyword arguments passed to start() + which are entirely optional. + + The same task instance is preserved between runs, so you can + theoretically store data in self (e.g. + start('mytask', action='store', data='foo')) and then use it later + (e.g. start('mytask', action='save')). + """ + pass + + def make_summary(self, comment): + """Makes an edit summary by filling in variables in a config value. + + config.wiki["summary"] is used, where $2 is replaced by the main + summary body, given as a method arg, and $1 is replaced by the task + number. + + If the config value is not found, we just return the arg as-is. + """ try: - _load_task(filename) - except AttributeError: - pass # The file is doesn't contain a task, so just move on + summary = config.wiki["summary"] + except KeyError: + return comment + return summary.replace("$1", str(self.number)).replace("$2", comment) + + def shutoff_enabled(self, site=None): + """Returns whether on-wiki shutoff is enabled for this task. + + We check a certain page for certain content. This is determined by + our config file: config.wiki["shutoff"]["page"] is used as the title, + with $1 replaced by our username and $2 replaced by the task number, + and config.wiki["shutoff"]["disabled"] is used as the content. + + If the page has that content or the page does not exist, then shutoff + is "disabled", meaning the bot is supposed to run normally, and we + return False. If the page's content is something other than what we + expect, shutoff is enabled, and we return True. + + If a site is not provided, we'll try to use self.site if it's set. + Otherwise, we'll use our default site. + """ + if not site: + try: + site = self.site + except AttributeError: + site = wiki.get_site() + + try: + cfg = config.wiki["shutoff"] + except KeyError: + return False + title = cfg.get("page", "User:$1/Shutoff/Task $2") + username = site.get_user().name() + title = title.replace("$1", username).replace("$2", str(self.number)) + page = site.get_page(title) + + try: + content = page.get() + except wiki.PageNotFoundError: + return False + if content == cfg.get("disabled", "run"): + return False + + self.logger.warn("Emergency task shutoff has been enabled!") + return True + + +class _TaskManager(object): + def __init__(self): + self.logger = logging.getLogger("earwigbot.commands") + self._base_dir = os.path.dirname(os.path.abspath(__file__)) + self._tasks = {} + + def _load_task(self, filename): + """Load a specific task from a module, identified by file name.""" + # Strip .py from the filename's end and join with our package name: + name = ".".join(("tasks", filename[:-3])) + try: + __import__(name) + except: + self.logger.exception("Couldn't load file {0}:".format(filename)) + return - logger.info("Found {0} tasks: {1}".format(len(_tasks), ', '.join(_tasks.keys()))) + try: + task = sys.modules[name].Task() + except AttributeError: + return # No task in this module + if not isinstance(task, BaseTask): + return + task._setup_logger() -def schedule(now=time.gmtime()): - """Start all tasks that are supposed to be run at a given time.""" - # Get list of tasks to run this turn: - tasks = config.schedule(now.tm_min, now.tm_hour, now.tm_mday, now.tm_mon, - now.tm_wday) + self._tasks[task.name] = task + self.logger.debug("Added task {0}".format(task.name)) - for task in tasks: - if isinstance(task, list): # they've specified kwargs - start(task[0], **task[1]) # so pass those to start_task - else: # otherwise, just pass task_name - start(task) + def _wrapper(self, task, **kwargs): + """Wrapper for task classes: run the task and catch any errors.""" + try: + task.run(**kwargs) + except: + 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(self): + """Load all valid tasks from tasks/ into self._tasks.""" + files = os.listdir(self._base_dir) + files.sort() + + for filename in files: + if filename.startswith("_") or not filename.endswith(".py"): + continue + self._load_task(filename) + + msg = "Found {0} tasks: {1}" + tasks = ', '.join(self._tasks.keys()) + self.logger.info(msg.format(len(self._tasks), tasks)) + + def schedule(self, now=None): + """Start all tasks that are supposed to be run at a given time.""" + if not now: + now = time.gmtime() + # Get list of tasks to run this turn: + tasks = 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_task + else: # Otherwise, just pass task_name + self.start(task) + + def start(self, task_name, **kwargs): + """Start a given task in a new thread. Pass args to the task's run() + function.""" + msg = "Starting task '{0}' in a new thread" + self.logger.info(msg.format(task_name)) -def start(task_name, **kwargs): - """Start a given task in a new thread. Pass args to the task's run() - function.""" - logger.info("Starting task '{0}' in a new thread".format(task_name)) + try: + task = self._tasks[task_name] + except KeyError: + e = "Couldn't find task '{0}': bot/tasks/{0}.py does not exist" + self.logger.error(e.format(task_name)) + return - try: - task = _tasks[task_name] - except KeyError: - error = "Couldn't find task '{0}': bot/tasks/{0}.py does not exist" - logger.error(error.format(task_name)) - return + func = lambda: self._wrapper(task, **kwargs) + task_thread = threading.Thread(target=func) + start_time = time.strftime("%b %d %H:%M:%S") + task_thread.name = "{0} ({1})".format(task_name, start_time) - task_thread = threading.Thread(target=lambda: _wrapper(task, **kwargs)) - start_time = time.strftime("%b %d %H:%M:%S") - task_thread.name = "{0} ({1})".format(task_name, start_time) + # Stop bot task threads automagically if the main bot stops: + task_thread.daemon = True - # Stop bot task threads automagically if the main bot stops: - task_thread.daemon = True + task_thread.start() - task_thread.start() + def get(self, task_name): + """Return the class instance associated with a certain task name. -def get(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] - Will raise KeyError if the task is not found. - """ - return _tasks[task_name] + def get_all(self): + """Return our dict of all loaded tasks.""" + return self._tasks -def get_all(): - """Return our dict of all loaded tasks.""" - return _tasks +task_manager = _TaskManager() diff --git a/earwigbot/tasks/afc_catdelink.py b/earwigbot/tasks/afc_catdelink.py index 0708d49..c5d3c0f 100644 --- a/earwigbot/tasks/afc_catdelink.py +++ b/earwigbot/tasks/afc_catdelink.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to delink mainspace categories in declined [[WP:AFC]] diff --git a/earwigbot/tasks/afc_copyvios.py b/earwigbot/tasks/afc_copyvios.py index 2d3f5d7..4db5a63 100644 --- a/earwigbot/tasks/afc_copyvios.py +++ b/earwigbot/tasks/afc_copyvios.py @@ -27,8 +27,8 @@ from threading import Lock import oursql from earwigbot import wiki -from earwigbot.classes import BaseTask from earwigbot.config import config +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to check newly-edited [[WP:AFC]] submissions for copyright diff --git a/earwigbot/tasks/afc_dailycats.py b/earwigbot/tasks/afc_dailycats.py index 44bb093..efddd20 100644 --- a/earwigbot/tasks/afc_dailycats.py +++ b/earwigbot/tasks/afc_dailycats.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """ A task to create daily categories for [[WP:AFC]].""" diff --git a/earwigbot/tasks/afc_history.py b/earwigbot/tasks/afc_history.py index 8abe5f5..03117ad 100644 --- a/earwigbot/tasks/afc_history.py +++ b/earwigbot/tasks/afc_history.py @@ -32,8 +32,8 @@ from numpy import arange import oursql from earwigbot import wiki -from earwigbot.classes import BaseTask from earwigbot.config import config +from earwigbot.tasks import BaseTask # Valid submission statuses: STATUS_NONE = 0 diff --git a/earwigbot/tasks/afc_statistics.py b/earwigbot/tasks/afc_statistics.py index 1ae57d5..3de023d 100644 --- a/earwigbot/tasks/afc_statistics.py +++ b/earwigbot/tasks/afc_statistics.py @@ -30,8 +30,8 @@ from time import sleep import oursql from earwigbot import wiki -from earwigbot.classes import BaseTask from earwigbot.config import config +from earwigbot.tasks import BaseTask # Chart status number constants: CHART_NONE = 0 diff --git a/earwigbot/tasks/afc_undated.py b/earwigbot/tasks/afc_undated.py index b947cee..512f09d 100644 --- a/earwigbot/tasks/afc_undated.py +++ b/earwigbot/tasks/afc_undated.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to clear [[Category:Undated AfC submissions]].""" diff --git a/earwigbot/tasks/blptag.py b/earwigbot/tasks/blptag.py index c268b45..5bb4052 100644 --- a/earwigbot/tasks/blptag.py +++ b/earwigbot/tasks/blptag.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to add |blp=yes to {{WPB}} or {{WPBS}} when it is used along with diff --git a/earwigbot/tasks/feed_dailycats.py b/earwigbot/tasks/feed_dailycats.py index 9798eff..3d6afd7 100644 --- a/earwigbot/tasks/feed_dailycats.py +++ b/earwigbot/tasks/feed_dailycats.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to create daily categories for [[WP:FEED]].""" diff --git a/earwigbot/tasks/wrongmime.py b/earwigbot/tasks/wrongmime.py index 6096c92..1b51589 100644 --- a/earwigbot/tasks/wrongmime.py +++ b/earwigbot/tasks/wrongmime.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseTask +from earwigbot.tasks import BaseTask class Task(BaseTask): """A task to tag files whose extensions do not agree with their MIME