Compare commits
	
		
			26 commits
		
	
	
		
			a79f80b808
			...
			f34678b305
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f34678b305 | |||
| faa12005fb | |||
| 9075159d0d | |||
| 79bab4f9e1 | |||
| 87af670df6 | |||
| a47dfbad01 | |||
| da149e932d | |||
| f01f5911da | |||
| 967c7b5948 | |||
| baa291f289 | |||
| f86aeff788 | |||
| da395e1880 | |||
| 7a0860363c | |||
| ab01f552e8 | |||
| 2969f3b7a5 | |||
| 1f4f613b62 | |||
| e845c66d86 | |||
| 6ac1cacdb3 | |||
| 3a263d272c | |||
| 99e6b98627 | |||
| 3010c5f435 | |||
| 74c3db6e3a | |||
| 1a0fee3582 | |||
| 3deab0b747 | |||
| dfe2b94f4f | |||
| 3137f0a3da | 
					 24 changed files with 217 additions and 150 deletions
				
			
		
							
								
								
									
										19
									
								
								.pylintrc
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								.pylintrc
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,11 +1,14 @@
 | 
			
		|||
# -*- mode: conf; -*-
 | 
			
		||||
 | 
			
		||||
[MESSAGES CONTROL]
 | 
			
		||||
disable=all
 | 
			
		||||
enable=anomalous-backslash-in-string,
 | 
			
		||||
        dangerous-default-value,
 | 
			
		||||
        inconsistent-return-statements,
 | 
			
		||||
        redefined-argument-from-local,
 | 
			
		||||
        unspecified-encoding,
 | 
			
		||||
        unused-argument,
 | 
			
		||||
        unused-import,
 | 
			
		||||
disable=
 | 
			
		||||
        attribute-defined-outside-init,
 | 
			
		||||
        fixme,
 | 
			
		||||
        import-outside-toplevel,
 | 
			
		||||
        too-many-arguments,
 | 
			
		||||
        too-many-branches,
 | 
			
		||||
        too-many-instance-attributes,
 | 
			
		||||
        too-many-lines,
 | 
			
		||||
        too-many-locals,
 | 
			
		||||
        too-many-positional-arguments,
 | 
			
		||||
        too-many-public-methods,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,6 +30,7 @@ dependencies = [
 | 
			
		|||
        'importlib-metadata; python_version < "3.10"',
 | 
			
		||||
        "importlib_resources ; python_version < '3.9'",
 | 
			
		||||
        "Mako",
 | 
			
		||||
        "packaging",
 | 
			
		||||
        "progress",
 | 
			
		||||
        "python-configuration",
 | 
			
		||||
        "typer",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,7 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
"""
 | 
			
		||||
Package Version
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from importlib.metadata import version
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -929,8 +929,8 @@ class AppHandler:
 | 
			
		|||
 | 
			
		||||
        # registered via entry points
 | 
			
		||||
        registered = []
 | 
			
		||||
        for Handler in load_entry_points(f'{self.appname}.batch.{key}').values():
 | 
			
		||||
            spec = Handler.get_spec()
 | 
			
		||||
        for handler in load_entry_points(f'{self.appname}.batch.{key}').values():
 | 
			
		||||
            spec = handler.get_spec()
 | 
			
		||||
            if spec not in handlers:
 | 
			
		||||
                registered.append(spec)
 | 
			
		||||
        if registered:
 | 
			
		||||
| 
						 | 
				
			
			@ -1045,7 +1045,7 @@ class AppHandler:
 | 
			
		|||
        self.get_email_handler().send_email(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppProvider:
 | 
			
		||||
class AppProvider: # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Base class for :term:`app providers<app provider>`.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1123,6 +1123,7 @@ class GenericHandler:
 | 
			
		|||
    def __init__(self, config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.app = self.config.get_app()
 | 
			
		||||
        self.modules = {}
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def appname(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -1139,3 +1140,35 @@ class GenericHandler:
 | 
			
		|||
        Returns the class :term:`spec` string for the handler.
 | 
			
		||||
        """
 | 
			
		||||
        return f'{cls.__module__}:{cls.__name__}'
 | 
			
		||||
 | 
			
		||||
    def get_provider_modules(self, module_type):
 | 
			
		||||
        """
 | 
			
		||||
        Returns a list of all available modules of the given type.
 | 
			
		||||
 | 
			
		||||
        Not all handlers would need such a thing, but notable ones
 | 
			
		||||
        which do are the :term:`email handler` and :term:`report
 | 
			
		||||
        handler`.  Both can obtain classes (emails or reports) from
 | 
			
		||||
        arbitrary modules, and this method is used to locate them.
 | 
			
		||||
 | 
			
		||||
        This will discover all modules exposed by the app
 | 
			
		||||
        :term:`providers <provider>`, which expose an attribute with
 | 
			
		||||
        name like ``f"{module_type}_modules"``.
 | 
			
		||||
 | 
			
		||||
        :param module_type: Unique name referring to a particular
 | 
			
		||||
           "type" of modules to locate, e.g. ``'email'``.
 | 
			
		||||
 | 
			
		||||
        :returns: List of module objects.
 | 
			
		||||
        """
 | 
			
		||||
        if module_type not in self.modules:
 | 
			
		||||
            self.modules[module_type] = []
 | 
			
		||||
            for provider in self.app.providers.values():
 | 
			
		||||
                name = f'{module_type}_modules'
 | 
			
		||||
                if hasattr(provider, name):
 | 
			
		||||
                    modules = getattr(provider, name)
 | 
			
		||||
                    if modules:
 | 
			
		||||
                        if isinstance(modules, str):
 | 
			
		||||
                            modules = [modules]
 | 
			
		||||
                        for modpath in modules:
 | 
			
		||||
                            module = importlib.import_module(modpath)
 | 
			
		||||
                            self.modules[module_type].append(module)
 | 
			
		||||
        return self.modules[module_type]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -348,7 +348,7 @@ class BatchHandler(GenericHandler):
 | 
			
		|||
        * :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_text`
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    def why_not_execute(self, batch, user=None, **kwargs):
 | 
			
		||||
    def why_not_execute(self, batch, user=None, **kwargs): # pylint: disable=unused-argument
 | 
			
		||||
        """
 | 
			
		||||
        Returns text indicating the reason (if any) that a given batch
 | 
			
		||||
        should *not* be executed.
 | 
			
		||||
| 
						 | 
				
			
			@ -385,6 +385,7 @@ class BatchHandler(GenericHandler):
 | 
			
		|||
        raise a ``RuntimeError`` if text was returned.  This is done
 | 
			
		||||
        out of safety, to avoid relying on the user interface.
 | 
			
		||||
        """
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def describe_execution(self, batch, user=None, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -467,16 +468,16 @@ class BatchHandler(GenericHandler):
 | 
			
		|||
        if batch.executed:
 | 
			
		||||
            raise ValueError(f"batch has already been executed: {batch}")
 | 
			
		||||
 | 
			
		||||
        reason = self.why_not_execute(batch, user=user, **kwargs)
 | 
			
		||||
        reason = self.why_not_execute(batch, user=user, **kwargs) # pylint: disable=assignment-from-none
 | 
			
		||||
        if reason:
 | 
			
		||||
            raise RuntimeError(f"batch execution not allowed: {reason}")
 | 
			
		||||
 | 
			
		||||
        result = self.execute(batch, user=user, progress=progress, **kwargs)
 | 
			
		||||
        result = self.execute(batch, user=user, progress=progress, **kwargs) # pylint: disable=assignment-from-none
 | 
			
		||||
        batch.executed = datetime.datetime.now()
 | 
			
		||||
        batch.executed_by = user
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def execute(self, batch, user=None, progress=None, **kwargs):
 | 
			
		||||
    def execute(self, batch, user=None, progress=None, **kwargs): # pylint: disable=unused-argument
 | 
			
		||||
        """
 | 
			
		||||
        Execute the given batch.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -502,6 +503,7 @@ class BatchHandler(GenericHandler):
 | 
			
		|||
           whatever it likes, in which case that will be also returned
 | 
			
		||||
           to the caller from :meth:`do_execute()`.
 | 
			
		||||
        """
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def do_delete(self, batch, user, dry_run=False, progress=None, **kwargs): # pylint: disable=unused-argument
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,7 +44,7 @@ def problems(
 | 
			
		|||
                         help="System for which to perform checks; can be specified more "
 | 
			
		||||
                         "than once. If not specified, all systems are assumed.")] = None,
 | 
			
		||||
 | 
			
		||||
        problems: Annotated[
 | 
			
		||||
        problems: Annotated[ # pylint: disable=redefined-outer-name
 | 
			
		||||
            List[str],
 | 
			
		||||
            typer.Option('--problem', '-p',
 | 
			
		||||
                         help="Identify a particular problem check; can be specified "
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +53,8 @@ def problems(
 | 
			
		|||
        list_checks: Annotated[
 | 
			
		||||
            bool,
 | 
			
		||||
            typer.Option('--list', '-l',
 | 
			
		||||
                         help="List available problem checks; optionally filtered per --system and --problem")] = False,
 | 
			
		||||
                         help="List available problem checks; optionally filtered "
 | 
			
		||||
                         "per --system and --problem")] = False,
 | 
			
		||||
):
 | 
			
		||||
    """
 | 
			
		||||
    Find and report on problems with the data or system.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2024 Lance Edgar
 | 
			
		||||
#  Copyright © 2024-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ class DatabaseHandler(GenericHandler):
 | 
			
		|||
    Base class and default implementation for the :term:`db handler`.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def get_dialect(self, bind):
 | 
			
		||||
    def get_dialect(self, bind): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        return bind.url.get_dialect().name
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,10 +45,11 @@ import sqlalchemy as sa
 | 
			
		|||
from sqlalchemy import orm
 | 
			
		||||
from sqlalchemy.ext.associationproxy import association_proxy
 | 
			
		||||
 | 
			
		||||
from . import Base, uuid_column, uuid_fk_column
 | 
			
		||||
from wuttjamaican.db.util import uuid_column, uuid_fk_column
 | 
			
		||||
from wuttjamaican.db.model.base import Base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Role(Base):
 | 
			
		||||
class Role(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents an authentication role within the system; used for
 | 
			
		||||
    permission management.
 | 
			
		||||
| 
						 | 
				
			
			@ -120,7 +121,7 @@ class Role(Base):
 | 
			
		|||
        return self.name or ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Permission(Base):
 | 
			
		||||
class Permission(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents a permission granted to a role.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +146,7 @@ class Permission(Base):
 | 
			
		|||
        return self.permission or ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class User(Base):
 | 
			
		||||
class User(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents a user of the system.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +232,7 @@ class User(Base):
 | 
			
		|||
        return self.username or ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserRole(Base):
 | 
			
		||||
class UserRole(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents the association between a user and a role; i.e. the
 | 
			
		||||
    user "belongs" or "is assigned" to the role.
 | 
			
		||||
| 
						 | 
				
			
			@ -260,7 +261,7 @@ class UserRole(Base):
 | 
			
		|||
        """)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserAPIToken(Base):
 | 
			
		||||
class UserAPIToken(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    User authentication token for use with HTTP API
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			@ -285,9 +286,13 @@ class UserAPIToken(Base):
 | 
			
		|||
    Raw token string, to be used by API clients.
 | 
			
		||||
    """)
 | 
			
		||||
 | 
			
		||||
    created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
 | 
			
		||||
    Date/time when the token was created.
 | 
			
		||||
    """)
 | 
			
		||||
    created = sa.Column(
 | 
			
		||||
        sa.DateTime(timezone=True),
 | 
			
		||||
        nullable=False,
 | 
			
		||||
        default=datetime.datetime.now,
 | 
			
		||||
        doc="""
 | 
			
		||||
        Date/time when the token was created.
 | 
			
		||||
        """)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.description or ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
 | 
			
		|||
from wuttjamaican.db.util import naming_convention, ModelBase, uuid_column
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WuttaModelBase(ModelBase):
 | 
			
		||||
class WuttaModelBase(ModelBase): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Base class for data models, from which :class:`Base` inherits.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +123,7 @@ metadata = sa.MetaData(naming_convention=naming_convention)
 | 
			
		|||
Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Setting(Base):
 | 
			
		||||
class Setting(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents a :term:`config setting`.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,8 @@ from sqlalchemy import orm
 | 
			
		|||
from sqlalchemy.ext.declarative import declared_attr
 | 
			
		||||
from sqlalchemy.ext.orderinglist import ordering_list
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.db.model import uuid_column, User
 | 
			
		||||
from wuttjamaican.db.model.base import uuid_column
 | 
			
		||||
from wuttjamaican.db.model.auth import User
 | 
			
		||||
from wuttjamaican.db.util import UUID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -185,7 +186,7 @@ class BatchMixin:
 | 
			
		|||
    """
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def __table_args__(cls):
 | 
			
		||||
    def __table_args__(cls): # pylint: disable=no-self-argument
 | 
			
		||||
        return cls.__default_table_args__()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
| 
						 | 
				
			
			@ -200,7 +201,8 @@ class BatchMixin:
 | 
			
		|||
        )
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def batch_type(cls):
 | 
			
		||||
    def batch_type(cls): # pylint: disable=empty-docstring,no-self-argument
 | 
			
		||||
        """ """
 | 
			
		||||
        return cls.__tablename__
 | 
			
		||||
 | 
			
		||||
    uuid = uuid_column()
 | 
			
		||||
| 
						 | 
				
			
			@ -226,7 +228,8 @@ class BatchMixin:
 | 
			
		|||
    created_by_uuid = sa.Column(UUID(), nullable=False)
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def created_by(cls):
 | 
			
		||||
    def created_by(cls): # pylint: disable=empty-docstring,no-self-argument
 | 
			
		||||
        """ """
 | 
			
		||||
        return orm.relationship(
 | 
			
		||||
            User,
 | 
			
		||||
            primaryjoin=lambda: User.uuid == cls.created_by_uuid,
 | 
			
		||||
| 
						 | 
				
			
			@ -238,7 +241,8 @@ class BatchMixin:
 | 
			
		|||
    executed_by_uuid = sa.Column(UUID(), nullable=True)
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def executed_by(cls):
 | 
			
		||||
    def executed_by(cls): # pylint: disable=empty-docstring,no-self-argument
 | 
			
		||||
        """ """
 | 
			
		||||
        return orm.relationship(
 | 
			
		||||
            User,
 | 
			
		||||
            primaryjoin=lambda: User.uuid == cls.executed_by_uuid,
 | 
			
		||||
| 
						 | 
				
			
			@ -266,7 +270,7 @@ class BatchMixin:
 | 
			
		|||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BatchRowMixin:
 | 
			
		||||
class BatchRowMixin: # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Mixin base class for :term:`data models <data model>` which
 | 
			
		||||
    represent a :term:`batch row`.
 | 
			
		||||
| 
						 | 
				
			
			@ -377,7 +381,7 @@ class BatchRowMixin:
 | 
			
		|||
    uuid = uuid_column()
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def __table_args__(cls):
 | 
			
		||||
    def __table_args__(cls): # pylint: disable=no-self-argument
 | 
			
		||||
        return cls.__default_table_args__()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
| 
						 | 
				
			
			@ -394,7 +398,8 @@ class BatchRowMixin:
 | 
			
		|||
    batch_uuid = sa.Column(UUID(), nullable=False)
 | 
			
		||||
 | 
			
		||||
    @declared_attr
 | 
			
		||||
    def batch(cls):
 | 
			
		||||
    def batch(cls): # pylint: disable=empty-docstring,no-self-argument
 | 
			
		||||
        """ """
 | 
			
		||||
        batch_class = cls.__batch_class__
 | 
			
		||||
        row_class = cls
 | 
			
		||||
        batch_class.__row_class__ = row_class
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,13 +29,13 @@ import datetime
 | 
			
		|||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
 | 
			
		||||
from . import Base, uuid_column, uuid_fk_column
 | 
			
		||||
from wuttjamaican.enum import UpgradeStatus
 | 
			
		||||
from wuttjamaican.db.util import UUID
 | 
			
		||||
from wuttjamaican.db.util import UUID, uuid_column, uuid_fk_column
 | 
			
		||||
from wuttjamaican.util import make_true_uuid
 | 
			
		||||
from wuttjamaican.db.model.base import Base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Upgrade(Base):
 | 
			
		||||
class Upgrade(Base): # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Represents an app upgrade.
 | 
			
		||||
    """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2023 Lance Edgar
 | 
			
		||||
#  Copyright © 2023-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ from sqlalchemy import orm
 | 
			
		|||
Session = orm.sessionmaker()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class short_session:
 | 
			
		||||
class short_session: # pylint: disable=invalid-name
 | 
			
		||||
    """
 | 
			
		||||
    Context manager for a short-lived database session.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,8 +26,8 @@ Database Utilities
 | 
			
		|||
 | 
			
		||||
import uuid as _uuid
 | 
			
		||||
from importlib.metadata import version
 | 
			
		||||
from packaging.version import Version
 | 
			
		||||
 | 
			
		||||
from packaging.version import Version
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy import orm
 | 
			
		||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ if Version(version('SQLAlchemy')) < Version('2'): # pragma: no cover
 | 
			
		|||
    SA2 = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ModelBase:
 | 
			
		||||
class ModelBase: # pylint: disable=empty-docstring
 | 
			
		||||
    """ """
 | 
			
		||||
 | 
			
		||||
    def __iter__(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -69,7 +69,7 @@ class ModelBase:
 | 
			
		|||
        raise KeyError(f"model instance has no attr with key: {key}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UUID(sa.types.TypeDecorator):
 | 
			
		||||
class UUID(sa.types.TypeDecorator): # pylint: disable=abstract-method,too-many-ancestors
 | 
			
		||||
    """
 | 
			
		||||
    Platform-independent UUID type.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,36 +82,35 @@ class UUID(sa.types.TypeDecorator):
 | 
			
		|||
    """
 | 
			
		||||
    impl = sa.CHAR
 | 
			
		||||
    cache_ok = True
 | 
			
		||||
    """ """                     # nb. suppress sphinx autodoc for cache_ok
 | 
			
		||||
    """ """ # nb. suppress sphinx autodoc for cache_ok
 | 
			
		||||
 | 
			
		||||
    def load_dialect_impl(self, dialect):
 | 
			
		||||
    def load_dialect_impl(self, dialect): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        if dialect.name == "postgresql":
 | 
			
		||||
            return dialect.type_descriptor(PGUUID())
 | 
			
		||||
        else:
 | 
			
		||||
            return dialect.type_descriptor(sa.CHAR(32))
 | 
			
		||||
        return dialect.type_descriptor(sa.CHAR(32))
 | 
			
		||||
 | 
			
		||||
    def process_bind_param(self, value, dialect):
 | 
			
		||||
    def process_bind_param(self, value, dialect): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        if value is None:
 | 
			
		||||
            return value
 | 
			
		||||
        elif dialect.name == "postgresql":
 | 
			
		||||
 | 
			
		||||
        if dialect.name == "postgresql":
 | 
			
		||||
            return str(value)
 | 
			
		||||
        else:
 | 
			
		||||
            if not isinstance(value, _uuid.UUID):
 | 
			
		||||
                return "%.32x" % _uuid.UUID(value).int
 | 
			
		||||
            else:
 | 
			
		||||
                # hexstring
 | 
			
		||||
                return "%.32x" % value.int
 | 
			
		||||
 | 
			
		||||
    def process_result_value(self, value, dialect): # pylint: disable=unused-argument
 | 
			
		||||
        if not isinstance(value, _uuid.UUID):
 | 
			
		||||
            value = _uuid.UUID(value)
 | 
			
		||||
 | 
			
		||||
        # hexstring
 | 
			
		||||
        return f"{value.int:032x}"
 | 
			
		||||
 | 
			
		||||
    def process_result_value(self, value, dialect): # pylint: disable=unused-argument,empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        if value is None:
 | 
			
		||||
            return value
 | 
			
		||||
        else:
 | 
			
		||||
            if not isinstance(value, _uuid.UUID):
 | 
			
		||||
                value = _uuid.UUID(value)
 | 
			
		||||
            return value
 | 
			
		||||
        if not isinstance(value, _uuid.UUID):
 | 
			
		||||
            value = _uuid.UUID(value)
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def uuid_column(*args, **kwargs):
 | 
			
		||||
| 
						 | 
				
			
			@ -150,8 +149,8 @@ def make_topo_sortkey(model):
 | 
			
		|||
       containing model classes.
 | 
			
		||||
    """
 | 
			
		||||
    metadata = model.Base.metadata
 | 
			
		||||
    tables = dict([(table.name, i)
 | 
			
		||||
                   for i, table in enumerate(metadata.sorted_tables, 1)])
 | 
			
		||||
    tables = {table.name: i
 | 
			
		||||
              for i, table in enumerate(metadata.sorted_tables, 1)}
 | 
			
		||||
 | 
			
		||||
    def sortkey(name):
 | 
			
		||||
        cls = getattr(model, name)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,6 @@
 | 
			
		|||
Email Handler
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import importlib
 | 
			
		||||
import logging
 | 
			
		||||
import smtplib
 | 
			
		||||
from email.mime.multipart import MIMEMultipart
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +40,7 @@ from wuttjamaican.util import resource_path
 | 
			
		|||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmailSetting:
 | 
			
		||||
class EmailSetting: # pylint: disable=too-few-public-methods
 | 
			
		||||
    """
 | 
			
		||||
    Base class for all :term:`email settings <email setting>`.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -188,15 +187,15 @@ class Message:
 | 
			
		|||
        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.to = self.get_recips(to)
 | 
			
		||||
        self.cc = self.get_recips(cc)
 | 
			
		||||
        self.bcc = self.get_recips(bcc)
 | 
			
		||||
        self.replyto = replyto
 | 
			
		||||
        self.txt_body = txt_body
 | 
			
		||||
        self.html_body = html_body
 | 
			
		||||
        self.attachments = attachments or []
 | 
			
		||||
 | 
			
		||||
    def set_recips(self, name, value):
 | 
			
		||||
    def get_recips(self, value): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        if value:
 | 
			
		||||
            if isinstance(value, str):
 | 
			
		||||
| 
						 | 
				
			
			@ -205,7 +204,7 @@ class Message:
 | 
			
		|||
                raise ValueError("must specify a string, tuple or list value")
 | 
			
		||||
        else:
 | 
			
		||||
            value = []
 | 
			
		||||
        setattr(self, name, list(value))
 | 
			
		||||
        return list(value)
 | 
			
		||||
 | 
			
		||||
    def as_string(self):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -305,20 +304,12 @@ class EmailHandler(GenericHandler):
 | 
			
		|||
 | 
			
		||||
        This will discover all email modules exposed by the
 | 
			
		||||
        :term:`app`, and/or its :term:`providers <provider>`.
 | 
			
		||||
        """
 | 
			
		||||
        if not hasattr(self, '_email_modules'):
 | 
			
		||||
            self._email_modules = []
 | 
			
		||||
            for provider in self.app.providers.values():
 | 
			
		||||
                if hasattr(provider, 'email_modules'):
 | 
			
		||||
                    modules = provider.email_modules
 | 
			
		||||
                    if modules:
 | 
			
		||||
                        if isinstance(modules, str):
 | 
			
		||||
                            modules = [modules]
 | 
			
		||||
                        for module in modules:
 | 
			
		||||
                            module = importlib.import_module(module)
 | 
			
		||||
                            self._email_modules.append(module)
 | 
			
		||||
 | 
			
		||||
        return self._email_modules
 | 
			
		||||
        Calls
 | 
			
		||||
        :meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()`
 | 
			
		||||
        under the hood, for ``email`` module type.
 | 
			
		||||
        """
 | 
			
		||||
        return self.get_provider_modules('email')
 | 
			
		||||
 | 
			
		||||
    def get_email_settings(self):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -549,7 +540,7 @@ class EmailHandler(GenericHandler):
 | 
			
		|||
        """
 | 
			
		||||
        return self.get_auto_recips(key, 'bcc')
 | 
			
		||||
 | 
			
		||||
    def get_auto_recips(self, key, typ):
 | 
			
		||||
    def get_auto_recips(self, key, typ): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        typ = typ.lower()
 | 
			
		||||
        if typ not in ('to', 'cc', 'bcc'):
 | 
			
		||||
| 
						 | 
				
			
			@ -589,16 +580,15 @@ class EmailHandler(GenericHandler):
 | 
			
		|||
            return template.render(**context)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_auto_body_template(self, key, mode):
 | 
			
		||||
    def get_auto_body_template(self, key, mode): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        mode = mode.lower()
 | 
			
		||||
        if mode not in ('txt', 'html'):
 | 
			
		||||
            raise ValueError("requested mode not supported")
 | 
			
		||||
 | 
			
		||||
        if mode == 'txt':
 | 
			
		||||
            templates = self.txt_templates
 | 
			
		||||
        elif mode == 'html':
 | 
			
		||||
            templates = self.html_templates
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError("requested mode not supported")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            return templates.get_template(f'{key}.{mode}.mako')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -141,9 +141,10 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
 | 
			
		||||
        This is normally called by :meth:`run()`.
 | 
			
		||||
        """
 | 
			
		||||
        self.rprint("\n\t[blue]Welcome to {}![/blue]".format(self.app.get_title()))
 | 
			
		||||
        self.rprint(f"\n\t[blue]Welcome to {self.app.get_title()}![/blue]")
 | 
			
		||||
        self.rprint("\n\tThis tool will install and configure the app.")
 | 
			
		||||
        self.rprint("\n\t[italic]NB. You should already have created the database in PostgreSQL or MySQL.[/italic]")
 | 
			
		||||
        self.rprint("\n\t[italic]NB. You should already have created "
 | 
			
		||||
                    "the database in PostgreSQL or MySQL.[/italic]")
 | 
			
		||||
 | 
			
		||||
        # shall we continue?
 | 
			
		||||
        if not self.prompt_bool("continue?", True):
 | 
			
		||||
| 
						 | 
				
			
			@ -221,14 +222,15 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
        error = self.test_db_connection(dbinfo['dburl'])
 | 
			
		||||
        if error:
 | 
			
		||||
            self.rprint("[bold red]cannot connect![/bold red] ..error was:")
 | 
			
		||||
            self.rprint("\n{}".format(error))
 | 
			
		||||
            self.rprint(f"\n{error}")
 | 
			
		||||
            self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n")
 | 
			
		||||
            sys.exit(1)
 | 
			
		||||
        self.rprint("[bold green]good[/bold green]")
 | 
			
		||||
 | 
			
		||||
        return dbinfo
 | 
			
		||||
 | 
			
		||||
    def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass):
 | 
			
		||||
    def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        from sqlalchemy.engine import URL
 | 
			
		||||
 | 
			
		||||
        if dbtype == 'mysql':
 | 
			
		||||
| 
						 | 
				
			
			@ -243,7 +245,8 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
                          port=dbport,
 | 
			
		||||
                          database=dbname)
 | 
			
		||||
 | 
			
		||||
    def test_db_connection(self, url):
 | 
			
		||||
    def test_db_connection(self, url): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
        engine = sa.create_engine(url)
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +255,7 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
        # just need to test interaction and this is a neutral way
 | 
			
		||||
        try:
 | 
			
		||||
            sa.inspect(engine).has_table('whatever')
 | 
			
		||||
        except Exception as error:
 | 
			
		||||
        except Exception as error: # pylint: disable=broad-exception-caught
 | 
			
		||||
            return str(error)
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -446,8 +449,8 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
               'upgrade', 'heads']
 | 
			
		||||
        subprocess.check_call(cmd)
 | 
			
		||||
 | 
			
		||||
        self.rprint("\n\tdb schema installed to:  [bold green]{}[/bold green]".format(
 | 
			
		||||
            obfuscate_url_pw(db_url)))
 | 
			
		||||
        self.rprint("\n\tdb schema installed to:  "
 | 
			
		||||
                    f"[bold green]{obfuscate_url_pw(db_url)}[/bold green]")
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def show_goodbye(self):
 | 
			
		||||
| 
						 | 
				
			
			@ -469,7 +472,8 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
    # console utility functions
 | 
			
		||||
    ##############################
 | 
			
		||||
 | 
			
		||||
    def require_prompt_toolkit(self, answer=None):
 | 
			
		||||
    def require_prompt_toolkit(self, answer=None): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        try:
 | 
			
		||||
            import prompt_toolkit # pylint: disable=unused-import
 | 
			
		||||
        except ImportError:
 | 
			
		||||
| 
						 | 
				
			
			@ -491,7 +495,8 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
        """
 | 
			
		||||
        rich.print(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_prompt_style(self):
 | 
			
		||||
    def get_prompt_style(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        from prompt_toolkit.styles import Style
 | 
			
		||||
 | 
			
		||||
        # message formatting styles
 | 
			
		||||
| 
						 | 
				
			
			@ -540,9 +545,9 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
        ]
 | 
			
		||||
        if default is not None:
 | 
			
		||||
            if is_bool:
 | 
			
		||||
                message.append(('', ' [{}]: '.format('Y' if default else 'N')))
 | 
			
		||||
                message.append(('', f' [{"Y" if default else "N"}]: '))
 | 
			
		||||
            else:
 | 
			
		||||
                message.append(('', ' [{}]: '.format(default)))
 | 
			
		||||
                message.append(('', f' [{default}]: '))
 | 
			
		||||
        else:
 | 
			
		||||
            message.append(('', ': '))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -558,9 +563,9 @@ class InstallHandler(GenericHandler):
 | 
			
		|||
        if is_bool:
 | 
			
		||||
            if text == '':
 | 
			
		||||
                return default
 | 
			
		||||
            elif text.upper() == 'Y':
 | 
			
		||||
            if text.upper() == 'Y':
 | 
			
		||||
                return True
 | 
			
		||||
            elif text.upper() == 'N':
 | 
			
		||||
            if text.upper() == 'N':
 | 
			
		||||
                return False
 | 
			
		||||
            self.rprint("\n\t[bold yellow]ambiguous, please try again[/bold yellow]\n")
 | 
			
		||||
            return self.prompt_generic(info, default, is_bool=True)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ class PeopleHandler(GenericHandler):
 | 
			
		|||
            person = obj
 | 
			
		||||
            return person
 | 
			
		||||
 | 
			
		||||
        elif isinstance(obj, model.User):
 | 
			
		||||
        if isinstance(obj, model.User):
 | 
			
		||||
            user = obj
 | 
			
		||||
            if user.person:
 | 
			
		||||
                return user.person
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -200,7 +200,7 @@ class ProblemHandler(GenericHandler):
 | 
			
		|||
        :returns: List of system keys.
 | 
			
		||||
        """
 | 
			
		||||
        checks = self.get_all_problem_checks()
 | 
			
		||||
        return sorted(set([check.system_key for check in checks]))
 | 
			
		||||
        return sorted({check.system_key for check in checks})
 | 
			
		||||
 | 
			
		||||
    def get_system_title(self, system_key):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
			
		||||
#  Copyright © 2023-2024 Lance Edgar
 | 
			
		||||
#  Copyright © 2023-2025 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of Wutta Framework.
 | 
			
		||||
#
 | 
			
		||||
| 
						 | 
				
			
			@ -97,17 +97,17 @@ class ConsoleProgress(ProgressBase):
 | 
			
		|||
 | 
			
		||||
    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,
 | 
			
		||||
        self.bar = Bar(message='', max=self.maximum, width=70, # pylint: disable=disallowed-name
 | 
			
		||||
                       suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds')
 | 
			
		||||
 | 
			
		||||
    def update(self, value):
 | 
			
		||||
    def update(self, value): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.bar.next()
 | 
			
		||||
 | 
			
		||||
    def finish(self):
 | 
			
		||||
    def finish(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.bar.finish()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,8 +24,6 @@
 | 
			
		|||
Report Utilities
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import importlib
 | 
			
		||||
 | 
			
		||||
from wuttjamaican.app import GenericHandler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -143,20 +141,12 @@ class ReportHandler(GenericHandler):
 | 
			
		|||
 | 
			
		||||
        This will discover all report modules exposed by the
 | 
			
		||||
        :term:`app`, and/or its :term:`providers <provider>`.
 | 
			
		||||
        """
 | 
			
		||||
        if not hasattr(self, '_report_modules'):
 | 
			
		||||
            self._report_modules = []
 | 
			
		||||
            for provider in self.app.providers.values():
 | 
			
		||||
                if hasattr(provider, 'report_modules'):
 | 
			
		||||
                    modules = provider.report_modules
 | 
			
		||||
                    if modules:
 | 
			
		||||
                        if isinstance(modules, str):
 | 
			
		||||
                            modules = [modules]
 | 
			
		||||
                        for module in modules:
 | 
			
		||||
                            module = importlib.import_module(module)
 | 
			
		||||
                            self._report_modules.append(module)
 | 
			
		||||
 | 
			
		||||
        return self._report_modules
 | 
			
		||||
        Calls
 | 
			
		||||
        :meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()`
 | 
			
		||||
        under the hood, for ``report`` module type.
 | 
			
		||||
        """
 | 
			
		||||
        return self.get_provider_modules('report')
 | 
			
		||||
 | 
			
		||||
    def get_reports(self):
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ class FileTestCase(TestCase):
 | 
			
		|||
       class.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
    def setUp(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.setup_files()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,14 +63,14 @@ class FileTestCase(TestCase):
 | 
			
		|||
        """
 | 
			
		||||
        self.tempdir = tempfile.mkdtemp()
 | 
			
		||||
 | 
			
		||||
    def setup_file_config(self): # pragma: no cover
 | 
			
		||||
    def setup_file_config(self): # pragma: no cover; pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        warnings.warn("FileTestCase.setup_file_config() is deprecated; "
 | 
			
		||||
                      "please use setup_files() instead",
 | 
			
		||||
                      DeprecationWarning, stacklevel=2)
 | 
			
		||||
        self.setup_files()
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
    def tearDown(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.teardown_files()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +80,7 @@ class FileTestCase(TestCase):
 | 
			
		|||
        """
 | 
			
		||||
        shutil.rmtree(self.tempdir)
 | 
			
		||||
 | 
			
		||||
    def teardown_file_config(self): # pragma: no cover
 | 
			
		||||
    def teardown_file_config(self): # pragma: no cover; pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        warnings.warn("FileTestCase.teardown_file_config() is deprecated; "
 | 
			
		||||
                      "please use teardown_files() instead",
 | 
			
		||||
| 
						 | 
				
			
			@ -99,7 +99,7 @@ class FileTestCase(TestCase):
 | 
			
		|||
            f.write(content)
 | 
			
		||||
        return path
 | 
			
		||||
 | 
			
		||||
    def mkdir(self, dirname): # pylint: disable=unused-argument
 | 
			
		||||
    def mkdir(self, dirname): # pylint: disable=unused-argument,empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        warnings.warn("FileTestCase.mkdir() is deprecated; "
 | 
			
		||||
                      "please use FileTestCase.mkdtemp() instead",
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ class ConfigTestCase(FileTestCase):
 | 
			
		|||
       methods for this class.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
    def setUp(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.setup_config()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +155,7 @@ class ConfigTestCase(FileTestCase):
 | 
			
		|||
        self.config = self.make_config()
 | 
			
		||||
        self.app = self.config.get_app()
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
    def tearDown(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.teardown_config()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -165,7 +165,7 @@ class ConfigTestCase(FileTestCase):
 | 
			
		|||
        """
 | 
			
		||||
        self.teardown_files()
 | 
			
		||||
 | 
			
		||||
    def make_config(self, **kwargs):
 | 
			
		||||
    def make_config(self, **kwargs): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        return WuttaConfig(**kwargs)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +203,7 @@ class DataTestCase(FileTestCase):
 | 
			
		|||
       teardown methods, as this class handles that automatically.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
    def setUp(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.setup_db()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -222,7 +222,7 @@ class DataTestCase(FileTestCase):
 | 
			
		|||
        model.Base.metadata.create_all(bind=self.config.appdb_engine)
 | 
			
		||||
        self.session = self.app.make_session()
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
    def tearDown(self): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        self.teardown_db()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -232,6 +232,6 @@ class DataTestCase(FileTestCase):
 | 
			
		|||
        """
 | 
			
		||||
        self.teardown_files()
 | 
			
		||||
 | 
			
		||||
    def make_config(self, **kwargs):
 | 
			
		||||
    def make_config(self, **kwargs): # pylint: disable=empty-docstring
 | 
			
		||||
        """ """
 | 
			
		||||
        return WuttaConfig(**kwargs)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,7 +115,7 @@ def load_entry_points(group, ignore_errors=False):
 | 
			
		|||
    for entry_point in eps:
 | 
			
		||||
        try:
 | 
			
		||||
            ep = entry_point.load()
 | 
			
		||||
        except:
 | 
			
		||||
        except Exception: # pylint: disable=broad-exception-caught
 | 
			
		||||
            if not ignore_errors:
 | 
			
		||||
                raise
 | 
			
		||||
            log.warning("failed to load entry point: %s", entry_point,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -761,6 +761,9 @@ class TestGenericHandler(ConfigTestCase):
 | 
			
		|||
        kw.setdefault('appname', 'wuttatest')
 | 
			
		||||
        return super().make_config(**kw)
 | 
			
		||||
 | 
			
		||||
    def make_handler(self, **kwargs):
 | 
			
		||||
        return mod.GenericHandler(self.config, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def test_constructor(self):
 | 
			
		||||
        handler = mod.GenericHandler(self.config)
 | 
			
		||||
        self.assertIs(handler.config, self.config)
 | 
			
		||||
| 
						 | 
				
			
			@ -769,3 +772,30 @@ class TestGenericHandler(ConfigTestCase):
 | 
			
		|||
 | 
			
		||||
    def test_get_spec(self):
 | 
			
		||||
        self.assertEqual(mod.GenericHandler.get_spec(), 'wuttjamaican.app:GenericHandler')
 | 
			
		||||
 | 
			
		||||
    def test_get_provider_modules(self):
 | 
			
		||||
 | 
			
		||||
        # no providers, no email modules
 | 
			
		||||
        with patch.object(self.app, 'providers', new={}):
 | 
			
		||||
            handler = self.make_handler()
 | 
			
		||||
            self.assertEqual(handler.get_provider_modules('email'), [])
 | 
			
		||||
 | 
			
		||||
        # provider may specify modules as list
 | 
			
		||||
        providers = {
 | 
			
		||||
            'wuttatest': MagicMock(email_modules=['wuttjamaican.app']),
 | 
			
		||||
        }
 | 
			
		||||
        with patch.object(self.app, 'providers', new=providers):
 | 
			
		||||
            handler = self.make_handler()
 | 
			
		||||
            modules = handler.get_provider_modules('email')
 | 
			
		||||
            self.assertEqual(len(modules), 1)
 | 
			
		||||
            self.assertIs(modules[0], mod)
 | 
			
		||||
 | 
			
		||||
        # provider may specify modules as string
 | 
			
		||||
        providers = {
 | 
			
		||||
            'wuttatest': MagicMock(email_modules='wuttjamaican.app'),
 | 
			
		||||
        }
 | 
			
		||||
        with patch.object(self.app, 'providers', new=providers):
 | 
			
		||||
            handler = self.make_handler()
 | 
			
		||||
            modules = handler.get_provider_modules('email')
 | 
			
		||||
            self.assertEqual(len(modules), 1)
 | 
			
		||||
            self.assertIs(modules[0], mod)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,28 +30,27 @@ class TestMessage(FileTestCase):
 | 
			
		|||
    def make_message(self, **kwargs):
 | 
			
		||||
        return mod.Message(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def test_set_recips(self):
 | 
			
		||||
    def test_get_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'])
 | 
			
		||||
        recips = msg.get_recips(['sally@example.com'])
 | 
			
		||||
        self.assertEqual(recips, ['sally@example.com'])
 | 
			
		||||
 | 
			
		||||
        # set as tuple
 | 
			
		||||
        msg.set_recips('to', ('barney@example.com',))
 | 
			
		||||
        self.assertEqual(msg.to, ['barney@example.com'])
 | 
			
		||||
        recips = msg.get_recips(('barney@example.com',))
 | 
			
		||||
        self.assertEqual(recips, ['barney@example.com'])
 | 
			
		||||
 | 
			
		||||
        # set as string
 | 
			
		||||
        msg.set_recips('to', 'wilma@example.com')
 | 
			
		||||
        self.assertEqual(msg.to, ['wilma@example.com'])
 | 
			
		||||
        recips = msg.get_recips('wilma@example.com')
 | 
			
		||||
        self.assertEqual(recips, ['wilma@example.com'])
 | 
			
		||||
 | 
			
		||||
        # set as null
 | 
			
		||||
        msg.set_recips('to', None)
 | 
			
		||||
        self.assertEqual(msg.to, [])
 | 
			
		||||
        recips = msg.get_recips(None)
 | 
			
		||||
        self.assertEqual(recips, [])
 | 
			
		||||
 | 
			
		||||
        # otherwise error
 | 
			
		||||
        self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'})
 | 
			
		||||
        self.assertRaises(ValueError, msg.get_recips, {'foo': 'foo@example.com'})
 | 
			
		||||
 | 
			
		||||
    def test_as_string(self):
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								tox.ini
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								tox.ini
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -12,8 +12,9 @@ extras = tests
 | 
			
		|||
 | 
			
		||||
[testenv:pylint]
 | 
			
		||||
basepython = python3.11
 | 
			
		||||
extras =
 | 
			
		||||
extras = db
 | 
			
		||||
deps = pylint
 | 
			
		||||
        prompt_toolkit
 | 
			
		||||
commands = pylint wuttjamaican
 | 
			
		||||
 | 
			
		||||
[testenv:coverage]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue