diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5b295..7126fd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,6 @@ All notable changes to WuttJamaican will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## v0.13.0 (2024-08-26) - -### Feat - -- add basic email handler support -- add `util.resource_path()` function -- add app handler method, `get_appdir()` -- add basic support for progress indicators -- add table/model for app upgrades - ## v0.12.1 (2024-08-22) ### Fix diff --git a/docs/api/wuttjamaican/db.model.upgrades.rst b/docs/api/wuttjamaican/db.model.upgrades.rst deleted file mode 100644 index f89fcf2..0000000 --- a/docs/api/wuttjamaican/db.model.upgrades.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.db.model.upgrades`` -================================== - -.. automodule:: wuttjamaican.db.model.upgrades - :members: diff --git a/docs/api/wuttjamaican/email.handler.rst b/docs/api/wuttjamaican/email.handler.rst deleted file mode 100644 index 4e4900f..0000000 --- a/docs/api/wuttjamaican/email.handler.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.email.handler`` -============================== - -.. automodule:: wuttjamaican.email.handler - :members: diff --git a/docs/api/wuttjamaican/email.message.rst b/docs/api/wuttjamaican/email.message.rst deleted file mode 100644 index 1656196..0000000 --- a/docs/api/wuttjamaican/email.message.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.email.message`` -============================== - -.. automodule:: wuttjamaican.email.message - :members: diff --git a/docs/api/wuttjamaican/email.rst b/docs/api/wuttjamaican/email.rst deleted file mode 100644 index d187d98..0000000 --- a/docs/api/wuttjamaican/email.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.email`` -====================== - -.. automodule:: wuttjamaican.email - :members: diff --git a/docs/api/wuttjamaican/enum.rst b/docs/api/wuttjamaican/enum.rst deleted file mode 100644 index 12b0081..0000000 --- a/docs/api/wuttjamaican/enum.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.enum`` -===================== - -.. automodule:: wuttjamaican.enum - :members: diff --git a/docs/api/wuttjamaican/index.rst b/docs/api/wuttjamaican/index.rst index 69a754e..43b1642 100644 --- a/docs/api/wuttjamaican/index.rst +++ b/docs/api/wuttjamaican/index.rst @@ -15,14 +15,8 @@ db.model db.model.auth db.model.base - db.model.upgrades db.sess - email - email.handler - email.message - enum exc people - progress testing util diff --git a/docs/api/wuttjamaican/progress.rst b/docs/api/wuttjamaican/progress.rst deleted file mode 100644 index 7a14cb3..0000000 --- a/docs/api/wuttjamaican/progress.rst +++ /dev/null @@ -1,6 +0,0 @@ - -``wuttjamaican.progress`` -========================= - -.. automodule:: wuttjamaican.progress - :members: diff --git a/docs/conf.py b/docs/conf.py index 23fc2cf..baf9505 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,6 @@ extensions = [ 'sphinxcontrib.programoutput', 'sphinx.ext.viewcode', 'sphinx.ext.todo', - 'enum_tools.autoenum', ] templates_path = ['_templates'] diff --git a/docs/glossary.rst b/docs/glossary.rst index 3b87762..c9b2f94 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -25,14 +25,6 @@ Glossary Usually this is named ``app`` and is located at the root of the virtual environment. - Can be retrieved via - :meth:`~wuttjamaican.app.AppHandler.get_appdir()`. - - app enum - Python module whose namespace contains all the "enum" values - used by the :term:`app`. Available on the :term:`app handler` - as :attr:`~wuttjamaican.app.AppHandler.enum`. - app handler Python object representing the core :term:`handler` for the :term:`app`. There is normally just one "global" app handler; @@ -124,12 +116,6 @@ Glossary In practice this generally refers to a :class:`~wuttjamaican.db.sess.Session` instance. - email handler - The :term:`handler` responsible for sending email on behalf of - the :term:`app`. - - Default is :class:`~wuttjamaican.email.handler.EmailHandler`. - entry point This refers to a "setuptools-style" entry point specifically, which is a mechanism used to register "plugins" and the like. diff --git a/pyproject.toml b/pyproject.toml index af34925..dab36b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "WuttJamaican" -version = "0.13.0" +version = "0.12.1" description = "Base package for Wutta Framework" readme = "README.md" authors = [{name = "Lance Edgar", email = "lance@edbob.org"}] @@ -27,16 +27,13 @@ classifiers = [ requires-python = ">= 3.8" dependencies = [ 'importlib-metadata; python_version < "3.10"', - "importlib_resources ; python_version < '3.9'", - "progress", "python-configuration", ] [project.optional-dependencies] -db = ["SQLAlchemy<2", "alembic", "alembic-postgresql-enum", "passlib"] -email = ["Mako"] -docs = ["Sphinx", "sphinxcontrib-programoutput", "enum-tools[sphinx]", "furo"] +db = ["SQLAlchemy<2", "alembic", "passlib"] +docs = ["Sphinx", "sphinxcontrib-programoutput", "furo"] tests = ["pytest-cov", "tox"] diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 5d67df2..0d1eb77 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -26,12 +26,9 @@ WuttJamaican - app handler import importlib import os -import sys import warnings -from wuttjamaican.util import (load_entry_points, load_object, - make_title, make_uuid, parse_bool, - progress_loop) +from wuttjamaican.util import load_entry_points, load_object, make_title, make_uuid, parse_bool class AppHandler: @@ -62,16 +59,6 @@ class AppHandler: need to call :meth:`get_model()` yourself - that part will happen automatically. - .. attribute:: enum - - Reference to the :term:`app enum` module. - - Note that :meth:`get_enum()` is responsible for determining - which module this will point to. However you can always get - the model using this attribute (e.g. ``app.enum``) and do not - need to call :meth:`get_enum()` yourself - that part will - happen automatically. - .. attribute:: providers Dictionary of :class:`AppProvider` instances, as returned by @@ -79,9 +66,7 @@ class AppHandler: """ default_app_title = "WuttJamaican" default_model_spec = 'wuttjamaican.db.model' - default_enum_spec = 'wuttjamaican.enum' default_auth_handler_spec = 'wuttjamaican.auth:AuthHandler' - default_email_handler_spec = 'wuttjamaican.email:EmailHandler' default_people_handler_spec = 'wuttjamaican.people:PeopleHandler' def __init__(self, config): @@ -118,9 +103,6 @@ class AppHandler: if name == 'model': return self.get_model() - if name == 'enum': - return self.get_enum() - if name == 'providers': self.providers = self.get_all_providers() return self.providers @@ -316,30 +298,6 @@ class AppHandler: self.model = importlib.import_module(spec) return self.model - def get_enum(self): - """ - Returns the :term:`app enum` module. - - Note that you don't actually need to call this method; you can - get the module by simply accessing :attr:`enum` - (e.g. ``app.enum``) instead. - - By default this will return :mod:`wuttjamaican.enum` unless - the config class or some :term:`config extension` has provided - another default. - - A custom app can override the default like so (within a config - extension):: - - config.setdefault('wutta.enum_spec', 'poser.enum') - """ - if 'enum' not in self.__dict__: - spec = self.config.get(f'{self.appname}.enum_spec', - usedb=False, - default=self.default_enum_spec) - self.enum = importlib.import_module(spec) - return self.enum - def load_object(self, spec): """ Import and/or load and return the object designated by the @@ -355,54 +313,6 @@ class AppHandler: """ return load_object(spec) - def get_appdir(self, *args, **kwargs): - """ - Returns path to the :term:`app dir`. - - This does not check for existence of the path, it only reads - it from config or (optionally) provides a default path. - - :param configured_only: Pass ``True`` here if you only want - the configured path and ignore the default path. - - :param create: Pass ``True`` here if you want to ensure the - returned path exists, creating it if necessary. - - :param \*args: Any additional args will be added as child - paths for the final value. - - For instance, assuming ``/srv/envs/poser`` is the virtual - environment root:: - - app.get_appdir() # => /srv/envs/poser/app - - app.get_appdir('data') # => /srv/envs/poser/app/data - """ - configured_only = kwargs.pop('configured_only', False) - create = kwargs.pop('create', False) - - # maybe specify default path - if not configured_only: - path = os.path.join(sys.prefix, 'app') - kwargs.setdefault('default', path) - - # get configured path - kwargs.setdefault('usedb', False) - path = self.config.get(f'{self.appname}.appdir', **kwargs) - - # add any subpath info - if path and args: - path = os.path.join(path, *args) - - # create path if requested/needed - if create: - if not path: - raise ValueError("appdir path unknown! so cannot create it.") - if not os.path.exists(path): - os.makedirs(path) - - return path - def make_appdir(self, path, subfolders=None, **kwargs): """ Establish an :term:`app dir` at the given path. @@ -469,18 +379,6 @@ class AppHandler: """ return make_uuid() - def progress_loop(self, *args, **kwargs): - """ - Convenience method to iterate over a set of items, invoking - logic for each, and updating a progress indicator along the - way. - - This is a wrapper around - :func:`wuttjamaican.util.progress_loop()`; see those docs for - param details. - """ - return progress_loop(*args, **kwargs) - def get_session(self, obj): """ Returns the SQLAlchemy session with which the given object is @@ -607,21 +505,6 @@ class AppHandler: self.handlers['auth'] = factory(self.config, **kwargs) return self.handlers['auth'] - def get_email_handler(self, **kwargs): - """ - Get the configured :term:`email handler`. - - See also :meth:`send_email()`. - - :rtype: :class:`~wuttjamaican.email.handler.EmailHandler` - """ - if 'email' not in self.handlers: - spec = self.config.get(f'{self.appname}.email.handler', - default=self.default_email_handler_spec) - factory = self.load_object(spec) - self.handlers['email'] = factory(self.config, **kwargs) - return self.handlers['email'] - def get_people_handler(self, **kwargs): """ Get the configured "people" :term:`handler`. @@ -650,15 +533,6 @@ class AppHandler: """ return self.get_people_handler().get_person(obj, **kwargs) - def send_email(self, *args, **kwargs): - """ - Send an email message. - - This is a convenience wrapper around - :meth:`~wuttjamaican.email.handler.EmailHandler.send_email()`. - """ - self.get_email_handler().send_email(*args, **kwargs) - class AppProvider: """ diff --git a/src/wuttjamaican/conf.py b/src/wuttjamaican/conf.py index c04b603..6c3adf5 100644 --- a/src/wuttjamaican/conf.py +++ b/src/wuttjamaican/conf.py @@ -440,8 +440,8 @@ class WuttaConfig: # raise error if required value not found if require: - message = message or "missing config" - raise ConfigurationError(f"{message}; set value for: {key}") + message = message or "missing or invalid config" + raise ConfigurationError(f"{message}; please set config value for: {key}") # give the default value if specified if default is not UNSPECIFIED: diff --git a/src/wuttjamaican/db/alembic/env.py b/src/wuttjamaican/db/alembic/env.py index 4a20bd5..2bc674c 100644 --- a/src/wuttjamaican/db/alembic/env.py +++ b/src/wuttjamaican/db/alembic/env.py @@ -1,6 +1,5 @@ # -*- coding: utf-8; -*- -import alembic_postgresql_enum from alembic import context from wuttjamaican.conf import make_config diff --git a/src/wuttjamaican/db/alembic/versions/ebd75b9feaa7_add_upgrades.py b/src/wuttjamaican/db/alembic/versions/ebd75b9feaa7_add_upgrades.py deleted file mode 100644 index 1ccbd66..0000000 --- a/src/wuttjamaican/db/alembic/versions/ebd75b9feaa7_add_upgrades.py +++ /dev/null @@ -1,46 +0,0 @@ -"""add upgrades - -Revision ID: ebd75b9feaa7 -Revises: 3abcc44f7f91 -Create Date: 2024-08-24 09:42:21.199679 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = 'ebd75b9feaa7' -down_revision: Union[str, None] = '3abcc44f7f91' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - - # upgrade - sa.Enum('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus').create(op.get_bind()) - op.create_table('upgrade', - sa.Column('uuid', sa.String(length=32), nullable=False), - sa.Column('created', sa.DateTime(timezone=True), nullable=False), - sa.Column('created_by_uuid', sa.String(length=32), nullable=False), - sa.Column('description', sa.String(length=255), nullable=False), - sa.Column('notes', sa.Text(), nullable=True), - sa.Column('executing', sa.Boolean(), nullable=False), - sa.Column('status', postgresql.ENUM('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus', create_type=False), nullable=False), - sa.Column('executed', sa.DateTime(timezone=True), nullable=True), - sa.Column('executed_by_uuid', sa.String(length=32), nullable=True), - sa.Column('exit_code', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['created_by_uuid'], ['user.uuid'], name=op.f('fk_upgrade_created_by_uuid_user')), - sa.ForeignKeyConstraint(['executed_by_uuid'], ['user.uuid'], name=op.f('fk_upgrade_executed_by_uuid_user')), - sa.PrimaryKeyConstraint('uuid', name=op.f('pk_upgrade')) - ) - - -def downgrade() -> None: - - # upgrade - op.drop_table('upgrade') - sa.Enum('PENDING', 'EXECUTING', 'SUCCESS', 'FAILURE', name='upgradestatus').drop(op.get_bind()) diff --git a/src/wuttjamaican/db/model/__init__.py b/src/wuttjamaican/db/model/__init__.py index 267738c..760e3a6 100644 --- a/src/wuttjamaican/db/model/__init__.py +++ b/src/wuttjamaican/db/model/__init__.py @@ -36,9 +36,7 @@ The ``wuttjamaican.db.model`` namespace contains the following: * :class:`~wuttjamaican.db.model.auth.Permission` * :class:`~wuttjamaican.db.model.auth.User` * :class:`~wuttjamaican.db.model.auth.UserRole` -* :class:`~wuttjamaican.db.model.upgrades.Upgrade` """ from .base import uuid_column, uuid_fk_column, Base, Setting, Person from .auth import Role, Permission, User, UserRole -from .upgrades import Upgrade diff --git a/src/wuttjamaican/db/model/upgrades.py b/src/wuttjamaican/db/model/upgrades.py deleted file mode 100644 index c8f3666..0000000 --- a/src/wuttjamaican/db/model/upgrades.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Upgrade Model -""" - -import datetime - -import sqlalchemy as sa -from sqlalchemy import orm - -from .base import Base, uuid_column, uuid_fk_column -from wuttjamaican.enum import UpgradeStatus - - -class Upgrade(Base): - """ - Represents an app upgrade. - """ - __tablename__ = 'upgrade' - - uuid = uuid_column() - - created = sa.Column(sa.DateTime(timezone=True), nullable=False, - default=datetime.datetime.now, doc=""" - When the upgrade record was created. - """) - - created_by_uuid = uuid_fk_column('user.uuid', nullable=False) - created_by = orm.relationship( - 'User', - foreign_keys=[created_by_uuid], - doc=""" - :class:`~wuttjamaican.db.model.auth.User` who created the - upgrade record. - """) - - description = sa.Column(sa.String(length=255), nullable=False, doc=""" - Basic (identifying) description for the upgrade. - """) - - notes = sa.Column(sa.Text(), nullable=True, doc=""" - Notes for the upgrade. - """) - - executing = sa.Column(sa.Boolean(), nullable=False, default=False, doc=""" - Whether or not the upgrade is currently being performed. - """) - - status = sa.Column(sa.Enum(UpgradeStatus), nullable=False, doc=""" - Current status for the upgrade. This field uses an enum, - :class:`~wuttjamaican.enum.UpgradeStatus`. - """) - - executed = sa.Column(sa.DateTime(timezone=True), nullable=True, doc=""" - When the upgrade was executed. - """) - - executed_by_uuid = uuid_fk_column('user.uuid', nullable=True) - executed_by = orm.relationship( - 'User', - foreign_keys=[executed_by_uuid], - doc=""" - :class:`~wuttjamaican.db.model.auth.User` who executed the - upgrade. - """) - - exit_code = sa.Column(sa.Integer(), nullable=True, doc=""" - Exit code for the upgrade execution process, if applicable. - """) - - def __str__(self): - return str(self.description or "") diff --git a/src/wuttjamaican/email/__init__.py b/src/wuttjamaican/email/__init__.py deleted file mode 100644 index 8702f9d..0000000 --- a/src/wuttjamaican/email/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Email Utilities - -The following are available in this ``wuttjamaican.email`` namespace: - -* :class:`~wuttjamaican.email.handler.EmailHandler` -* :class:`~wuttjamaican.email.message.Message` -""" - -from .handler import EmailHandler -from .message import Message diff --git a/src/wuttjamaican/email/handler.py b/src/wuttjamaican/email/handler.py deleted file mode 100644 index 59f328c..0000000 --- a/src/wuttjamaican/email/handler.py +++ /dev/null @@ -1,423 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Email Handler -""" - -import logging -import smtplib - -from wuttjamaican.app import GenericHandler -from wuttjamaican.util import resource_path -from wuttjamaican.email.message import Message - - -log = logging.getLogger(__name__) - - -class EmailHandler(GenericHandler): - """ - Base class and default implementation for the :term:`email - handler`. - - Responsible for sending email messages on behalf of the - :term:`app`. - - You normally would not create this directly, but instead call - :meth:`~wuttjamaican.app.AppHandler.get_email_handler()` on your - :term:`app handler`. - """ - - # nb. this is fallback/default subject for auto-message - universal_subject = "Automated message" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - from mako.lookup import TemplateLookup - - # prefer configured list of template lookup paths, if set - templates = self.config.get_list(f'{self.config.appname}.email.templates') - if not templates: - - # otherwise use all available paths, from app providers - available = [] - for provider in self.app.providers.values(): - if hasattr(provider, 'email_templates'): - templates = provider.email_templates - if isinstance(templates, str): - templates = [templates] - if templates: - available.extend(templates) - templates = available - - # convert all to true file paths - if templates: - templates = [resource_path(p) for p in templates] - - # will use these lookups from now on - self.txt_templates = TemplateLookup(directories=templates) - self.html_templates = TemplateLookup(directories=templates, - # nb. escape HTML special chars - # TODO: sounds great but i forget why? - default_filters=['h']) - - def make_message(self, **kwargs): - """ - Make and return a new email message. - - This is the "raw" factory which is simply a wrapper around the - class constructor. See also :meth:`make_auto_message()`. - - :returns: :class:`~wuttjamaican.email.message.Message` object. - """ - return Message(**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. - - Once everything has been collected/prepared, - :meth:`make_message()` is called to create the final message, - and that is returned. - - :param key: Unique key for this particular "type" of message. - This key is used as a prefix for all config settings and - template names pertinent to the message. - - :param context: Context dict used to render template(s) for - the message. - - :param \**kwargs: Any remaining kwargs are passed as-is to - :meth:`make_message()`. More on this below. - - :returns: :class:`~wuttjamaican.email.message.Message` object. - - This method may invoke some others, to gather the message - attributes. Each will check config, or render a template, or - both. However if a particular attribute is provided by the - caller, the corresponding "auto" method is skipped. - - * :meth:`get_auto_sender()` - * :meth:`get_auto_subject()` - * :meth:`get_auto_to()` - * :meth:`get_auto_cc()` - * :meth:`get_auto_bcc()` - * :meth:`get_auto_txt_body()` - * :meth:`get_auto_html_body()` - """ - kwargs['key'] = key - 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) - if 'to' not in kwargs: - kwargs['to'] = self.get_auto_to(key) - if 'cc' not in kwargs: - kwargs['cc'] = self.get_auto_cc(key) - if 'bcc' not in kwargs: - kwargs['bcc'] = self.get_auto_bcc(key) - if 'txt_body' not in kwargs: - kwargs['txt_body'] = self.get_auto_txt_body(key, context) - if 'html_body' not in kwargs: - kwargs['html_body'] = self.get_auto_html_body(key, context) - return self.make_message(**kwargs) - - def get_auto_sender(self, key): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.sender` address for - a message, as determined by config. - """ - # prefer configured sender specific to key - sender = self.config.get(f'{self.config.appname}.email.{key}.sender') - if sender: - return sender - - # fall back to global default (required!) - return self.config.require(f'{self.config.appname}.email.default.sender') - - def get_auto_subject(self, key, context={}, rendered=True): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.subject` line for a - message, as determined by config. - - This calls :meth:`get_auto_subject_template()` and then - renders the result using the given context. - - :param rendered: If this is ``False``, the "raw" subject - template will be returned, instead of the final/rendered - subject text. - """ - from mako.template import Template - - template = self.get_auto_subject_template(key) - if not rendered: - return template - return Template(template).render(**context) - - def get_auto_subject_template(self, key): - """ - Returns the template string to use for automatic subject line - of a message, as determined by config. - - In many cases this will be a simple string and not a - "template" per se; however it is still treated as a template. - - The template returned from this method is used to render the - final subject line in :meth:`get_auto_subject()`. - """ - # prefer configured subject specific to key - template = self.config.get(f'{self.config.appname}.email.{key}.subject') - if template: - return template - - # fall back to global default - return self.config.get(f'{self.config.appname}.email.default.subject', - default=self.universal_subject) - - def get_auto_to(self, key): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.to` recipient - address(es) for a message, as determined by config. - """ - return self.get_auto_recips(key, 'to') - - def get_auto_cc(self, key): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.cc` recipient - address(es) for a message, as determined by config. - """ - return self.get_auto_recips(key, 'cc') - - def get_auto_bcc(self, key): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.bcc` recipient - address(es) for a message, as determined by config. - """ - return self.get_auto_recips(key, 'bcc') - - def get_auto_recips(self, key, typ): - """ """ - typ = typ.lower() - if typ not in ('to', 'cc', 'bcc'): - raise ValueError("requested type not supported") - - # prefer configured recips specific to key - recips = self.config.get_list(f'{self.config.appname}.email.{key}.{typ}') - if recips: - return recips - - # fall back to global default - return self.config.get_list(f'{self.config.appname}.email.default.{typ}', - default=[]) - - def get_auto_txt_body(self, key, context={}): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.txt_body` content - for a message, as determined by config. This renders a - template with the given context. - """ - template = self.get_auto_body_template(key, 'txt') - if template: - return template.render(**context) - - def get_auto_html_body(self, key, context={}): - """ - Returns automatic - :attr:`~wuttjamaican.email.message.Message.html_body` content - for a message, as determined by config. This renders a - template with the given context. - """ - template = self.get_auto_body_template(key, 'html') - if template: - return template.render(**context) - - def get_auto_body_template(self, key, typ): - """ """ - from mako.exceptions import TopLevelLookupException - - typ = typ.lower() - if typ not in ('txt', 'html'): - raise ValueError("requested type not supported") - - if typ == 'txt': - templates = self.txt_templates - elif typ == 'html': - templates = self.html_templates - - try: - return templates.get_template(f'{key}.{typ}.mako') - except TopLevelLookupException: - pass - - def deliver_message(self, message, sender=None, recips=None): - """ - Deliver a message via SMTP smarthost. - - :param message: Either a - :class:`~wuttjamaican.email.message.Message` object or - similar, or a string representing the complete message to - be sent as-is. - - :param sender: Optional sender address to use for delivery. - If not specified, will be read from ``message``. - - :param recips: Optional recipient address(es) for delivery. - If not specified, will be read from ``message``. - - A general rule here is that you can either provide a proper - :class:`~wuttjamaican.email.message.Message` object, **or** - you *must* provide ``sender`` and ``recips``. The logic is - not smart enough (yet?) to parse sender/recips from a simple - string message. - - Note also, this method does not (yet?) have robust error - handling, so if an error occurs with the SMTP session, it will - simply raise to caller. - - :returns: ``None`` - """ - if not sender: - sender = message.sender - if not sender: - raise ValueError("no sender identified for message delivery") - - if not recips: - recips = set() - if message.to: - recips.update(message.to) - if message.cc: - recips.update(message.cc) - if message.bcc: - recips.update(message.bcc) - elif isinstance(recips, str): - recips = [recips] - - recips = set(recips) - if not recips: - raise ValueError("no recipients identified for message delivery") - - if not isinstance(message, str): - message = message.as_string() - - # get smtp info - server = self.config.get(f'{self.config.appname}.mail.smtp.server', default='localhost') - username = self.config.get(f'{self.config.appname}.mail.smtp.username') - password = self.config.get(f'{self.config.appname}.mail.smtp.password') - - # make sure sending is enabled - log.debug("sending email from %s; to %s", sender, recips) - if not self.sending_is_enabled(): - log.debug("nevermind, config says no emails") - return - - # smtp connect - session = smtplib.SMTP(server) - if username and password: - session.login(username, password) - - # smtp send - session.sendmail(sender, recips, message) - session.quit() - log.debug("email was sent") - - def sending_is_enabled(self): - """ - Returns boolean indicating if email sending is enabled. - - Set this flag in config like this: - - .. code-block:: ini - - [wutta.mail] - send_emails = true - - Note that it is OFF by default. - """ - return self.config.get_bool(f'{self.config.appname}.mail.send_emails', - default=False) - - def send_email(self, key=None, context={}, message=None, sender=None, recips=None, **kwargs): - """ - Send an email message. - - This method can send a ``message`` you provide, or it can - construct one automatically from key/config/templates. - - :param key: Indicates which "type" of automatic email to send. - Used to lookup config settings and template files. - - :param context: Context dict for rendering automatic email - template(s). - - :param message: Optional pre-built message instance, to send - as-is. - - :param sender: Optional sender address for the - message/delivery. - - If ``message`` is not provided, then the ``sender`` (if - provided) will also be used when constructing the - auto-message (i.e. to set the ``From:`` header). - - In any case if ``sender`` is provided, it will be used for - the actual SMTP delivery. - - :param recips: Optional list of recipient addresses for - delivery. If not specified, will be read from the message - itself (after auto-generating it, if applicable). - - .. note:: - - This param does not affect an auto-generated message; it - is used for delivery only. As such it must contain - *all* true recipients. - - If you provide the ``message`` but not the ``recips``, - the latter will be read from message headers: ``To:``, - ``Cc:`` and ``Bcc:`` - - If you want an auto-generated message but also want to - override various recipient headers, then you must - provide those explicitly:: - - context = {'data': [1, 2, 3]} - app.send_email('foo', context, to='me@example.com', cc='bobby@example.com') - - :param \**kwargs: Any remaining kwargs are passed along to - :meth:`make_auto_message()`. So, not used if you provide - the ``message``. - """ - if message is None: - if sender: - kwargs['sender'] = sender - message = self.make_auto_message(key, context, **kwargs) - - self.deliver_message(message, recips=recips) diff --git a/src/wuttjamaican/email/message.py b/src/wuttjamaican/email/message.py deleted file mode 100644 index 0e9f25e..0000000 --- a/src/wuttjamaican/email/message.py +++ /dev/null @@ -1,158 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Email Message -""" - -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - - -class Message: - """ - Represents an email message to be sent. - - :param to: Recipient(s) for the message. This may be either a - string, or list of strings. If a string, it will be converted - to a list since that is how the :attr:`to` attribute tracks it. - Similar logic is used for :attr:`cc` and :attr:`bcc`. - - All attributes shown below may also be specified via constructor. - - .. attribute:: key - - Unique key indicating the "type" of message. An "ad-hoc" - message created arbitrarily may not have/need a key; however - one created via - :meth:`~wuttjamaican.email.handler.EmailHandler.make_auto_message()` - will always have a key. - - This key is not used for anything within the ``Message`` class - logic. It is used by - :meth:`~wuttjamaican.email.handler.EmailHandler.make_auto_message()` - when constructing the message, and the key is set on the final - message only as a reference. - - .. attribute:: sender - - Sender (``From:``) address for the message. - - .. attribute:: subject - - Subject text for the message. - - .. attribute:: to - - List of ``To:`` recipients for the message. - - .. attribute:: cc - - List of ``Cc:`` recipients for the message. - - .. attribute:: bcc - - List of ``Bcc:`` recipients for the message. - - .. attribute:: replyto - - Optional reply-to (``Reply-To:``) address for the message. - - .. attribute:: txt_body - - String with the ``text/plain`` body content. - - .. attribute:: html_body - - String with the ``text/html`` body content. - """ - - def __init__( - self, - key=None, - sender=None, - subject=None, - to=None, - cc=None, - bcc=None, - replyto=None, - txt_body=None, - html_body=None, - ): - self.key = key - self.sender = sender - self.subject = subject - self.set_recips('to', to) - self.set_recips('cc', cc) - self.set_recips('bcc', bcc) - self.replyto = replyto - self.txt_body = txt_body - self.html_body = html_body - - def set_recips(self, name, value): - """ """ - if value: - if isinstance(value, str): - value = [value] - if not isinstance(value, (list, tuple)): - raise ValueError("must specify a string, tuple or list value") - else: - value = [] - setattr(self, name, list(value)) - - def as_string(self): - """ - Returns the complete message as string. This is called from - within - :meth:`~wuttjamaican.email.handler.EmailHandler.deliver_message()` - to obtain the SMTP payload. - """ - msg = None - - if self.txt_body and self.html_body: - txt = MIMEText(self.txt_body, _charset='utf_8') - html = MIMEText(self.html_body, _subtype='html', _charset='utf_8') - msg = MIMEMultipart(_subtype='alternative', _subparts=[txt, html]) - - elif self.txt_body: - msg = MIMEText(self.txt_body, _charset='utf_8') - - elif self.html_body: - msg = MIMEText(self.html_body, 'html', _charset='utf_8') - - if not msg: - raise ValueError("message has no body parts") - - msg['Subject'] = self.subject - msg['From'] = self.sender - - for addr in self.to: - msg['To'] = addr - for addr in self.cc: - msg['Cc'] = addr - for addr in self.bcc: - msg['Bcc'] = addr - - if self.replyto: - msg.add_header('Reply-To', self.replyto) - - return msg.as_string() diff --git a/src/wuttjamaican/enum.py b/src/wuttjamaican/enum.py deleted file mode 100644 index 3265745..0000000 --- a/src/wuttjamaican/enum.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Enum Values -""" - -from enum import Enum - - -class UpgradeStatus(Enum): - """ - Enum values for - :attr:`wuttjamaican.db.model.upgrades.Upgrade.status`. - """ - PENDING = 'pending' - EXECUTING = 'executing' - SUCCESS = 'success' - FAILURE = 'failure' diff --git a/src/wuttjamaican/progress.py b/src/wuttjamaican/progress.py deleted file mode 100644 index 712675c..0000000 --- a/src/wuttjamaican/progress.py +++ /dev/null @@ -1,113 +0,0 @@ -# -*- coding: utf-8; -*- -################################################################################ -# -# WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 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 . -# -################################################################################ -""" -Progress Indicators -""" - -import sys - -from progress.bar import Bar - - -class ProgressBase: - """ - Base class for progress indicators. - - This is *only* a base class, and should not be used directly. For - simple console use, see :class:`ConsoleProgress`. - - Progress indicators are created via factory from various places in - the code. The factory is called with ``(message, maximum)`` args - and it must return a progress instance with these methods: - - * :meth:`update()` - * :meth:`finish()` - - Code may call ``update()`` several times while its operation - continues; it then ultimately should call ``finish()``. - - See also :func:`wuttjamaican.util.progress_loop()` and - :meth:`wuttjamaican.app.AppHandler.progress_loop()` for a way to - do these things automatically from code. - - :param message: Info message to be displayed along with the - progress bar. - - :param maximum: Max progress value. - """ - - def __init__(self, message, maximum): - self.message = message - self.maximum = maximum - - def update(self, value): - """ - Update the current progress value. - - :param value: New progress value to be displayed. - """ - - def finish(self): - """ - Wrap things up for the progress display etc. - """ - - -class ConsoleProgress(ProgressBase): - """ - Provides a console-based progress bar. - - This is a subclass of :class:`ProgressBase`. - - Simple usage is like:: - - from wuttjamaican.progress import ConsoleProgress - - def action(obj, i): - print(obj) - - items = [1, 2, 3, 4, 5] - - app = config.get_app() - app.progress_loop(action, items, ConsoleProgress, - message="printing items") - - See also :func:`~wuttjamaican.util.progress_loop()`. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args) - - self.stderr = kwargs.get('stderr', sys.stderr) - self.stderr.write(f"\n{self.message}...\n") - - self.bar = Bar(message='', max=self.maximum, width=70, - suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds') - - def update(self, value): - """ """ - self.bar.next() - - def finish(self): - """ """ - self.bar.finish() diff --git a/src/wuttjamaican/testing.py b/src/wuttjamaican/testing.py index a51f3b5..069d130 100644 --- a/src/wuttjamaican/testing.py +++ b/src/wuttjamaican/testing.py @@ -97,11 +97,3 @@ class FileConfigTestCase(TestCase): with open(path, 'wt') as f: f.write(content) return path - - def mkdir(self, dirname): - """ - Make a new temporary folder and return its path. - - Note that this will be created *underneath* :attr:`tempdir`. - """ - return tempfile.mkdtemp(dir=self.tempdir) diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index 342868e..51bdb03 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -26,7 +26,6 @@ WuttJamaican - utilities import importlib import logging -import os import shlex from uuid import uuid1 @@ -211,99 +210,3 @@ def parse_list(value): elif value.startswith("'") and value.endswith("'"): values[i] = value[1:-1] return values - - -def progress_loop(func, items, factory, message=None): - """ - Convenience function to iterate over a set of items, invoking - logic for each, and updating a progress indicator along the way. - - This function may also be called via the :term:`app handler`; see - :meth:`~wuttjamaican.app.AppHandler.progress_loop()`. - - The ``factory`` will be called to create the progress indicator, - which should be an instance of - :class:`~wuttjamaican.progress.ProgressBase`. - - The ``factory`` may also be ``None`` in which case there is no - progress, and this is really just a simple "for loop". - - :param func: Callable to be invoked for each item in the sequence. - See below for more details. - - :param items: Sequence of items over which to iterate. - - :param factory: Callable which creates/returns a progress - indicator, or can be ``None`` for no progress. - - :param message: Message to display along with the progress - indicator. If no message is specified, whether a default is - shown will be up to the progress indicator. - - The ``func`` param should be a callable which accepts 2 positional - args ``(obj, i)`` - meaning for which is as follows: - - :param obj: This will be an item within the sequence. - - :param i: This will be the *one-based* sequence number for the - item. - - See also :class:`~wuttjamaican.progress.ConsoleProgress` for a - usage example. - """ - progress = None - if factory: - count = len(items) - progress = factory(message, count) - - for i, item in enumerate(items, 1): - - func(item, i) - - if progress: - progress.update(i) - - if progress: - progress.finish() - - -def resource_path(path): - """ - Returns the absolute file path for the given resource path. - - A "resource path" is one which designates a python package name, - plus some path under that. For instance: - - .. code-block:: none - - wuttjamaican.email:templates - - Assuming such a path should exist, the question is "where?" - - So this function uses :mod:`python:importlib.resources` to locate - the path, possibly extracting the file(s) from a zipped package, - and returning the final path on disk. - - It only does this if it detects it is needed, based on the given - ``path`` argument. If that is already an absolute path then it - will be returned as-is. - - :param path: Either a package resource specifier as shown above, - or regular file path. - - :returns: Absolute file path to the resource. - """ - if not os.path.isabs(path) and ':' in path: - - try: - # nb. these were added in python 3.9 - from importlib.resources import files, as_file - except ImportError: # python < 3.9 - from importlib_resources import files, as_file - - package, filename = path.split(':') - ref = files(package) / filename - with as_file(ref) as path: - return str(path) - - return path diff --git a/tests/db/model/test_upgrades.py b/tests/db/model/test_upgrades.py deleted file mode 100644 index c40a193..0000000 --- a/tests/db/model/test_upgrades.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest import TestCase - -try: - from wuttjamaican.db.model import upgrades as mod -except ImportError: - pass -else: - - class TestUpgrade(TestCase): - - def test_str(self): - upgrade = mod.Upgrade(description="upgrade foo") - self.assertEqual(str(upgrade), "upgrade foo") diff --git a/tests/email/__init__.py b/tests/email/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/email/templates/test_foo.html.mako b/tests/email/templates/test_foo.html.mako deleted file mode 100644 index babdeaf..0000000 --- a/tests/email/templates/test_foo.html.mako +++ /dev/null @@ -1 +0,0 @@ -

hello from foo html template

diff --git a/tests/email/templates/test_foo.txt.mako b/tests/email/templates/test_foo.txt.mako deleted file mode 100644 index dcbc4c6..0000000 --- a/tests/email/templates/test_foo.txt.mako +++ /dev/null @@ -1 +0,0 @@ -hello from foo txt template diff --git a/tests/email/test_handler.py b/tests/email/test_handler.py deleted file mode 100644 index 63c4874..0000000 --- a/tests/email/test_handler.py +++ /dev/null @@ -1,403 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest import TestCase -from unittest.mock import patch, MagicMock - -from wuttjamaican.email import handler as mod -from wuttjamaican.email import Message -from wuttjamaican.conf import WuttaConfig -from wuttjamaican.util import resource_path -from wuttjamaican.exc import ConfigurationError - - -class TestEmailHandler(TestCase): - - def setUp(self): - self.config = WuttaConfig() - self.app = self.config.get_app() - - def make_handler(self, **kwargs): - return mod.EmailHandler(self.config, **kwargs) - - def test_constructor_lookups(self): - - # empty lookup paths by default, if no providers - with patch.object(self.app, 'providers', new={}): - handler = self.make_handler() - self.assertEqual(handler.txt_templates.directories, []) - self.assertEqual(handler.html_templates.directories, []) - - # provider may specify paths as list - providers = { - 'wuttatest': MagicMock(email_templates=['wuttjamaican.email:templates']), - } - with patch.object(self.app, 'providers', new=providers): - handler = self.make_handler() - path = resource_path('wuttjamaican.email:templates') - self.assertEqual(handler.txt_templates.directories, [path]) - self.assertEqual(handler.html_templates.directories, [path]) - - # provider may specify paths as string - providers = { - 'wuttatest': MagicMock(email_templates='wuttjamaican.email:templates'), - } - with patch.object(self.app, 'providers', new=providers): - handler = self.make_handler() - path = resource_path('wuttjamaican.email:templates') - self.assertEqual(handler.txt_templates.directories, [path]) - self.assertEqual(handler.html_templates.directories, [path]) - - def test_make_message(self): - handler = self.make_handler() - msg = handler.make_message() - self.assertIsInstance(msg, Message) - - def test_make_auto_message(self): - handler = self.make_handler() - - # error if default sender not defined - self.assertRaises(ConfigurationError, handler.make_auto_message, 'foo') - - # so let's define that - self.config.setdefault('wutta.email.default.sender', 'bob@example.com') - - # message is empty by default - msg = handler.make_auto_message('foo') - self.assertIsInstance(msg, Message) - self.assertEqual(msg.key, 'foo') - self.assertEqual(msg.sender, 'bob@example.com') - self.assertEqual(msg.subject, "Automated message") - self.assertEqual(msg.to, []) - self.assertEqual(msg.cc, []) - self.assertEqual(msg.bcc, []) - self.assertIsNone(msg.replyto) - self.assertIsNone(msg.txt_body) - self.assertIsNone(msg.html_body) - - # but if there is a proper email profile configured for key, - # then we should get back a more complete message - self.config.setdefault('wutta.email.test_foo.subject', "hello foo") - self.config.setdefault('wutta.email.test_foo.to', 'sally@example.com') - self.config.setdefault('wutta.email.templates', 'tests.email:templates') - handler = self.make_handler() - msg = handler.make_auto_message('test_foo') - self.assertEqual(msg.key, 'test_foo') - self.assertEqual(msg.sender, 'bob@example.com') - self.assertEqual(msg.subject, "hello foo") - self.assertEqual(msg.to, ['sally@example.com']) - self.assertEqual(msg.cc, []) - self.assertEqual(msg.bcc, []) - self.assertIsNone(msg.replyto) - self.assertEqual(msg.txt_body, "hello from foo txt template\n") - self.assertEqual(msg.html_body, "

hello from foo html template

\n") - - # *some* auto methods get skipped if caller specifies the - # kwarg at all; others get skipped if kwarg is empty - - # sender - with patch.object(handler, 'get_auto_sender') as get_auto_sender: - msg = handler.make_auto_message('foo', sender=None) - get_auto_sender.assert_not_called() - msg = handler.make_auto_message('foo') - get_auto_sender.assert_called_once_with('foo') - - # subject - with patch.object(handler, 'get_auto_subject') as get_auto_subject: - 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', {}) - - # to - with patch.object(handler, 'get_auto_to') as get_auto_to: - msg = handler.make_auto_message('foo', to=None) - get_auto_to.assert_not_called() - get_auto_to.return_value = None - msg = handler.make_auto_message('foo') - get_auto_to.assert_called_once_with('foo') - - # cc - with patch.object(handler, 'get_auto_cc') as get_auto_cc: - msg = handler.make_auto_message('foo', cc=None) - get_auto_cc.assert_not_called() - get_auto_cc.return_value = None - msg = handler.make_auto_message('foo') - get_auto_cc.assert_called_once_with('foo') - - # bcc - with patch.object(handler, 'get_auto_bcc') as get_auto_bcc: - msg = handler.make_auto_message('foo', bcc=None) - get_auto_bcc.assert_not_called() - get_auto_bcc.return_value = None - msg = handler.make_auto_message('foo') - get_auto_bcc.assert_called_once_with('foo') - - # txt_body - with patch.object(handler, 'get_auto_txt_body') as get_auto_txt_body: - msg = handler.make_auto_message('foo', txt_body=None) - get_auto_txt_body.assert_not_called() - msg = handler.make_auto_message('foo') - get_auto_txt_body.assert_called_once_with('foo', {}) - - # html_body - with patch.object(handler, 'get_auto_html_body') as get_auto_html_body: - msg = handler.make_auto_message('foo', html_body=None) - get_auto_html_body.assert_not_called() - msg = handler.make_auto_message('foo') - get_auto_html_body.assert_called_once_with('foo', {}) - - def test_get_auto_sender(self): - handler = self.make_handler() - - # error if none configured - self.assertRaises(ConfigurationError, handler.get_auto_sender, 'foo') - - # can set global default - self.config.setdefault('wutta.email.default.sender', 'bob@example.com') - self.assertEqual(handler.get_auto_sender('foo'), 'bob@example.com') - - # can set for key - self.config.setdefault('wutta.email.foo.sender', 'sally@example.com') - self.assertEqual(handler.get_auto_sender('foo'), 'sally@example.com') - - def test_get_auto_subject_template(self): - handler = self.make_handler() - - # global default - template = handler.get_auto_subject_template('foo') - self.assertEqual(template, "Automated message") - - # can configure alternate global default - self.config.setdefault('wutta.email.default.subject', "Wutta Message") - template = handler.get_auto_subject_template('foo') - self.assertEqual(template, "Wutta Message") - - # can configure just for key - self.config.setdefault('wutta.email.foo.subject', "Foo Message") - template = handler.get_auto_subject_template('foo') - self.assertEqual(template, "Foo Message") - - def test_get_auto_subject(self): - handler = self.make_handler() - - # global default - subject = handler.get_auto_subject('foo') - self.assertEqual(subject, "Automated message") - - # can configure alternate global default - self.config.setdefault('wutta.email.default.subject', "Wutta Message") - subject = handler.get_auto_subject('foo') - self.assertEqual(subject, "Wutta Message") - - # can configure just for key - self.config.setdefault('wutta.email.foo.subject', "Foo Message") - subject = handler.get_auto_subject('foo') - self.assertEqual(subject, "Foo Message") - - # proper template is rendered - self.config.setdefault('wutta.email.bar.subject', "${foo} Message") - subject = handler.get_auto_subject('bar', {'foo': "FOO"}) - self.assertEqual(subject, "FOO Message") - - # unless we ask it not to - subject = handler.get_auto_subject('bar', {'foo': "FOO"}, rendered=False) - self.assertEqual(subject, "${foo} Message") - - def test_get_auto_recips(self): - handler = self.make_handler() - - # error if bad type requested - self.assertRaises(ValueError, handler.get_auto_recips, 'foo', 'doesnotexist') - - # can configure global default - self.config.setdefault('wutta.email.default.to', 'admin@example.com') - recips = handler.get_auto_recips('foo', 'to') - self.assertEqual(recips, ['admin@example.com']) - - # can configure just for key - self.config.setdefault('wutta.email.foo.to', 'bob@example.com') - recips = handler.get_auto_recips('foo', 'to') - self.assertEqual(recips, ['bob@example.com']) - - def test_get_auto_body_template(self): - from mako.template import Template - - handler = self.make_handler() - - # error if bad request - self.assertRaises(ValueError, handler.get_auto_body_template, 'foo', 'BADTYPE') - - # empty by default - template = handler.get_auto_body_template('foo', 'txt') - self.assertIsNone(template) - - # but returns a template if it exists - providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), - } - with patch.object(self.app, 'providers', new=providers): - handler = self.make_handler() - template = handler.get_auto_body_template('test_foo', 'txt') - self.assertIsInstance(template, Template) - self.assertEqual(template.uri, 'test_foo.txt.mako') - - def test_get_auto_txt_body(self): - handler = self.make_handler() - - # empty by default - body = handler.get_auto_txt_body('some-random-email') - self.assertIsNone(body) - - # but returns body if template exists - providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), - } - with patch.object(self.app, 'providers', new=providers): - handler = self.make_handler() - body = handler.get_auto_txt_body('test_foo') - self.assertEqual(body, 'hello from foo txt template\n') - - def test_get_auto_html_body(self): - handler = self.make_handler() - - # empty by default - body = handler.get_auto_html_body('some-random-email') - self.assertIsNone(body) - - # but returns body if template exists - providers = { - 'wuttatest': MagicMock(email_templates=['tests.email:templates']), - } - with patch.object(self.app, 'providers', new=providers): - handler = self.make_handler() - body = handler.get_auto_html_body('test_foo') - self.assertEqual(body, '

hello from foo html template

\n') - - def test_deliver_message(self): - handler = self.make_handler() - - msg = handler.make_message(sender='bob@example.com', to='sally@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - - # no smtp session since sending email is disabled by default - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_not_called() - session.login.assert_not_called() - session.sendmail.assert_not_called() - - # now let's enable sending - self.config.setdefault('wutta.mail.send_emails', 'true') - - # smtp login not attempted by default - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_called_once_with('localhost') - session.login.assert_not_called() - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # but login attempted if config has credentials - self.config.setdefault('wutta.mail.smtp.username', 'bob') - self.config.setdefault('wutta.mail.smtp.password', 'seekrit') - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_called_once_with('localhost') - session.login.assert_called_once_with('bob', 'seekrit') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # error if no sender - msg = handler.make_message(to='sally@example.com') - self.assertRaises(ValueError, handler.deliver_message, msg) - - # error if no recips - msg = handler.make_message(sender='bob@example.com') - self.assertRaises(ValueError, handler.deliver_message, msg) - - # can set recips as list - msg = handler.make_message(sender='bob@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg, recips=['sally@example.com']) - smtplib.SMTP.assert_called_once_with('localhost') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # can set recips as string - msg = handler.make_message(sender='bob@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg, recips='sally@example.com') - smtplib.SMTP.assert_called_once_with('localhost') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # can set recips via to - msg = handler.make_message(sender='bob@example.com', to='sally@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_called_once_with('localhost') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # can set recips via cc - msg = handler.make_message(sender='bob@example.com', cc='sally@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_called_once_with('localhost') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - # can set recips via bcc - msg = handler.make_message(sender='bob@example.com', bcc='sally@example.com') - with patch.object(msg, 'as_string', return_value='msg-str'): - with patch.object(mod, 'smtplib') as smtplib: - session = MagicMock() - smtplib.SMTP.return_value = session - handler.deliver_message(msg) - smtplib.SMTP.assert_called_once_with('localhost') - session.sendmail.assert_called_once_with('bob@example.com', {'sally@example.com'}, 'msg-str') - - def test_sending_is_enabled(self): - handler = self.make_handler() - - # off by default - self.assertFalse(handler.sending_is_enabled()) - - # but can be turned on - self.config.setdefault('wutta.mail.send_emails', 'true') - self.assertTrue(handler.sending_is_enabled()) - - def test_send_email(self): - with patch.object(mod.EmailHandler, 'deliver_message') as deliver_message: - handler = self.make_handler() - - # deliver_message() is called - handler.send_email('foo', sender='bob@example.com', to='sally@example.com', - txt_body='hello world') - deliver_message.assert_called_once() - - # make_auto_message() called only if needed - with patch.object(handler, 'make_auto_message') as make_auto_message: - - msg = handler.make_message() - handler.send_email(message=msg) - make_auto_message.assert_not_called() - - handler.send_email('foo', sender='bob@example.com', to='sally@example.com', - txt_body='hello world') - make_auto_message.assert_called_once_with('foo', {}, - sender='bob@example.com', - to='sally@example.com', - txt_body='hello world') diff --git a/tests/email/test_message.py b/tests/email/test_message.py deleted file mode 100644 index f8ff67a..0000000 --- a/tests/email/test_message.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest import TestCase - -from wuttjamaican.email import message as mod - - -class TestMessage(TestCase): - - def make_message(self, **kwargs): - return mod.Message(**kwargs) - - def test_set_recips(self): - msg = self.make_message() - self.assertEqual(msg.to, []) - - # set as list - msg.set_recips('to', ['sally@example.com']) - self.assertEqual(msg.to, ['sally@example.com']) - - # set as tuple - msg.set_recips('to', ('barney@example.com',)) - self.assertEqual(msg.to, ['barney@example.com']) - - # set as string - msg.set_recips('to', 'wilma@example.com') - self.assertEqual(msg.to, ['wilma@example.com']) - - # set as null - msg.set_recips('to', None) - self.assertEqual(msg.to, []) - - # otherwise error - self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'}) - - def test_as_string(self): - - # error if no body - msg = self.make_message() - self.assertRaises(ValueError, msg.as_string) - - # txt body - msg = self.make_message(sender='bob@example.com', - txt_body="hello world") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # html body - msg = self.make_message(sender='bob@example.com', - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # txt + html body - msg = self.make_message(sender='bob@example.com', - txt_body="hello world", - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - - # everything - msg = self.make_message(sender='bob@example.com', - subject='meeting follow-up', - to='sally@example.com', - cc='marketing@example.com', - bcc='bob@example.com', - replyto='sales@example.com', - txt_body="hello world", - html_body="

hello world

") - complete = msg.as_string() - self.assertIn('From: bob@example.com', complete) - self.assertIn('Subject: meeting follow-up', complete) - self.assertIn('To: sally@example.com', complete) - self.assertIn('Cc: marketing@example.com', complete) - self.assertIn('Bcc: bob@example.com', complete) - self.assertIn('Reply-To: sales@example.com', complete) diff --git a/tests/test_app.py b/tests/test_app.py index ef4f254..35ec466 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -10,18 +10,14 @@ from unittest.mock import patch, MagicMock import pytest -import wuttjamaican.enum from wuttjamaican import app -from wuttjamaican.progress import ProgressBase from wuttjamaican.conf import WuttaConfig from wuttjamaican.util import UNSPECIFIED -from wuttjamaican.testing import FileConfigTestCase -class TestAppHandler(FileConfigTestCase): +class TestAppHandler(TestCase): def setUp(self): - self.setup_files() self.config = WuttaConfig(appname='wuttatest') self.app = app.AppHandler(self.config) self.config.app = self.app @@ -31,9 +27,6 @@ class TestAppHandler(FileConfigTestCase): self.assertEqual(self.app.handlers, {}) self.assertEqual(self.app.appname, 'wuttatest') - def test_get_enum(self): - self.assertIs(self.app.get_enum(), wuttjamaican.enum) - def test_load_object(self): # just confirm the method works on a basic level; the @@ -41,40 +34,6 @@ class TestAppHandler(FileConfigTestCase): obj = self.app.load_object('wuttjamaican.util:UNSPECIFIED') self.assertIs(obj, UNSPECIFIED) - def test_get_appdir(self): - - mockdir = self.mkdir('mockdir') - - # default appdir - with patch.object(sys, 'prefix', new=mockdir): - - # default is returned by default - appdir = self.app.get_appdir() - self.assertEqual(appdir, os.path.join(mockdir, 'app')) - - # but not if caller wants config only - appdir = self.app.get_appdir(configured_only=True) - self.assertIsNone(appdir) - - # also, cannot create if appdir path not known - self.assertRaises(ValueError, self.app.get_appdir, configured_only=True, create=True) - - # configured appdir - self.config.setdefault('wuttatest.appdir', mockdir) - appdir = self.app.get_appdir() - self.assertEqual(appdir, mockdir) - - # appdir w/ subpath - appdir = self.app.get_appdir('foo', 'bar') - self.assertEqual(appdir, os.path.join(mockdir, 'foo', 'bar')) - - # subpath is created - self.assertEqual(len(os.listdir(mockdir)), 0) - appdir = self.app.get_appdir('foo', 'bar', create=True) - self.assertEqual(appdir, os.path.join(mockdir, 'foo', 'bar')) - self.assertEqual(os.listdir(mockdir), ['foo']) - self.assertEqual(os.listdir(os.path.join(mockdir, 'foo')), ['bar']) - def test_make_appdir(self): # appdir is created, and 3 subfolders added by default @@ -352,19 +311,6 @@ class TestAppHandler(FileConfigTestCase): uuid = self.app.make_uuid() self.assertEqual(len(uuid), 32) - def test_progress_loop(self): - - def act(obj, i): - pass - - # with progress - self.app.progress_loop(act, [1, 2, 3], ProgressBase, - message="whatever") - - # without progress - self.app.progress_loop(act, [1, 2, 3], None, - message="whatever") - def test_get_session(self): try: import sqlalchemy as sa @@ -397,25 +343,12 @@ class TestAppHandler(FileConfigTestCase): auth = self.app.get_auth_handler() self.assertIsInstance(auth, AuthHandler) - def test_get_email_handler(self): - from wuttjamaican.email import EmailHandler - - mail = self.app.get_email_handler() - self.assertIsInstance(mail, EmailHandler) - def test_get_people_handler(self): from wuttjamaican.people import PeopleHandler people = self.app.get_people_handler() self.assertIsInstance(people, PeopleHandler) - def test_get_send_email(self): - from wuttjamaican.email import EmailHandler - - with patch.object(EmailHandler, 'send_email') as send_email: - self.app.send_email('foo') - send_email.assert_called_once_with('foo') - class TestAppProvider(TestCase): @@ -470,12 +403,6 @@ class TestAppProvider(TestCase): def test_getattr(self): - # enum - self.assertNotIn('enum', self.app.__dict__) - self.assertIs(self.app.enum, wuttjamaican.enum) - - # now we test that providers are loaded... - class FakeProvider(app.AppProvider): def fake_foo(self): return 42 @@ -490,16 +417,6 @@ class TestAppProvider(TestCase): self.assertIs(self.app.providers, fake_providers) get_all_providers.assert_called_once_with() - def test_getattr_model(self): - try: - import wuttjamaican.db.model - except ImportError: - pytest.skip("test not relevant without sqlalchemy") - - # model - self.assertNotIn('model', self.app.__dict__) - self.assertIs(self.app.model, wuttjamaican.db.model) - def test_getattr_providers(self): # collection of providers is loaded on demand diff --git a/tests/test_progress.py b/tests/test_progress.py deleted file mode 100644 index 16a6787..0000000 --- a/tests/test_progress.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8; -*- - -from unittest import TestCase - -from wuttjamaican import progress as mod - - -class TestProgressBase(TestCase): - - def test_basic(self): - - # sanity / coverage check - prog = mod.ProgressBase('testing', 2) - prog.update(1) - prog.update(2) - prog.finish() - - -class TestConsoleProgress(TestCase): - - def test_basic(self): - - # sanity / coverage check - prog = mod.ConsoleProgress('testing', 2) - prog.update(1) - prog.update(2) - prog.finish() diff --git a/tests/test_util.py b/tests/test_util.py index 3d350cd..0f2baf4 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -7,7 +7,6 @@ from unittest.mock import patch, MagicMock import pytest from wuttjamaican import util as mod -from wuttjamaican.progress import ProgressBase class A: pass @@ -261,59 +260,3 @@ class TestMakeTitle(TestCase): def test_basic(self): text = mod.make_title('foo_bar') self.assertEqual(text, "Foo Bar") - - -class TestProgressLoop(TestCase): - - def test_basic(self): - - def act(obj, i): - pass - - # with progress - mod.progress_loop(act, [1, 2, 3], ProgressBase, - message="whatever") - - # without progress - mod.progress_loop(act, [1, 2, 3], None, - message="whatever") - - -class TestResourcePath(TestCase): - - def test_basic(self): - - # package spec is resolved to path - path = mod.resource_path('wuttjamaican:util.py') - self.assertTrue(path.endswith('wuttjamaican/util.py')) - - # absolute path returned as-is - self.assertEqual(mod.resource_path('/tmp/doesnotexist.txt'), '/tmp/doesnotexist.txt') - - def test_basic_pre_python_3_9(self): - - # the goal here is to get coverage for code which would only - # run on python 3.8 and older, but we only need that coverage - # if we are currently testing python 3.9+ - if sys.version_info.major == 3 and sys.version_info.minor < 9: - pytest.skip("this test is not relevant before python 3.9") - - from importlib.resources import files, as_file - - orig_import = __import__ - - def mock_import(name, globals=None, locals=None, fromlist=(), level=0): - if name == 'importlib.resources': - raise ImportError - if name == 'importlib_resources': - return MagicMock(files=files, as_file=as_file) - return orig_import(name, globals, locals, fromlist, level) - - with patch('builtins.__import__', side_effect=mock_import): - - # package spec is resolved to path - path = mod.resource_path('wuttjamaican:util.py') - self.assertTrue(path.endswith('wuttjamaican/util.py')) - - # absolute path returned as-is - self.assertEqual(mod.resource_path('/tmp/doesnotexist.txt'), '/tmp/doesnotexist.txt')