diff --git a/docs/api/wuttjamaican.cli.problems.rst b/docs/api/wuttjamaican.cli.problems.rst deleted file mode 100644 index 1bb76c7..0000000 --- a/docs/api/wuttjamaican.cli.problems.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.cli.problems`` -============================= - -.. automodule:: wuttjamaican.cli.problems - :members: diff --git a/docs/api/wuttjamaican.problems.rst b/docs/api/wuttjamaican.problems.rst deleted file mode 100644 index 5448a79..0000000 --- a/docs/api/wuttjamaican.problems.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.problems`` -========================= - -.. automodule:: wuttjamaican.problems - :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 0e4c48a..48db25e 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -243,24 +243,6 @@ Glossary modules etc. which is installed via ``pip``. See also :doc:`narr/install/pkg`. - problem check - This refers to a special "report" which runs (usually) on a - nighty basis. Such a report is only looking for "problems" - and if any are found, an email notification is sent. - - Apps can define custom problem checks (based on - :class:`~wuttjamaican.problems.ProblemCheck`), which can then be - ran via the :term:`problem handler`. - - problem handler - The :term:`handler` responsible for finding and reporting on - "problems" with the data or system. Most typically this runs - nightly :term:`checks ` and will send email if - problems are found. - - Default handler is - :class:`~wuttjamaican.problems.ProblemHandler`. - provider Python object which "provides" extra functionality to some portion of the :term:`app`. Similar to a "plugin" concept; see diff --git a/docs/index.rst b/docs/index.rst index e5829e9..baa26ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -70,7 +70,6 @@ Contents api/wuttjamaican.cli.base api/wuttjamaican.cli.make_appdir api/wuttjamaican.cli.make_uuid - api/wuttjamaican.cli.problems api/wuttjamaican.conf api/wuttjamaican.db api/wuttjamaican.db.conf @@ -87,7 +86,6 @@ Contents api/wuttjamaican.exc api/wuttjamaican.install api/wuttjamaican.people - api/wuttjamaican.problems api/wuttjamaican.progress api/wuttjamaican.reports api/wuttjamaican.testing diff --git a/docs/narr/cli/builtin.rst b/docs/narr/cli/builtin.rst index f51deb9..2eed21d 100644 --- a/docs/narr/cli/builtin.rst +++ b/docs/narr/cli/builtin.rst @@ -51,15 +51,3 @@ Print a new universally-unique identifier to standard output. Defined in: :mod:`wuttjamaican.cli.make_uuid` .. program-output:: wutta make-uuid --help - - -.. _wutta-problems: - -``wutta problems`` ------------------- - -Find and report on problems with the data or system. - -Defined in: :mod:`wuttjamaican.cli.problems` - -.. program-output:: wutta problems --help diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index c7d0e23..113b7b7 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -26,7 +26,6 @@ WuttJamaican - app handler import datetime import importlib -import logging import os import sys import warnings @@ -38,9 +37,6 @@ from wuttjamaican.util import (load_entry_points, load_object, progress_loop, resource_path, simple_error) -log = logging.getLogger(__name__) - - class AppHandler: """ Base class and default implementation for top-level :term:`app @@ -92,7 +88,6 @@ class AppHandler: default_email_handler_spec = 'wuttjamaican.email:EmailHandler' default_install_handler_spec = 'wuttjamaican.install:InstallHandler' default_people_handler_spec = 'wuttjamaican.people:PeopleHandler' - default_problem_handler_spec = 'wuttjamaican.problems:ProblemHandler' default_report_handler_spec = 'wuttjamaican.reports:ReportHandler' def __init__(self, config): @@ -994,20 +989,6 @@ class AppHandler: self.handlers['people'] = factory(self.config, **kwargs) return self.handlers['people'] - def get_problem_handler(self, **kwargs): - """ - Get the configured :term:`problem handler`. - - :rtype: :class:`~wuttjamaican.problems.ProblemHandler` - """ - if 'problems' not in self.handlers: - spec = self.config.get(f'{self.appname}.problems.handler', - default=self.default_problem_handler_spec) - log.debug("problem_handler spec is: %s", spec) - factory = self.load_object(spec) - self.handlers['problems'] = factory(self.config, **kwargs) - return self.handlers['problems'] - def get_report_handler(self, **kwargs): """ Get the configured :term:`report handler`. diff --git a/src/wuttjamaican/cli/__init__.py b/src/wuttjamaican/cli/__init__.py index 048a3c4..0650b10 100644 --- a/src/wuttjamaican/cli/__init__.py +++ b/src/wuttjamaican/cli/__init__.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2025 Lance Edgar +# Copyright © 2023-2024 Lance Edgar # # This file is part of Wutta Framework. # @@ -36,7 +36,6 @@ from .base import wutta_typer, make_typer # nb. must bring in all modules for discovery to work from . import make_appdir from . import make_uuid -from . import problems # discover more commands, installed via other packages from .base import typer_eager_imports diff --git a/src/wuttjamaican/cli/problems.py b/src/wuttjamaican/cli/problems.py deleted file mode 100644 index 4135065..0000000 --- a/src/wuttjamaican/cli/problems.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2025 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -See also: :ref:`wutta-problems` -""" - -import sys -from typing import List - -import rich -import typer -from typing_extensions import Annotated - -from .base import wutta_typer - - -@wutta_typer.command() -def problems( - ctx: typer.Context, - - systems: Annotated[ - List[str], - typer.Option('--system', '-s', - help="System for which to perform checks; can be specified more " - "than once. If not specified, all systems are assumed.")] = None, - - problems: Annotated[ - List[str], - typer.Option('--problem', '-p', - help="Identify a particular problem check; can be specified " - "more than once. If not specified, all checks are assumed.")] = None, - - list_checks: Annotated[ - bool, - typer.Option('--list', '-l', - help="List available problem checks; optionally filtered per --system and --problem")] = False, -): - """ - Find and report on problems with the data or system. - """ - config = ctx.parent.wutta_config - app = config.get_app() - handler = app.get_problem_handler() - - # try to warn user if unknown system is specified; but otherwise ignore - supported = handler.get_supported_systems() - for key in (systems or []): - if key not in supported: - rich.print(f"\n[bold yellow]No problem reports exist for system: {key}[/bold yellow]") - - checks = handler.filter_problem_checks(systems=systems, problems=problems) - - if list_checks: - - count = 0 - organized = handler.organize_problem_checks(checks) - for system in sorted(organized): - rich.print(f"\n[bold]{system}[/bold]") - sys.stdout.write("-------------------------\n") - for problem in sorted(organized[system]): - sys.stdout.write(f"{problem}\n") - count += 1 - - sys.stdout.write("\n") - sys.stdout.write(f"found {count} problem checks\n") - - else: - handler.run_problem_checks(checks) diff --git a/src/wuttjamaican/email.py b/src/wuttjamaican/email.py index ab89081..a7f138f 100644 --- a/src/wuttjamaican/email.py +++ b/src/wuttjamaican/email.py @@ -166,10 +166,6 @@ class Message: .. attribute:: html_body String with the ``text/html`` body content. - - .. attribute:: attachments - - List of file attachments for the message. """ def __init__( @@ -183,7 +179,6 @@ class Message: replyto=None, txt_body=None, html_body=None, - attachments=None, ): self.key = key self.sender = sender @@ -194,7 +189,6 @@ class Message: self.replyto = replyto self.txt_body = txt_body self.html_body = html_body - self.attachments = attachments or [] def set_recips(self, name, value): """ """ @@ -230,13 +224,6 @@ class Message: if not msg: raise ValueError("message has no body parts") - if self.attachments: - for attachment in self.attachments: - if isinstance(attachment, str): - raise ValueError("must specify valid MIME attachments; this class cannot " - "auto-create them from file path etc.") - msg = MIMEMultipart(_subtype='mixed', _subparts=[msg] + self.attachments) - msg['Subject'] = self.subject msg['From'] = self.sender @@ -370,7 +357,7 @@ class EmailHandler(GenericHandler): """ return Message(**kwargs) - def make_auto_message(self, key, context={}, default_subject=None, **kwargs): + def make_auto_message(self, key, context={}, **kwargs): """ Make a new email message using config to determine its properties, and auto-generating body from a template. @@ -386,9 +373,6 @@ class EmailHandler(GenericHandler): :param context: Context dict used to render template(s) for the message. - :param default_subject: Optional :attr:`~Message.subject` - template/string to use, if config does not specify one. - :param \**kwargs: Any remaining kwargs are passed as-is to :meth:`make_message()`. More on this below. @@ -411,7 +395,7 @@ class EmailHandler(GenericHandler): if 'sender' not in kwargs: kwargs['sender'] = self.get_auto_sender(key) if 'subject' not in kwargs: - kwargs['subject'] = self.get_auto_subject(key, context, default=default_subject) + kwargs['subject'] = self.get_auto_subject(key, context) if 'to' not in kwargs: kwargs['to'] = self.get_auto_to(key) if 'cc' not in kwargs: @@ -452,7 +436,7 @@ class EmailHandler(GenericHandler): # fall back to global default, if present return self.config.get(f'{self.config.appname}.email.default.replyto') - def get_auto_subject(self, key, context={}, rendered=True, setting=None, default=None): + def get_auto_subject(self, key, context={}, rendered=True, setting=None): """ Returns automatic :attr:`~wuttjamaican.email.Message.subject` line for a message, as determined by config. @@ -473,17 +457,15 @@ class EmailHandler(GenericHandler): instance. This is passed along to :meth:`get_auto_subject_template()`. - :param default: Default subject to use if none is configured. - :returns: Final subject text, either "raw" or rendered. """ - template = self.get_auto_subject_template(key, setting=setting, default=default) + template = self.get_auto_subject_template(key, setting=setting) if not rendered: return template return Template(template).render(**context) - def get_auto_subject_template(self, key, setting=None, default=None): + def get_auto_subject_template(self, key, setting=None): """ Returns the template string to use for automatic subject line of a message, as determined by config. @@ -502,8 +484,6 @@ class EmailHandler(GenericHandler): optimization; otherwise it will be fetched if needed via :meth:`get_email_setting()`. - :param default: Default subject to use if none is configured. - :returns: Final subject template, as raw text. """ # prefer configured subject specific to key @@ -511,10 +491,6 @@ class EmailHandler(GenericHandler): if template: return template - # or use caller-specified default, if applicable - if default: - return default - # or subject from email setting, if defined if not setting: setting = self.get_email_setting(key) diff --git a/src/wuttjamaican/problems.py b/src/wuttjamaican/problems.py deleted file mode 100644 index 76a5282..0000000 --- a/src/wuttjamaican/problems.py +++ /dev/null @@ -1,418 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2025 Lance Edgar -# -# This file is part of Wutta Framework. -# -# Wutta Framework is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) any -# later version. -# -# Wutta Framework is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# Wutta Framework. If not, see . -# -################################################################################ -""" -Problem Checks + Handler -""" - -import calendar -import datetime -import importlib -import logging - -from wuttjamaican.app import GenericHandler - - -log = logging.getLogger(__name__) - - -class ProblemCheck: - """ - Base class for :term:`problem checks `. - - Each subclass must define logic for discovery of problems, - according to its purpose; see :meth:`find_problems()`. - - If the check does find problems, and an email is to be sent, the - check instance is also able to affect that email somewhat, e.g. by - adding an attachment. See :meth:`get_email_context()` and - :meth:`make_email_attachments()`. - - :param config: App :term:`config object`. - """ - - def __init__(self, config): - self.config = config - self.app = self.config.get_app() - - @property - def system_key(self): - """ - Key identifying which "system" the check pertains to. - - Many apps may only have one "system" which corresponds to the - app itself. However some apps may integrate with other - systems and have ability/need to check for problems on those - systems as well. - - See also :attr:`problem_key` and :attr:`title`. - """ - raise AttributeError(f"system_key not defined for {self.__class__}") - - @property - def problem_key(self): - """ - Key identifying this problem check. - - This key must be unique within the context of the "system" it - pertains to. - - See also :attr:`system_key` and :attr:`title`. - """ - raise AttributeError(f"problem_key not defined for {self.__class__}") - - @property - def title(self): - """ - Display title for the problem check. - - See also :attr:`system_key` and :attr:`problem_key`. - """ - raise AttributeError(f"title not defined for {self.__class__}") - - def find_problems(self): - """ - Find all problems relevant to this check. - - This should always return a list, although no constraint is - made on what type of elements it contains. - - :returns: List of problems found. - """ - return [] - - def get_email_context(self, problems, **kwargs): - """ - This can be used to add extra context for a specific check's - report email template. - - :param problems: List of problems found. - - :returns: Context dict for email template. - """ - return kwargs - - def make_email_attachments(self, context): - """ - Optionally generate some attachment(s) for the report email. - - :param context: Context dict for the report email. In - particular see ``context['problems']`` for main data. - - :returns: List of attachments, if applicable. - """ - - -class ProblemHandler(GenericHandler): - """ - Base class and default implementation for the :term:`problem - handler`. - - There is normally no need to instantiate this yourself; instead - call :meth:`~wuttjamaican.app.AppHandler.get_problem_handler()` on - the :term:`app handler`. - - The problem handler can be used to discover and run :term:`problem - checks `. In particular see: - - * :meth:`get_all_problem_checks()` - * :meth:`filter_problem_checks()` - * :meth:`run_problem_checks()` - """ - - def get_all_problem_checks(self): - """ - Return a list of all :term:`problem checks ` - which are "available" according to config. - - See also :meth:`filter_problem_checks()`. - - :returns: List of :class:`ProblemCheck` classes. - """ - checks = [] - modules = self.config.get_list(f'{self.config.appname}.problems.modules', - default=['wuttjamaican.problems']) - for module_path in modules: - module = importlib.import_module(module_path) - for name in dir(module): - obj = getattr(module, name) - if (isinstance(obj, type) and - issubclass(obj, ProblemCheck) and - obj is not ProblemCheck): - checks.append(obj) - return checks - - def filter_problem_checks(self, systems=None, problems=None): - """ - Return a list of all :term:`problem checks ` - which match the given criteria. - - This first calls :meth:`get_all_problem_checks()` and then - filters the result according to params. - - :param systems: Optional list of "system keys" which a problem check - must match, in order to be included in return value. - - :param problems: Optional list of "problem keys" which a problem check - must match, in order to be included in return value. - - :returns: List of :class:`ProblemCheck` classes; may be an - empty list. - """ - all_checks = self.get_all_problem_checks() - if not (systems or problems): - return all_checks - - matches = [] - for check in all_checks: - if not systems or check.system_key in systems: - if not problems or check.problem_key in problems: - matches.append(check) - return matches - - def get_supported_systems(self, checks=None): - """ - Returns list of keys for all systems which are supported by - any of the problem checks. - - :param checks: Optional list of :class:`ProblemCheck` classes. - If not specified, calls :meth:`get_all_problem_checks()`. - - :returns: List of system keys. - """ - checks = self.get_all_problem_checks() - return sorted(set([check.system_key for check in checks])) - - def get_system_title(self, system_key): - """ - Returns the display title for a given system. - - The default logic returns the ``system_key`` as-is; subclass - may override as needed. - - :param system_key: Key identifying a checked system. - - :returns: Display title for the system. - """ - return system_key - - def is_enabled(self, check): - """ - Returns boolean indicating if the given problem check is - enabled, per config. - - :param check: :class:`ProblemCheck` class or instance. - - :returns: ``True`` if enabled; ``False`` if not. - """ - key = f'{check.system_key}.{check.problem_key}' - enabled = self.config.get_bool(f'{self.config.appname}.problems.{key}.enabled') - if enabled is not None: - return enabled - return True - - def should_run_for_weekday(self, check, weekday): - """ - Returns boolean indicating if the given problem check is - configured to run for the given weekday. - - :param check: :class:`ProblemCheck` class or instance. - - :param weekday: Integer corresponding to a particular weekday. - Uses the same conventions as Python itself, i.e. Monday is - represented as 0 and Sunday as 6. - - :returns: ``True`` if check should run; ``False`` if not. - """ - key = f'{check.system_key}.{check.problem_key}' - enabled = self.config.get_bool(f'{self.config.appname}.problems.{key}.day{weekday}') - if enabled is not None: - return enabled - return True - - def organize_problem_checks(self, checks): - """ - Organize the problem checks by grouping them according to - their :attr:`~ProblemCheck.system_key`. - - :param checks: List of :class:`ProblemCheck` classes. - - :returns: Dict with "system" keys; each value is a list of - problem checks pertaining to that system. - """ - organized = {} - - for check in checks: - system = organized.setdefault(check.system_key, {}) - system[check.problem_key] = check - - return organized - - def run_problem_checks(self, checks, force=False): - """ - Run the given problem checks. - - This calls :meth:`run_problem_check()` for each, so config is - consulted to determine if each check should actually run - - unless ``force=True``. - - :param checks: List of :class:`ProblemCheck` classes. - - :param force: If true, run the checks regardless of whether - each is configured to run. - """ - organized = self.organize_problem_checks(checks) - for system_key in sorted(organized): - system = organized[system_key] - for problem_key in sorted(system): - check = system[problem_key] - self.run_problem_check(check, force=force) - - def run_problem_check(self, check, force=False): - """ - Run the given problem check, if it is enabled and configured - to run for the current weekday. - - Running a check involves calling :meth:`find_problems()` and - possibly :meth:`send_problem_report()`. - - See also :meth:`run_problem_checks()`. - - :param check: :class:`ProblemCheck` class. - - :param force: If true, run the check regardless of whether it - is configured to run. - """ - key = f'{check.system_key}.{check.problem_key}' - log.info("running problem check: %s", key) - - if not self.is_enabled(check): - log.debug("problem check is not enabled: %s", key) - if not force: - return - - weekday = datetime.date.today().weekday() - if not self.should_run_for_weekday(check, weekday): - log.debug("problem check is not scheduled for %s: %s", - calendar.day_name[weekday], key) - if not force: - return - - check_instance = check(self.config) - problems = self.find_problems(check_instance) - log.info("found %s problems", len(problems)) - if problems: - self.send_problem_report(check_instance, problems) - return problems - - def find_problems(self, check): - """ - Execute the given check to find relevant problems. - - This mostly calls :meth:`ProblemCheck.find_problems()` - although subclass may override if needed. - - This should always return a list, although no constraint is - made on what type of elements it contains. - - :param check: :class:`ProblemCheck` instance. - - :returns: List of problems found. - """ - return check.find_problems() or [] - - def get_email_key(self, check): - """ - Return the "email key" to be used when sending report email - resulting from the given problem check. - - This follows a convention using the check's - :attr:`~ProblemCheck.system_key` and - :attr:`~ProblemCheck.problem_key`. - - This is called by :meth:`send_problem_report()`. - - :param check: :class:`ProblemCheck` class or instance. - - :returns: Config key for problem report email message. - """ - return f'{check.system_key}_problems_{check.problem_key}' - - def send_problem_report(self, check, problems): - """ - Send an email with details of the given problem check report. - - This calls :meth:`get_email_key()` to determine which key to - use for sending email. - - It also calls :meth:`get_global_email_context()` and - :meth:`get_check_email_context()` to build the email template - context. - - And it calls :meth:`ProblemCheck.make_email_attachments()` to - allow the check to provide message attachments. - - :param check: :class:`ProblemCheck` instance. - - :param problems: List of problems found. - """ - context = self.get_global_email_context() - context = self.get_check_email_context(check, problems, **context) - context.update({ - 'config': self.config, - 'app': self.app, - 'check': check, - 'problems': problems, - }) - - email_key = self.get_email_key(check) - attachments = check.make_email_attachments(context) - self.app.send_email(email_key, context, - default_subject=check.title, - attachments=attachments) - - def get_global_email_context(self, **kwargs): - """ - This can be used to add extra context for all email report - templates, regardless of which problem check is involved. - - :returns: Context dict for all email templates. - """ - return kwargs - - def get_check_email_context(self, check, problems, **kwargs): - """ - This can be used to add extra context for a specific check's - report email template. - - Note that this calls :meth:`ProblemCheck.get_email_context()` - and in many cases that is where customizations should live. - - :param check: :class:`ProblemCheck` instance. - - :param problems: List of problems found. - - :returns: Context dict for email template. - """ - kwargs['system_title'] = self.get_system_title(check.system_key) - kwargs = check.get_email_context(problems, **kwargs) - return kwargs diff --git a/tests/cli/test_problems.py b/tests/cli/test_problems.py deleted file mode 100644 index c6dd6f8..0000000 --- a/tests/cli/test_problems.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest.mock import Mock, patch - -from wuttjamaican.testing import ConfigTestCase -from wuttjamaican.cli import problems as mod -from wuttjamaican.problems import ProblemHandler, ProblemCheck - - -class FakeCheck(ProblemCheck): - system_key = 'wuttatest' - problem_key = 'fake_check' - title = "Fake problem check" - - -class TestProblems(ConfigTestCase): - - def test_basic(self): - ctx = Mock() - ctx.parent.wutta_config = self.config - - # nb. avoid printing to console - with patch.object(mod.rich, 'print') as rich_print: - - # nb. use fake check - with patch.object(ProblemHandler, 'get_all_problem_checks', return_value=[FakeCheck]): - - with patch.object(ProblemHandler, 'run_problem_checks') as run_problem_checks: - - # list problem checks - orig_organize = ProblemHandler.organize_problem_checks - def mock_organize(checks): - return orig_organize(None, checks) - with patch.object(ProblemHandler, 'organize_problem_checks', side_effect=mock_organize) as organize_problem_checks: - mod.problems(ctx, list_checks=True) - organize_problem_checks.assert_called_once_with([FakeCheck]) - run_problem_checks.assert_not_called() - - # warning if unknown system key requested - rich_print.reset_mock() - # nb. just --list for convenience - # note that since we also specify invalid --system, no checks will - # match and hence nothing significant will be printed to stdout - mod.problems(ctx, list_checks=True, systems=['craziness']) - rich_print.assert_called_once() - self.assertEqual(len(rich_print.call_args.args), 1) - self.assertIn("No problem reports exist for system", rich_print.call_args.args[0]) - self.assertEqual(len(rich_print.call_args.kwargs), 0) - run_problem_checks.assert_not_called() - - # run problem checks - mod.problems(ctx) - run_problem_checks.assert_called_once_with([FakeCheck]) diff --git a/tests/test_app.py b/tests/test_app.py index e20e324..b17788c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -634,12 +634,6 @@ app_title = WuttaTest people = self.app.get_people_handler() self.assertIsInstance(people, PeopleHandler) - def test_get_problem_handler(self): - from wuttjamaican.problems import ProblemHandler - - handler = self.app.get_problem_handler() - self.assertIsInstance(handler, ProblemHandler) - def test_get_report_handler(self): from wuttjamaican.reports import ReportHandler diff --git a/tests/test_email.py b/tests/test_email.py index 8cf1623..1776723 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -1,6 +1,5 @@ # -*- coding: utf-8; -*- -from email.mime.text import MIMEText from unittest import TestCase from unittest.mock import patch, MagicMock @@ -9,7 +8,7 @@ import pytest from wuttjamaican import email as mod from wuttjamaican.util import resource_path from wuttjamaican.exc import ConfigurationError -from wuttjamaican.testing import ConfigTestCase, FileTestCase +from wuttjamaican.testing import ConfigTestCase class TestEmailSetting(ConfigTestCase): @@ -25,7 +24,7 @@ class TestEmailSetting(ConfigTestCase): self.assertEqual(setting.sample_data(), {}) -class TestMessage(FileTestCase): +class TestMessage(TestCase): def make_message(self, **kwargs): return mod.Message(**kwargs) @@ -78,26 +77,6 @@ class TestMessage(FileTestCase): complete = msg.as_string() self.assertIn('From: bob@example.com', complete) - # html + attachment - csv_part = MIMEText("foo,bar\n1,2", 'csv', 'utf_8') - msg = self.make_message(sender='bob@example.com', - html_body="

hello world

", - attachments=[csv_part]) - complete = msg.as_string() - self.assertIn('Content-Type: multipart/mixed; boundary=', complete) - self.assertIn('Content-Type: text/csv; charset="utf_8"', complete) - - # error if improper attachment - csv_path = self.write_file('data.csv', "foo,bar\n1,2") - msg = self.make_message(sender='bob@example.com', - html_body="

hello world

", - attachments=[csv_path]) - self.assertRaises(ValueError, msg.as_string) - try: - msg.as_string() - except ValueError as err: - self.assertIn("must specify valid MIME attachments", str(err)) - # everything msg = self.make_message(sender='bob@example.com', subject='meeting follow-up', @@ -291,7 +270,7 @@ class TestEmailHandler(ConfigTestCase): msg = handler.make_auto_message('foo', subject=None) get_auto_subject.assert_not_called() msg = handler.make_auto_message('foo') - get_auto_subject.assert_called_once_with('foo', {}, default=None) + get_auto_subject.assert_called_once_with('foo', {}) # to with patch.object(handler, 'get_auto_to') as get_auto_to: @@ -376,7 +355,7 @@ class TestEmailHandler(ConfigTestCase): template = handler.get_auto_subject_template('foo') self.assertEqual(template, "Foo Message") - # EmailSetting can provide default subject + # setting can provide default subject providers = { 'wuttatest': MagicMock(email_modules=['tests.test_email']), } @@ -385,10 +364,6 @@ class TestEmailHandler(ConfigTestCase): template = handler.get_auto_subject_template('mock_foo') self.assertEqual(template, "MOCK FOO!") - # caller can provide default subject - template = handler.get_auto_subject_template('mock_foo', default="whatever is clever") - self.assertEqual(template, "whatever is clever") - def test_get_auto_subject(self): handler = self.make_handler() @@ -401,10 +376,6 @@ class TestEmailHandler(ConfigTestCase): subject = handler.get_auto_subject('foo') self.assertEqual(subject, "Wutta Message") - # caller can provide default subject - subject = handler.get_auto_subject('foo', default="whatever is clever") - self.assertEqual(subject, "whatever is clever") - # can configure just for key self.config.setdefault('wutta.email.foo.subject', "Foo Message") subject = handler.get_auto_subject('foo') diff --git a/tests/test_problems.py b/tests/test_problems.py deleted file mode 100644 index a791bed..0000000 --- a/tests/test_problems.py +++ /dev/null @@ -1,270 +0,0 @@ -# -*- coding: utf-8; -*- - -import datetime -from unittest.mock import patch - -from wuttjamaican import problems as mod -from wuttjamaican.testing import ConfigTestCase - - -class TestProblemCheck(ConfigTestCase): - - def make_check(self): - return mod.ProblemCheck(self.config) - - def test_system_key(self): - check = self.make_check() - self.assertRaises(AttributeError, getattr, check, 'system_key') - - def test_problem_key(self): - check = self.make_check() - self.assertRaises(AttributeError, getattr, check, 'problem_key') - - def test_title(self): - check = self.make_check() - self.assertRaises(AttributeError, getattr, check, 'title') - - def test_find_problems(self): - check = self.make_check() - problems = check.find_problems() - self.assertEqual(problems, []) - - def test_get_email_context(self): - check = self.make_check() - problems = check.find_problems() - context = check.get_email_context(problems) - self.assertEqual(context, {}) - - def test_make_email_attachments(self): - check = self.make_check() - problems = check.find_problems() - context = check.get_email_context(problems) - attachments = check.make_email_attachments(context) - self.assertIsNone(attachments) - - -class FakeProblemCheck(mod.ProblemCheck): - system_key = 'wuttatest' - problem_key = 'fake_check' - title = "Fake problem check" - - # def find_problems(self): - # return [{'foo': 'bar'}] - - -class TestProblemHandler(ConfigTestCase): - - def setUp(self): - super().setUp() - self.handler = self.make_handler() - - def make_handler(self): - return mod.ProblemHandler(self.config) - - def test_get_all_problem_checks(self): - - # no checks by default - checks = self.handler.get_all_problem_checks() - self.assertIsInstance(checks, list) - self.assertEqual(len(checks), 0) - - # but let's configure our fake check - self.config.setdefault('wutta.problems.modules', 'tests.test_problems') - checks = self.handler.get_all_problem_checks() - self.assertIsInstance(checks, list) - self.assertEqual(len(checks), 1) - - def test_filtered_problem_checks(self): - - # no checks by default - checks = self.handler.filter_problem_checks() - self.assertIsInstance(checks, list) - self.assertEqual(len(checks), 0) - - # but let's configure our fake check - self.config.setdefault('wutta.problems.modules', 'tests.test_problems') - checks = self.handler.filter_problem_checks() - self.assertIsInstance(checks, list) - self.assertEqual(len(checks), 1) - - # filter by system_key - checks = self.handler.filter_problem_checks(systems=['wuttatest']) - self.assertEqual(len(checks), 1) - checks = self.handler.filter_problem_checks(systems=['something_else']) - self.assertEqual(len(checks), 0) - - # filter by problem_key - checks = self.handler.filter_problem_checks(problems=['fake_check']) - self.assertEqual(len(checks), 1) - checks = self.handler.filter_problem_checks(problems=['something_else']) - self.assertEqual(len(checks), 0) - - # filter by both - checks = self.handler.filter_problem_checks(systems=['wuttatest'], problems=['fake_check']) - self.assertEqual(len(checks), 1) - checks = self.handler.filter_problem_checks(systems=['wuttatest'], problems=['bad_check']) - self.assertEqual(len(checks), 0) - - def test_get_supported_systems(self): - - # no checks by default - systems = self.handler.get_supported_systems() - self.assertIsInstance(systems, list) - self.assertEqual(len(systems), 0) - - # but let's configure our fake check - self.config.setdefault('wutta.problems.modules', 'tests.test_problems') - systems = self.handler.get_supported_systems() - self.assertIsInstance(systems, list) - self.assertEqual(systems, ['wuttatest']) - - def test_get_system_title(self): - title = self.handler.get_system_title('wutta') - self.assertEqual(title, 'wutta') - - def test_is_enabled(self): - check = FakeProblemCheck(self.config) - - # enabled by default - self.assertTrue(self.handler.is_enabled(check)) - - # config can disable - self.config.setdefault('wutta.problems.wuttatest.fake_check.enabled', 'false') - self.assertFalse(self.handler.is_enabled(check)) - - def test_should_run_for_weekday(self): - check = FakeProblemCheck(self.config) - - # should run by default - for weekday in range(7): - self.assertTrue(self.handler.should_run_for_weekday(check, weekday)) - - # config can disable, e.g. for weekends - self.config.setdefault('wutta.problems.wuttatest.fake_check.day5', 'false') - self.config.setdefault('wutta.problems.wuttatest.fake_check.day6', 'false') - for weekday in range(5): - self.assertTrue(self.handler.should_run_for_weekday(check, weekday)) - for weekday in (5, 6): - self.assertFalse(self.handler.should_run_for_weekday(check, weekday)) - - def test_organize_problem_checks(self): - checks = [FakeProblemCheck] - - organized = self.handler.organize_problem_checks(checks) - self.assertIsInstance(organized, dict) - self.assertEqual(list(organized), ['wuttatest']) - self.assertIsInstance(organized['wuttatest'], dict) - self.assertEqual(list(organized['wuttatest']), ['fake_check']) - self.assertIs(organized['wuttatest']['fake_check'], FakeProblemCheck) - - def test_find_problems(self): - check = FakeProblemCheck(self.config) - problems = self.handler.find_problems(check) - self.assertEqual(problems, []) - - def test_get_email_key(self): - check = FakeProblemCheck(self.config) - key = self.handler.get_email_key(check) - self.assertEqual(key, 'wuttatest_problems_fake_check') - - def test_get_global_email_context(self): - context = self.handler.get_global_email_context() - self.assertEqual(context, {}) - - def test_get_check_email_context(self): - check = FakeProblemCheck(self.config) - problems = [] - context = self.handler.get_check_email_context(check, problems) - self.assertEqual(context, {'system_title': 'wuttatest'}) - - def test_send_problem_report(self): - check = FakeProblemCheck(self.config) - problems = [] - with patch.object(self.app, 'send_email') as send_email: - self.handler.send_problem_report(check, problems) - send_email.assert_called_once_with('wuttatest_problems_fake_check', { - 'system_title': 'wuttatest', - 'config': self.config, - 'app': self.app, - 'check': check, - 'problems': problems, - }, default_subject="Fake problem check", attachments=None) - - def test_run_problem_check(self): - with patch.object(FakeProblemCheck, 'find_problems') as find_problems: - with patch.object(self.handler, 'send_problem_report') as send_problem_report: - - # check runs by default - find_problems.return_value = [{'foo': 'bar'}] - problems = self.handler.run_problem_check(FakeProblemCheck) - self.assertEqual(problems, [{'foo': 'bar'}]) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once() - - # does not run if generally disabled - find_problems.reset_mock() - send_problem_report.reset_mock() - with patch.object(self.handler, 'is_enabled', return_value=False): - problems = self.handler.run_problem_check(FakeProblemCheck) - self.assertIsNone(problems) - find_problems.assert_not_called() - send_problem_report.assert_not_called() - - # unless caller gives force flag - problems = self.handler.run_problem_check(FakeProblemCheck, force=True) - self.assertEqual(problems, [{'foo': 'bar'}]) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once() - - # does not run if disabled for weekday - find_problems.reset_mock() - send_problem_report.reset_mock() - weekday = datetime.date.today().weekday() - self.config.setdefault(f'wutta.problems.wuttatest.fake_check.day{weekday}', 'false') - problems = self.handler.run_problem_check(FakeProblemCheck) - self.assertIsNone(problems) - find_problems.assert_not_called() - send_problem_report.assert_not_called() - - # unless caller gives force flag - problems = self.handler.run_problem_check(FakeProblemCheck, force=True) - self.assertEqual(problems, [{'foo': 'bar'}]) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once() - - def test_run_problem_checks(self): - with patch.object(FakeProblemCheck, 'find_problems') as find_problems: - with patch.object(self.handler, 'send_problem_report') as send_problem_report: - - # check runs by default - find_problems.return_value = [{'foo': 'bar'}] - self.handler.run_problem_checks([FakeProblemCheck]) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once() - - # does not run if generally disabled - find_problems.reset_mock() - send_problem_report.reset_mock() - with patch.object(self.handler, 'is_enabled', return_value=False): - self.handler.run_problem_checks([FakeProblemCheck]) - find_problems.assert_not_called() - send_problem_report.assert_not_called() - - # unless caller gives force flag - self.handler.run_problem_checks([FakeProblemCheck], force=True) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once() - - # does not run if disabled for weekday - find_problems.reset_mock() - send_problem_report.reset_mock() - weekday = datetime.date.today().weekday() - self.config.setdefault(f'wutta.problems.wuttatest.fake_check.day{weekday}', 'false') - self.handler.run_problem_checks([FakeProblemCheck]) - find_problems.assert_not_called() - send_problem_report.assert_not_called() - - # unless caller gives force flag - self.handler.run_problem_checks([FakeProblemCheck], force=True) - find_problems.assert_called_once_with() - send_problem_report.assert_called_once()