3
0
Fork 0

Compare commits

..

No commits in common. "f34678b30518201360fbeab9753a2bbae2e5dc59" and "a79f80b808394d5fc1981194963b350e815a32ea" have entirely different histories.

24 changed files with 150 additions and 217 deletions

View file

@ -1,14 +1,11 @@
# -*- mode: conf; -*- # -*- mode: conf; -*-
[MESSAGES CONTROL] [MESSAGES CONTROL]
disable= disable=all
attribute-defined-outside-init, enable=anomalous-backslash-in-string,
fixme, dangerous-default-value,
import-outside-toplevel, inconsistent-return-statements,
too-many-arguments, redefined-argument-from-local,
too-many-branches, unspecified-encoding,
too-many-instance-attributes, unused-argument,
too-many-lines, unused-import,
too-many-locals,
too-many-positional-arguments,
too-many-public-methods,

View file

@ -30,7 +30,6 @@ dependencies = [
'importlib-metadata; python_version < "3.10"', 'importlib-metadata; python_version < "3.10"',
"importlib_resources ; python_version < '3.9'", "importlib_resources ; python_version < '3.9'",
"Mako", "Mako",
"packaging",
"progress", "progress",
"python-configuration", "python-configuration",
"typer", "typer",

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
"""
Package Version
"""
from importlib.metadata import version from importlib.metadata import version

View file

@ -929,8 +929,8 @@ class AppHandler:
# registered via entry points # registered via entry points
registered = [] registered = []
for handler in load_entry_points(f'{self.appname}.batch.{key}').values(): for Handler in load_entry_points(f'{self.appname}.batch.{key}').values():
spec = handler.get_spec() spec = Handler.get_spec()
if spec not in handlers: if spec not in handlers:
registered.append(spec) registered.append(spec)
if registered: if registered:
@ -1045,7 +1045,7 @@ class AppHandler:
self.get_email_handler().send_email(*args, **kwargs) self.get_email_handler().send_email(*args, **kwargs)
class AppProvider: # pylint: disable=too-few-public-methods class AppProvider:
""" """
Base class for :term:`app providers<app provider>`. Base class for :term:`app providers<app provider>`.
@ -1123,7 +1123,6 @@ class GenericHandler:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.app = self.config.get_app() self.app = self.config.get_app()
self.modules = {}
@property @property
def appname(self): def appname(self):
@ -1140,35 +1139,3 @@ class GenericHandler:
Returns the class :term:`spec` string for the handler. Returns the class :term:`spec` string for the handler.
""" """
return f'{cls.__module__}:{cls.__name__}' 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]

View file

@ -348,7 +348,7 @@ class BatchHandler(GenericHandler):
* :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_text` * :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_text`
""" """
def why_not_execute(self, batch, user=None, **kwargs): # pylint: disable=unused-argument def why_not_execute(self, batch, user=None, **kwargs):
""" """
Returns text indicating the reason (if any) that a given batch Returns text indicating the reason (if any) that a given batch
should *not* be executed. should *not* be executed.
@ -385,7 +385,6 @@ class BatchHandler(GenericHandler):
raise a ``RuntimeError`` if text was returned. This is done raise a ``RuntimeError`` if text was returned. This is done
out of safety, to avoid relying on the user interface. out of safety, to avoid relying on the user interface.
""" """
return None
def describe_execution(self, batch, user=None, **kwargs): def describe_execution(self, batch, user=None, **kwargs):
""" """
@ -468,16 +467,16 @@ class BatchHandler(GenericHandler):
if batch.executed: if batch.executed:
raise ValueError(f"batch has already been executed: {batch}") raise ValueError(f"batch has already been executed: {batch}")
reason = self.why_not_execute(batch, user=user, **kwargs) # pylint: disable=assignment-from-none reason = self.why_not_execute(batch, user=user, **kwargs)
if reason: if reason:
raise RuntimeError(f"batch execution not allowed: {reason}") raise RuntimeError(f"batch execution not allowed: {reason}")
result = self.execute(batch, user=user, progress=progress, **kwargs) # pylint: disable=assignment-from-none result = self.execute(batch, user=user, progress=progress, **kwargs)
batch.executed = datetime.datetime.now() batch.executed = datetime.datetime.now()
batch.executed_by = user batch.executed_by = user
return result return result
def execute(self, batch, user=None, progress=None, **kwargs): # pylint: disable=unused-argument def execute(self, batch, user=None, progress=None, **kwargs):
""" """
Execute the given batch. Execute the given batch.
@ -503,7 +502,6 @@ class BatchHandler(GenericHandler):
whatever it likes, in which case that will be also returned whatever it likes, in which case that will be also returned
to the caller from :meth:`do_execute()`. 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 def do_delete(self, batch, user, dry_run=False, progress=None, **kwargs): # pylint: disable=unused-argument
""" """

View file

@ -44,7 +44,7 @@ def problems(
help="System for which to perform checks; can be specified more " help="System for which to perform checks; can be specified more "
"than once. If not specified, all systems are assumed.")] = None, "than once. If not specified, all systems are assumed.")] = None,
problems: Annotated[ # pylint: disable=redefined-outer-name problems: Annotated[
List[str], List[str],
typer.Option('--problem', '-p', typer.Option('--problem', '-p',
help="Identify a particular problem check; can be specified " help="Identify a particular problem check; can be specified "
@ -53,8 +53,7 @@ def problems(
list_checks: Annotated[ list_checks: Annotated[
bool, bool,
typer.Option('--list', '-l', typer.Option('--list', '-l',
help="List available problem checks; optionally filtered " help="List available problem checks; optionally filtered per --system and --problem")] = False,
"per --system and --problem")] = False,
): ):
""" """
Find and report on problems with the data or system. Find and report on problems with the data or system.

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# WuttJamaican -- Base package for Wutta Framework # WuttJamaican -- Base package for Wutta Framework
# Copyright © 2024-2025 Lance Edgar # Copyright © 2024 Lance Edgar
# #
# This file is part of Wutta Framework. # This file is part of Wutta Framework.
# #
@ -34,7 +34,7 @@ class DatabaseHandler(GenericHandler):
Base class and default implementation for the :term:`db handler`. Base class and default implementation for the :term:`db handler`.
""" """
def get_dialect(self, bind): # pylint: disable=empty-docstring def get_dialect(self, bind):
""" """ """ """
return bind.url.get_dialect().name return bind.url.get_dialect().name

View file

@ -45,11 +45,10 @@ import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db.util import uuid_column, uuid_fk_column from . import Base, uuid_column, uuid_fk_column
from wuttjamaican.db.model.base import Base
class Role(Base): # pylint: disable=too-few-public-methods class Role(Base):
""" """
Represents an authentication role within the system; used for Represents an authentication role within the system; used for
permission management. permission management.
@ -121,7 +120,7 @@ class Role(Base): # pylint: disable=too-few-public-methods
return self.name or "" return self.name or ""
class Permission(Base): # pylint: disable=too-few-public-methods class Permission(Base):
""" """
Represents a permission granted to a role. Represents a permission granted to a role.
""" """
@ -146,7 +145,7 @@ class Permission(Base): # pylint: disable=too-few-public-methods
return self.permission or "" return self.permission or ""
class User(Base): # pylint: disable=too-few-public-methods class User(Base):
""" """
Represents a user of the system. Represents a user of the system.
@ -232,7 +231,7 @@ class User(Base): # pylint: disable=too-few-public-methods
return self.username or "" return self.username or ""
class UserRole(Base): # pylint: disable=too-few-public-methods class UserRole(Base):
""" """
Represents the association between a user and a role; i.e. the Represents the association between a user and a role; i.e. the
user "belongs" or "is assigned" to the role. user "belongs" or "is assigned" to the role.
@ -261,7 +260,7 @@ class UserRole(Base): # pylint: disable=too-few-public-methods
""") """)
class UserAPIToken(Base): # pylint: disable=too-few-public-methods class UserAPIToken(Base):
""" """
User authentication token for use with HTTP API User authentication token for use with HTTP API
""" """
@ -286,13 +285,9 @@ class UserAPIToken(Base): # pylint: disable=too-few-public-methods
Raw token string, to be used by API clients. Raw token string, to be used by API clients.
""") """)
created = sa.Column( created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
sa.DateTime(timezone=True), Date/time when the token was created.
nullable=False, """)
default=datetime.datetime.now,
doc="""
Date/time when the token was created.
""")
def __str__(self): def __str__(self):
return self.description or "" return self.description or ""

View file

@ -39,7 +39,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
from wuttjamaican.db.util import naming_convention, ModelBase, uuid_column from wuttjamaican.db.util import naming_convention, ModelBase, uuid_column
class WuttaModelBase(ModelBase): # pylint: disable=too-few-public-methods class WuttaModelBase(ModelBase):
""" """
Base class for data models, from which :class:`Base` inherits. 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) Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase)
class Setting(Base): # pylint: disable=too-few-public-methods class Setting(Base):
""" """
Represents a :term:`config setting`. Represents a :term:`config setting`.
""" """

View file

@ -31,8 +31,7 @@ from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.ext.orderinglist import ordering_list
from wuttjamaican.db.model.base import uuid_column from wuttjamaican.db.model import uuid_column, User
from wuttjamaican.db.model.auth import User
from wuttjamaican.db.util import UUID from wuttjamaican.db.util import UUID
@ -186,7 +185,7 @@ class BatchMixin:
""" """
@declared_attr @declared_attr
def __table_args__(cls): # pylint: disable=no-self-argument def __table_args__(cls):
return cls.__default_table_args__() return cls.__default_table_args__()
@classmethod @classmethod
@ -201,8 +200,7 @@ class BatchMixin:
) )
@declared_attr @declared_attr
def batch_type(cls): # pylint: disable=empty-docstring,no-self-argument def batch_type(cls):
""" """
return cls.__tablename__ return cls.__tablename__
uuid = uuid_column() uuid = uuid_column()
@ -228,8 +226,7 @@ class BatchMixin:
created_by_uuid = sa.Column(UUID(), nullable=False) created_by_uuid = sa.Column(UUID(), nullable=False)
@declared_attr @declared_attr
def created_by(cls): # pylint: disable=empty-docstring,no-self-argument def created_by(cls):
""" """
return orm.relationship( return orm.relationship(
User, User,
primaryjoin=lambda: User.uuid == cls.created_by_uuid, primaryjoin=lambda: User.uuid == cls.created_by_uuid,
@ -241,8 +238,7 @@ class BatchMixin:
executed_by_uuid = sa.Column(UUID(), nullable=True) executed_by_uuid = sa.Column(UUID(), nullable=True)
@declared_attr @declared_attr
def executed_by(cls): # pylint: disable=empty-docstring,no-self-argument def executed_by(cls):
""" """
return orm.relationship( return orm.relationship(
User, User,
primaryjoin=lambda: User.uuid == cls.executed_by_uuid, primaryjoin=lambda: User.uuid == cls.executed_by_uuid,
@ -270,7 +266,7 @@ class BatchMixin:
return None return None
class BatchRowMixin: # pylint: disable=too-few-public-methods class BatchRowMixin:
""" """
Mixin base class for :term:`data models <data model>` which Mixin base class for :term:`data models <data model>` which
represent a :term:`batch row`. represent a :term:`batch row`.
@ -381,7 +377,7 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods
uuid = uuid_column() uuid = uuid_column()
@declared_attr @declared_attr
def __table_args__(cls): # pylint: disable=no-self-argument def __table_args__(cls):
return cls.__default_table_args__() return cls.__default_table_args__()
@classmethod @classmethod
@ -398,8 +394,7 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods
batch_uuid = sa.Column(UUID(), nullable=False) batch_uuid = sa.Column(UUID(), nullable=False)
@declared_attr @declared_attr
def batch(cls): # pylint: disable=empty-docstring,no-self-argument def batch(cls):
""" """
batch_class = cls.__batch_class__ batch_class = cls.__batch_class__
row_class = cls row_class = cls
batch_class.__row_class__ = row_class batch_class.__row_class__ = row_class

View file

@ -29,13 +29,13 @@ import datetime
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from . import Base, uuid_column, uuid_fk_column
from wuttjamaican.enum import UpgradeStatus from wuttjamaican.enum import UpgradeStatus
from wuttjamaican.db.util import UUID, uuid_column, uuid_fk_column from wuttjamaican.db.util import UUID
from wuttjamaican.util import make_true_uuid from wuttjamaican.util import make_true_uuid
from wuttjamaican.db.model.base import Base
class Upgrade(Base): # pylint: disable=too-few-public-methods class Upgrade(Base):
""" """
Represents an app upgrade. Represents an app upgrade.
""" """

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# WuttJamaican -- Base package for Wutta Framework # WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-2025 Lance Edgar # Copyright © 2023 Lance Edgar
# #
# This file is part of Wutta Framework. # This file is part of Wutta Framework.
# #
@ -38,7 +38,7 @@ from sqlalchemy import orm
Session = orm.sessionmaker() Session = orm.sessionmaker()
class short_session: # pylint: disable=invalid-name class short_session:
""" """
Context manager for a short-lived database session. Context manager for a short-lived database session.

View file

@ -26,8 +26,8 @@ Database Utilities
import uuid as _uuid import uuid as _uuid
from importlib.metadata import version from importlib.metadata import version
from packaging.version import Version from packaging.version import Version
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.dialects.postgresql import UUID as PGUUID
@ -51,7 +51,7 @@ if Version(version('SQLAlchemy')) < Version('2'): # pragma: no cover
SA2 = False SA2 = False
class ModelBase: # pylint: disable=empty-docstring class ModelBase:
""" """ """ """
def __iter__(self): def __iter__(self):
@ -69,7 +69,7 @@ class ModelBase: # pylint: disable=empty-docstring
raise KeyError(f"model instance has no attr with key: {key}") raise KeyError(f"model instance has no attr with key: {key}")
class UUID(sa.types.TypeDecorator): # pylint: disable=abstract-method,too-many-ancestors class UUID(sa.types.TypeDecorator):
""" """
Platform-independent UUID type. Platform-independent UUID type.
@ -82,35 +82,36 @@ class UUID(sa.types.TypeDecorator): # pylint: disable=abstract-method,too-many-a
""" """
impl = sa.CHAR impl = sa.CHAR
cache_ok = True cache_ok = True
""" """ # nb. suppress sphinx autodoc for cache_ok """ """ # nb. suppress sphinx autodoc for cache_ok
def load_dialect_impl(self, dialect): # pylint: disable=empty-docstring def load_dialect_impl(self, dialect):
""" """ """ """
if dialect.name == "postgresql": if dialect.name == "postgresql":
return dialect.type_descriptor(PGUUID()) return dialect.type_descriptor(PGUUID())
return dialect.type_descriptor(sa.CHAR(32)) else:
return dialect.type_descriptor(sa.CHAR(32))
def process_bind_param(self, value, dialect): # pylint: disable=empty-docstring def process_bind_param(self, value, dialect):
""" """ """ """
if value is None: if value is None:
return value return value
elif dialect.name == "postgresql":
if dialect.name == "postgresql":
return str(value) return str(value)
else:
if not isinstance(value, _uuid.UUID):
return "%.32x" % _uuid.UUID(value).int
else:
# hexstring
return "%.32x" % value.int
if not isinstance(value, _uuid.UUID): def process_result_value(self, value, dialect): # pylint: disable=unused-argument
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: if value is None:
return value return value
if not isinstance(value, _uuid.UUID): else:
value = _uuid.UUID(value) if not isinstance(value, _uuid.UUID):
return value value = _uuid.UUID(value)
return value
def uuid_column(*args, **kwargs): def uuid_column(*args, **kwargs):
@ -149,8 +150,8 @@ def make_topo_sortkey(model):
containing model classes. containing model classes.
""" """
metadata = model.Base.metadata metadata = model.Base.metadata
tables = {table.name: i tables = dict([(table.name, i)
for i, table in enumerate(metadata.sorted_tables, 1)} for i, table in enumerate(metadata.sorted_tables, 1)])
def sortkey(name): def sortkey(name):
cls = getattr(model, name) cls = getattr(model, name)

View file

@ -24,6 +24,7 @@
Email Handler Email Handler
""" """
import importlib
import logging import logging
import smtplib import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
@ -40,7 +41,7 @@ from wuttjamaican.util import resource_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class EmailSetting: # pylint: disable=too-few-public-methods class EmailSetting:
""" """
Base class for all :term:`email settings <email setting>`. Base class for all :term:`email settings <email setting>`.
@ -187,15 +188,15 @@ class Message:
self.key = key self.key = key
self.sender = sender self.sender = sender
self.subject = subject self.subject = subject
self.to = self.get_recips(to) self.set_recips('to', to)
self.cc = self.get_recips(cc) self.set_recips('cc', cc)
self.bcc = self.get_recips(bcc) self.set_recips('bcc', bcc)
self.replyto = replyto self.replyto = replyto
self.txt_body = txt_body self.txt_body = txt_body
self.html_body = html_body self.html_body = html_body
self.attachments = attachments or [] self.attachments = attachments or []
def get_recips(self, value): # pylint: disable=empty-docstring def set_recips(self, name, value):
""" """ """ """
if value: if value:
if isinstance(value, str): if isinstance(value, str):
@ -204,7 +205,7 @@ class Message:
raise ValueError("must specify a string, tuple or list value") raise ValueError("must specify a string, tuple or list value")
else: else:
value = [] value = []
return list(value) setattr(self, name, list(value))
def as_string(self): def as_string(self):
""" """
@ -304,12 +305,20 @@ class EmailHandler(GenericHandler):
This will discover all email modules exposed by the This will discover all email modules exposed by the
:term:`app`, and/or its :term:`providers <provider>`. :term:`app`, and/or its :term:`providers <provider>`.
Calls
:meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()`
under the hood, for ``email`` module type.
""" """
return self.get_provider_modules('email') 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
def get_email_settings(self): def get_email_settings(self):
""" """
@ -540,7 +549,7 @@ class EmailHandler(GenericHandler):
""" """
return self.get_auto_recips(key, 'bcc') return self.get_auto_recips(key, 'bcc')
def get_auto_recips(self, key, typ): # pylint: disable=empty-docstring def get_auto_recips(self, key, typ):
""" """ """ """
typ = typ.lower() typ = typ.lower()
if typ not in ('to', 'cc', 'bcc'): if typ not in ('to', 'cc', 'bcc'):
@ -580,15 +589,16 @@ class EmailHandler(GenericHandler):
return template.render(**context) return template.render(**context)
return None return None
def get_auto_body_template(self, key, mode): # pylint: disable=empty-docstring def get_auto_body_template(self, key, mode):
""" """ """ """
mode = mode.lower() mode = mode.lower()
if mode not in ('txt', 'html'):
raise ValueError("requested mode not supported")
if mode == 'txt': if mode == 'txt':
templates = self.txt_templates templates = self.txt_templates
elif mode == 'html': elif mode == 'html':
templates = self.html_templates templates = self.html_templates
else:
raise ValueError("requested mode not supported")
try: try:
return templates.get_template(f'{key}.{mode}.mako') return templates.get_template(f'{key}.{mode}.mako')

View file

@ -141,10 +141,9 @@ class InstallHandler(GenericHandler):
This is normally called by :meth:`run()`. This is normally called by :meth:`run()`.
""" """
self.rprint(f"\n\t[blue]Welcome to {self.app.get_title()}![/blue]") self.rprint("\n\t[blue]Welcome to {}![/blue]".format(self.app.get_title()))
self.rprint("\n\tThis tool will install and configure the app.") self.rprint("\n\tThis tool will install and configure the app.")
self.rprint("\n\t[italic]NB. You should already have created " self.rprint("\n\t[italic]NB. You should already have created the database in PostgreSQL or MySQL.[/italic]")
"the database in PostgreSQL or MySQL.[/italic]")
# shall we continue? # shall we continue?
if not self.prompt_bool("continue?", True): if not self.prompt_bool("continue?", True):
@ -222,15 +221,14 @@ class InstallHandler(GenericHandler):
error = self.test_db_connection(dbinfo['dburl']) error = self.test_db_connection(dbinfo['dburl'])
if error: if error:
self.rprint("[bold red]cannot connect![/bold red] ..error was:") self.rprint("[bold red]cannot connect![/bold red] ..error was:")
self.rprint(f"\n{error}") self.rprint("\n{}".format(error))
self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n") self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n")
sys.exit(1) sys.exit(1)
self.rprint("[bold green]good[/bold green]") self.rprint("[bold green]good[/bold green]")
return dbinfo return dbinfo
def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass): # pylint: disable=empty-docstring def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass):
""" """
from sqlalchemy.engine import URL from sqlalchemy.engine import URL
if dbtype == 'mysql': if dbtype == 'mysql':
@ -245,8 +243,7 @@ class InstallHandler(GenericHandler):
port=dbport, port=dbport,
database=dbname) database=dbname)
def test_db_connection(self, url): # pylint: disable=empty-docstring def test_db_connection(self, url):
""" """
import sqlalchemy as sa import sqlalchemy as sa
engine = sa.create_engine(url) engine = sa.create_engine(url)
@ -255,7 +252,7 @@ class InstallHandler(GenericHandler):
# just need to test interaction and this is a neutral way # just need to test interaction and this is a neutral way
try: try:
sa.inspect(engine).has_table('whatever') sa.inspect(engine).has_table('whatever')
except Exception as error: # pylint: disable=broad-exception-caught except Exception as error:
return str(error) return str(error)
return None return None
@ -449,8 +446,8 @@ class InstallHandler(GenericHandler):
'upgrade', 'heads'] 'upgrade', 'heads']
subprocess.check_call(cmd) subprocess.check_call(cmd)
self.rprint("\n\tdb schema installed to: " self.rprint("\n\tdb schema installed to: [bold green]{}[/bold green]".format(
f"[bold green]{obfuscate_url_pw(db_url)}[/bold green]") obfuscate_url_pw(db_url)))
return True return True
def show_goodbye(self): def show_goodbye(self):
@ -472,8 +469,7 @@ class InstallHandler(GenericHandler):
# console utility functions # console utility functions
############################## ##############################
def require_prompt_toolkit(self, answer=None): # pylint: disable=empty-docstring def require_prompt_toolkit(self, answer=None):
""" """
try: try:
import prompt_toolkit # pylint: disable=unused-import import prompt_toolkit # pylint: disable=unused-import
except ImportError: except ImportError:
@ -495,8 +491,7 @@ class InstallHandler(GenericHandler):
""" """
rich.print(*args, **kwargs) rich.print(*args, **kwargs)
def get_prompt_style(self): # pylint: disable=empty-docstring def get_prompt_style(self):
""" """
from prompt_toolkit.styles import Style from prompt_toolkit.styles import Style
# message formatting styles # message formatting styles
@ -545,9 +540,9 @@ class InstallHandler(GenericHandler):
] ]
if default is not None: if default is not None:
if is_bool: if is_bool:
message.append(('', f' [{"Y" if default else "N"}]: ')) message.append(('', ' [{}]: '.format('Y' if default else 'N')))
else: else:
message.append(('', f' [{default}]: ')) message.append(('', ' [{}]: '.format(default)))
else: else:
message.append(('', ': ')) message.append(('', ': '))
@ -563,9 +558,9 @@ class InstallHandler(GenericHandler):
if is_bool: if is_bool:
if text == '': if text == '':
return default return default
if text.upper() == 'Y': elif text.upper() == 'Y':
return True return True
if text.upper() == 'N': elif text.upper() == 'N':
return False return False
self.rprint("\n\t[bold yellow]ambiguous, please try again[/bold yellow]\n") self.rprint("\n\t[bold yellow]ambiguous, please try again[/bold yellow]\n")
return self.prompt_generic(info, default, is_bool=True) return self.prompt_generic(info, default, is_bool=True)

View file

@ -84,7 +84,7 @@ class PeopleHandler(GenericHandler):
person = obj person = obj
return person return person
if isinstance(obj, model.User): elif isinstance(obj, model.User):
user = obj user = obj
if user.person: if user.person:
return user.person return user.person

View file

@ -200,7 +200,7 @@ class ProblemHandler(GenericHandler):
:returns: List of system keys. :returns: List of system keys.
""" """
checks = self.get_all_problem_checks() checks = self.get_all_problem_checks()
return sorted({check.system_key for check in checks}) return sorted(set([check.system_key for check in checks]))
def get_system_title(self, system_key): def get_system_title(self, system_key):
""" """

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# WuttJamaican -- Base package for Wutta Framework # WuttJamaican -- Base package for Wutta Framework
# Copyright © 2023-2025 Lance Edgar # Copyright © 2023-2024 Lance Edgar
# #
# This file is part of Wutta Framework. # This file is part of Wutta Framework.
# #
@ -97,17 +97,17 @@ class ConsoleProgress(ProgressBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args) super().__init__(*args)
self.stderr = kwargs.get('stderr', sys.stderr) self.stderr = kwargs.get('stderr', sys.stderr)
self.stderr.write(f"\n{self.message}...\n") self.stderr.write(f"\n{self.message}...\n")
self.bar = Bar(message='', max=self.maximum, width=70, # pylint: disable=disallowed-name self.bar = Bar(message='', max=self.maximum, width=70,
suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds') suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds')
def update(self, value): # pylint: disable=empty-docstring def update(self, value):
""" """ """ """
self.bar.next() self.bar.next()
def finish(self): # pylint: disable=empty-docstring def finish(self):
""" """ """ """
self.bar.finish() self.bar.finish()

View file

@ -24,6 +24,8 @@
Report Utilities Report Utilities
""" """
import importlib
from wuttjamaican.app import GenericHandler from wuttjamaican.app import GenericHandler
@ -141,12 +143,20 @@ class ReportHandler(GenericHandler):
This will discover all report modules exposed by the This will discover all report modules exposed by the
:term:`app`, and/or its :term:`providers <provider>`. :term:`app`, and/or its :term:`providers <provider>`.
Calls
:meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()`
under the hood, for ``report`` module type.
""" """
return self.get_provider_modules('report') 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
def get_reports(self): def get_reports(self):
""" """

View file

@ -53,7 +53,7 @@ class FileTestCase(TestCase):
class. class.
""" """
def setUp(self): # pylint: disable=empty-docstring def setUp(self):
""" """ """ """
self.setup_files() self.setup_files()
@ -63,14 +63,14 @@ class FileTestCase(TestCase):
""" """
self.tempdir = tempfile.mkdtemp() self.tempdir = tempfile.mkdtemp()
def setup_file_config(self): # pragma: no cover; pylint: disable=empty-docstring def setup_file_config(self): # pragma: no cover
""" """ """ """
warnings.warn("FileTestCase.setup_file_config() is deprecated; " warnings.warn("FileTestCase.setup_file_config() is deprecated; "
"please use setup_files() instead", "please use setup_files() instead",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
self.setup_files() self.setup_files()
def tearDown(self): # pylint: disable=empty-docstring def tearDown(self):
""" """ """ """
self.teardown_files() self.teardown_files()
@ -80,7 +80,7 @@ class FileTestCase(TestCase):
""" """
shutil.rmtree(self.tempdir) shutil.rmtree(self.tempdir)
def teardown_file_config(self): # pragma: no cover; pylint: disable=empty-docstring def teardown_file_config(self): # pragma: no cover
""" """ """ """
warnings.warn("FileTestCase.teardown_file_config() is deprecated; " warnings.warn("FileTestCase.teardown_file_config() is deprecated; "
"please use teardown_files() instead", "please use teardown_files() instead",
@ -99,7 +99,7 @@ class FileTestCase(TestCase):
f.write(content) f.write(content)
return path return path
def mkdir(self, dirname): # pylint: disable=unused-argument,empty-docstring def mkdir(self, dirname): # pylint: disable=unused-argument
""" """ """ """
warnings.warn("FileTestCase.mkdir() is deprecated; " warnings.warn("FileTestCase.mkdir() is deprecated; "
"please use FileTestCase.mkdtemp() instead", "please use FileTestCase.mkdtemp() instead",
@ -143,7 +143,7 @@ class ConfigTestCase(FileTestCase):
methods for this class. methods for this class.
""" """
def setUp(self): # pylint: disable=empty-docstring def setUp(self):
""" """ """ """
self.setup_config() self.setup_config()
@ -155,7 +155,7 @@ class ConfigTestCase(FileTestCase):
self.config = self.make_config() self.config = self.make_config()
self.app = self.config.get_app() self.app = self.config.get_app()
def tearDown(self): # pylint: disable=empty-docstring def tearDown(self):
""" """ """ """
self.teardown_config() self.teardown_config()
@ -165,7 +165,7 @@ class ConfigTestCase(FileTestCase):
""" """
self.teardown_files() self.teardown_files()
def make_config(self, **kwargs): # pylint: disable=empty-docstring def make_config(self, **kwargs):
""" """ """ """
return WuttaConfig(**kwargs) return WuttaConfig(**kwargs)
@ -203,7 +203,7 @@ class DataTestCase(FileTestCase):
teardown methods, as this class handles that automatically. teardown methods, as this class handles that automatically.
""" """
def setUp(self): # pylint: disable=empty-docstring def setUp(self):
""" """ """ """
self.setup_db() self.setup_db()
@ -222,7 +222,7 @@ class DataTestCase(FileTestCase):
model.Base.metadata.create_all(bind=self.config.appdb_engine) model.Base.metadata.create_all(bind=self.config.appdb_engine)
self.session = self.app.make_session() self.session = self.app.make_session()
def tearDown(self): # pylint: disable=empty-docstring def tearDown(self):
""" """ """ """
self.teardown_db() self.teardown_db()
@ -232,6 +232,6 @@ class DataTestCase(FileTestCase):
""" """
self.teardown_files() self.teardown_files()
def make_config(self, **kwargs): # pylint: disable=empty-docstring def make_config(self, **kwargs):
""" """ """ """
return WuttaConfig(**kwargs) return WuttaConfig(**kwargs)

View file

@ -115,7 +115,7 @@ def load_entry_points(group, ignore_errors=False):
for entry_point in eps: for entry_point in eps:
try: try:
ep = entry_point.load() ep = entry_point.load()
except Exception: # pylint: disable=broad-exception-caught except:
if not ignore_errors: if not ignore_errors:
raise raise
log.warning("failed to load entry point: %s", entry_point, log.warning("failed to load entry point: %s", entry_point,

View file

@ -761,9 +761,6 @@ class TestGenericHandler(ConfigTestCase):
kw.setdefault('appname', 'wuttatest') kw.setdefault('appname', 'wuttatest')
return super().make_config(**kw) return super().make_config(**kw)
def make_handler(self, **kwargs):
return mod.GenericHandler(self.config, **kwargs)
def test_constructor(self): def test_constructor(self):
handler = mod.GenericHandler(self.config) handler = mod.GenericHandler(self.config)
self.assertIs(handler.config, self.config) self.assertIs(handler.config, self.config)
@ -772,30 +769,3 @@ class TestGenericHandler(ConfigTestCase):
def test_get_spec(self): def test_get_spec(self):
self.assertEqual(mod.GenericHandler.get_spec(), 'wuttjamaican.app:GenericHandler') 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)

View file

@ -30,27 +30,28 @@ class TestMessage(FileTestCase):
def make_message(self, **kwargs): def make_message(self, **kwargs):
return mod.Message(**kwargs) return mod.Message(**kwargs)
def test_get_recips(self): def test_set_recips(self):
msg = self.make_message() msg = self.make_message()
self.assertEqual(msg.to, [])
# set as list # set as list
recips = msg.get_recips(['sally@example.com']) msg.set_recips('to', ['sally@example.com'])
self.assertEqual(recips, ['sally@example.com']) self.assertEqual(msg.to, ['sally@example.com'])
# set as tuple # set as tuple
recips = msg.get_recips(('barney@example.com',)) msg.set_recips('to', ('barney@example.com',))
self.assertEqual(recips, ['barney@example.com']) self.assertEqual(msg.to, ['barney@example.com'])
# set as string # set as string
recips = msg.get_recips('wilma@example.com') msg.set_recips('to', 'wilma@example.com')
self.assertEqual(recips, ['wilma@example.com']) self.assertEqual(msg.to, ['wilma@example.com'])
# set as null # set as null
recips = msg.get_recips(None) msg.set_recips('to', None)
self.assertEqual(recips, []) self.assertEqual(msg.to, [])
# otherwise error # otherwise error
self.assertRaises(ValueError, msg.get_recips, {'foo': 'foo@example.com'}) self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'})
def test_as_string(self): def test_as_string(self):

View file

@ -12,9 +12,8 @@ extras = tests
[testenv:pylint] [testenv:pylint]
basepython = python3.11 basepython = python3.11
extras = db extras =
deps = pylint deps = pylint
prompt_toolkit
commands = pylint wuttjamaican commands = pylint wuttjamaican
[testenv:coverage] [testenv:coverage]