Compare commits
No commits in common. "f34678b30518201360fbeab9753a2bbae2e5dc59" and "a79f80b808394d5fc1981194963b350e815a32ea" have entirely different histories.
f34678b305
...
a79f80b808
24 changed files with 150 additions and 217 deletions
19
.pylintrc
19
.pylintrc
|
@ -1,14 +1,11 @@
|
|||
# -*- mode: conf; -*-
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
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,
|
||||
disable=all
|
||||
enable=anomalous-backslash-in-string,
|
||||
dangerous-default-value,
|
||||
inconsistent-return-statements,
|
||||
redefined-argument-from-local,
|
||||
unspecified-encoding,
|
||||
unused-argument,
|
||||
unused-import,
|
||||
|
|
|
@ -30,7 +30,6 @@ dependencies = [
|
|||
'importlib-metadata; python_version < "3.10"',
|
||||
"importlib_resources ; python_version < '3.9'",
|
||||
"Mako",
|
||||
"packaging",
|
||||
"progress",
|
||||
"python-configuration",
|
||||
"typer",
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- 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: # pylint: disable=too-few-public-methods
|
||||
class AppProvider:
|
||||
"""
|
||||
Base class for :term:`app providers<app provider>`.
|
||||
|
||||
|
@ -1123,7 +1123,6 @@ class GenericHandler:
|
|||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.app = self.config.get_app()
|
||||
self.modules = {}
|
||||
|
||||
@property
|
||||
def appname(self):
|
||||
|
@ -1140,35 +1139,3 @@ 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): # pylint: disable=unused-argument
|
||||
def why_not_execute(self, batch, user=None, **kwargs):
|
||||
"""
|
||||
Returns text indicating the reason (if any) that a given batch
|
||||
should *not* be executed.
|
||||
|
@ -385,7 +385,6 @@ 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):
|
||||
"""
|
||||
|
@ -468,16 +467,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) # pylint: disable=assignment-from-none
|
||||
reason = self.why_not_execute(batch, user=user, **kwargs)
|
||||
if 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_by = user
|
||||
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.
|
||||
|
||||
|
@ -503,7 +502,6 @@ 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[ # pylint: disable=redefined-outer-name
|
||||
problems: Annotated[
|
||||
List[str],
|
||||
typer.Option('--problem', '-p',
|
||||
help="Identify a particular problem check; can be specified "
|
||||
|
@ -53,8 +53,7 @@ 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-2025 Lance Edgar
|
||||
# Copyright © 2024 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): # pylint: disable=empty-docstring
|
||||
def get_dialect(self, bind):
|
||||
""" """
|
||||
return bind.url.get_dialect().name
|
||||
|
||||
|
|
|
@ -45,11 +45,10 @@ import sqlalchemy as sa
|
|||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from wuttjamaican.db.util import uuid_column, uuid_fk_column
|
||||
from wuttjamaican.db.model.base import Base
|
||||
from . import Base, uuid_column, uuid_fk_column
|
||||
|
||||
|
||||
class Role(Base): # pylint: disable=too-few-public-methods
|
||||
class Role(Base):
|
||||
"""
|
||||
Represents an authentication role within the system; used for
|
||||
permission management.
|
||||
|
@ -121,7 +120,7 @@ class Role(Base): # pylint: disable=too-few-public-methods
|
|||
return self.name or ""
|
||||
|
||||
|
||||
class Permission(Base): # pylint: disable=too-few-public-methods
|
||||
class Permission(Base):
|
||||
"""
|
||||
Represents a permission granted to a role.
|
||||
"""
|
||||
|
@ -146,7 +145,7 @@ class Permission(Base): # pylint: disable=too-few-public-methods
|
|||
return self.permission or ""
|
||||
|
||||
|
||||
class User(Base): # pylint: disable=too-few-public-methods
|
||||
class User(Base):
|
||||
"""
|
||||
Represents a user of the system.
|
||||
|
||||
|
@ -232,7 +231,7 @@ class User(Base): # pylint: disable=too-few-public-methods
|
|||
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
|
||||
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
|
||||
"""
|
||||
|
@ -286,11 +285,7 @@ class UserAPIToken(Base): # pylint: disable=too-few-public-methods
|
|||
Raw token string, to be used by API clients.
|
||||
""")
|
||||
|
||||
created = sa.Column(
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=datetime.datetime.now,
|
||||
doc="""
|
||||
created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc="""
|
||||
Date/time when the token was created.
|
||||
""")
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ from sqlalchemy.ext.associationproxy import association_proxy
|
|||
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.
|
||||
|
||||
|
@ -123,7 +123,7 @@ metadata = sa.MetaData(naming_convention=naming_convention)
|
|||
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`.
|
||||
"""
|
||||
|
|
|
@ -31,8 +31,7 @@ from sqlalchemy import orm
|
|||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.ext.orderinglist import ordering_list
|
||||
|
||||
from wuttjamaican.db.model.base import uuid_column
|
||||
from wuttjamaican.db.model.auth import User
|
||||
from wuttjamaican.db.model import uuid_column, User
|
||||
from wuttjamaican.db.util import UUID
|
||||
|
||||
|
||||
|
@ -186,7 +185,7 @@ class BatchMixin:
|
|||
"""
|
||||
|
||||
@declared_attr
|
||||
def __table_args__(cls): # pylint: disable=no-self-argument
|
||||
def __table_args__(cls):
|
||||
return cls.__default_table_args__()
|
||||
|
||||
@classmethod
|
||||
|
@ -201,8 +200,7 @@ class BatchMixin:
|
|||
)
|
||||
|
||||
@declared_attr
|
||||
def batch_type(cls): # pylint: disable=empty-docstring,no-self-argument
|
||||
""" """
|
||||
def batch_type(cls):
|
||||
return cls.__tablename__
|
||||
|
||||
uuid = uuid_column()
|
||||
|
@ -228,8 +226,7 @@ class BatchMixin:
|
|||
created_by_uuid = sa.Column(UUID(), nullable=False)
|
||||
|
||||
@declared_attr
|
||||
def created_by(cls): # pylint: disable=empty-docstring,no-self-argument
|
||||
""" """
|
||||
def created_by(cls):
|
||||
return orm.relationship(
|
||||
User,
|
||||
primaryjoin=lambda: User.uuid == cls.created_by_uuid,
|
||||
|
@ -241,8 +238,7 @@ class BatchMixin:
|
|||
executed_by_uuid = sa.Column(UUID(), nullable=True)
|
||||
|
||||
@declared_attr
|
||||
def executed_by(cls): # pylint: disable=empty-docstring,no-self-argument
|
||||
""" """
|
||||
def executed_by(cls):
|
||||
return orm.relationship(
|
||||
User,
|
||||
primaryjoin=lambda: User.uuid == cls.executed_by_uuid,
|
||||
|
@ -270,7 +266,7 @@ class BatchMixin:
|
|||
return None
|
||||
|
||||
|
||||
class BatchRowMixin: # pylint: disable=too-few-public-methods
|
||||
class BatchRowMixin:
|
||||
"""
|
||||
Mixin base class for :term:`data models <data model>` which
|
||||
represent a :term:`batch row`.
|
||||
|
@ -381,7 +377,7 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods
|
|||
uuid = uuid_column()
|
||||
|
||||
@declared_attr
|
||||
def __table_args__(cls): # pylint: disable=no-self-argument
|
||||
def __table_args__(cls):
|
||||
return cls.__default_table_args__()
|
||||
|
||||
@classmethod
|
||||
|
@ -398,8 +394,7 @@ class BatchRowMixin: # pylint: disable=too-few-public-methods
|
|||
batch_uuid = sa.Column(UUID(), nullable=False)
|
||||
|
||||
@declared_attr
|
||||
def batch(cls): # pylint: disable=empty-docstring,no-self-argument
|
||||
""" """
|
||||
def batch(cls):
|
||||
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, uuid_column, uuid_fk_column
|
||||
from wuttjamaican.db.util import 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.
|
||||
"""
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# WuttJamaican -- Base package for Wutta Framework
|
||||
# Copyright © 2023-2025 Lance Edgar
|
||||
# Copyright © 2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
|
@ -38,7 +38,7 @@ from sqlalchemy import orm
|
|||
Session = orm.sessionmaker()
|
||||
|
||||
|
||||
class short_session: # pylint: disable=invalid-name
|
||||
class short_session:
|
||||
"""
|
||||
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
|
||||
|
||||
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: # pylint: disable=empty-docstring
|
||||
class ModelBase:
|
||||
""" """
|
||||
|
||||
def __iter__(self):
|
||||
|
@ -69,7 +69,7 @@ class ModelBase: # pylint: disable=empty-docstring
|
|||
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.
|
||||
|
||||
|
@ -84,30 +84,31 @@ class UUID(sa.types.TypeDecorator): # pylint: disable=abstract-method,too-many-a
|
|||
cache_ok = True
|
||||
""" """ # 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":
|
||||
return dialect.type_descriptor(PGUUID())
|
||||
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:
|
||||
return value
|
||||
|
||||
if dialect.name == "postgresql":
|
||||
elif dialect.name == "postgresql":
|
||||
return str(value)
|
||||
|
||||
else:
|
||||
if not isinstance(value, _uuid.UUID):
|
||||
value = _uuid.UUID(value)
|
||||
|
||||
return "%.32x" % _uuid.UUID(value).int
|
||||
else:
|
||||
# hexstring
|
||||
return f"{value.int:032x}"
|
||||
return "%.32x" % value.int
|
||||
|
||||
def process_result_value(self, value, dialect): # pylint: disable=unused-argument,empty-docstring
|
||||
def process_result_value(self, value, dialect): # pylint: disable=unused-argument
|
||||
""" """
|
||||
if value is None:
|
||||
return value
|
||||
else:
|
||||
if not isinstance(value, _uuid.UUID):
|
||||
value = _uuid.UUID(value)
|
||||
return value
|
||||
|
@ -149,8 +150,8 @@ def make_topo_sortkey(model):
|
|||
containing model classes.
|
||||
"""
|
||||
metadata = model.Base.metadata
|
||||
tables = {table.name: i
|
||||
for i, table in enumerate(metadata.sorted_tables, 1)}
|
||||
tables = dict([(table.name, i)
|
||||
for i, table in enumerate(metadata.sorted_tables, 1)])
|
||||
|
||||
def sortkey(name):
|
||||
cls = getattr(model, name)
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
Email Handler
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
@ -40,7 +41,7 @@ from wuttjamaican.util import resource_path
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailSetting: # pylint: disable=too-few-public-methods
|
||||
class EmailSetting:
|
||||
"""
|
||||
Base class for all :term:`email settings <email setting>`.
|
||||
|
||||
|
@ -187,15 +188,15 @@ class Message:
|
|||
self.key = key
|
||||
self.sender = sender
|
||||
self.subject = subject
|
||||
self.to = self.get_recips(to)
|
||||
self.cc = self.get_recips(cc)
|
||||
self.bcc = self.get_recips(bcc)
|
||||
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
|
||||
self.attachments = attachments or []
|
||||
|
||||
def get_recips(self, value): # pylint: disable=empty-docstring
|
||||
def set_recips(self, name, value):
|
||||
""" """
|
||||
if value:
|
||||
if isinstance(value, str):
|
||||
|
@ -204,7 +205,7 @@ class Message:
|
|||
raise ValueError("must specify a string, tuple or list value")
|
||||
else:
|
||||
value = []
|
||||
return list(value)
|
||||
setattr(self, name, list(value))
|
||||
|
||||
def as_string(self):
|
||||
"""
|
||||
|
@ -304,12 +305,20 @@ class EmailHandler(GenericHandler):
|
|||
|
||||
This will discover all email modules exposed by the
|
||||
: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):
|
||||
"""
|
||||
|
@ -540,7 +549,7 @@ class EmailHandler(GenericHandler):
|
|||
"""
|
||||
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()
|
||||
if typ not in ('to', 'cc', 'bcc'):
|
||||
|
@ -580,15 +589,16 @@ class EmailHandler(GenericHandler):
|
|||
return template.render(**context)
|
||||
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()
|
||||
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,10 +141,9 @@ class InstallHandler(GenericHandler):
|
|||
|
||||
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\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):
|
||||
|
@ -222,15 +221,14 @@ class InstallHandler(GenericHandler):
|
|||
error = self.test_db_connection(dbinfo['dburl'])
|
||||
if error:
|
||||
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")
|
||||
sys.exit(1)
|
||||
self.rprint("[bold green]good[/bold green]")
|
||||
|
||||
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
|
||||
|
||||
if dbtype == 'mysql':
|
||||
|
@ -245,8 +243,7 @@ class InstallHandler(GenericHandler):
|
|||
port=dbport,
|
||||
database=dbname)
|
||||
|
||||
def test_db_connection(self, url): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
def test_db_connection(self, url):
|
||||
import sqlalchemy as sa
|
||||
|
||||
engine = sa.create_engine(url)
|
||||
|
@ -255,7 +252,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: # pylint: disable=broad-exception-caught
|
||||
except Exception as error:
|
||||
return str(error)
|
||||
return None
|
||||
|
||||
|
@ -449,8 +446,8 @@ class InstallHandler(GenericHandler):
|
|||
'upgrade', 'heads']
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
self.rprint("\n\tdb schema installed to: "
|
||||
f"[bold green]{obfuscate_url_pw(db_url)}[/bold green]")
|
||||
self.rprint("\n\tdb schema installed to: [bold green]{}[/bold green]".format(
|
||||
obfuscate_url_pw(db_url)))
|
||||
return True
|
||||
|
||||
def show_goodbye(self):
|
||||
|
@ -472,8 +469,7 @@ class InstallHandler(GenericHandler):
|
|||
# console utility functions
|
||||
##############################
|
||||
|
||||
def require_prompt_toolkit(self, answer=None): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
def require_prompt_toolkit(self, answer=None):
|
||||
try:
|
||||
import prompt_toolkit # pylint: disable=unused-import
|
||||
except ImportError:
|
||||
|
@ -495,8 +491,7 @@ class InstallHandler(GenericHandler):
|
|||
"""
|
||||
rich.print(*args, **kwargs)
|
||||
|
||||
def get_prompt_style(self): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
def get_prompt_style(self):
|
||||
from prompt_toolkit.styles import Style
|
||||
|
||||
# message formatting styles
|
||||
|
@ -545,9 +540,9 @@ class InstallHandler(GenericHandler):
|
|||
]
|
||||
if default is not None:
|
||||
if is_bool:
|
||||
message.append(('', f' [{"Y" if default else "N"}]: '))
|
||||
message.append(('', ' [{}]: '.format('Y' if default else 'N')))
|
||||
else:
|
||||
message.append(('', f' [{default}]: '))
|
||||
message.append(('', ' [{}]: '.format(default)))
|
||||
else:
|
||||
message.append(('', ': '))
|
||||
|
||||
|
@ -563,9 +558,9 @@ class InstallHandler(GenericHandler):
|
|||
if is_bool:
|
||||
if text == '':
|
||||
return default
|
||||
if text.upper() == 'Y':
|
||||
elif text.upper() == 'Y':
|
||||
return True
|
||||
if text.upper() == 'N':
|
||||
elif 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
|
||||
|
||||
if isinstance(obj, model.User):
|
||||
elif 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({check.system_key for check in checks})
|
||||
return sorted(set([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-2025 Lance Edgar
|
||||
# Copyright © 2023-2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Wutta Framework.
|
||||
#
|
||||
|
@ -101,13 +101,13 @@ class ConsoleProgress(ProgressBase):
|
|||
self.stderr = kwargs.get('stderr', sys.stderr)
|
||||
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')
|
||||
|
||||
def update(self, value): # pylint: disable=empty-docstring
|
||||
def update(self, value):
|
||||
""" """
|
||||
self.bar.next()
|
||||
|
||||
def finish(self): # pylint: disable=empty-docstring
|
||||
def finish(self):
|
||||
""" """
|
||||
self.bar.finish()
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
Report Utilities
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
from wuttjamaican.app import GenericHandler
|
||||
|
||||
|
||||
|
@ -141,12 +143,20 @@ class ReportHandler(GenericHandler):
|
|||
|
||||
This will discover all report modules exposed by the
|
||||
: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):
|
||||
"""
|
||||
|
|
|
@ -53,7 +53,7 @@ class FileTestCase(TestCase):
|
|||
class.
|
||||
"""
|
||||
|
||||
def setUp(self): # pylint: disable=empty-docstring
|
||||
def setUp(self):
|
||||
""" """
|
||||
self.setup_files()
|
||||
|
||||
|
@ -63,14 +63,14 @@ class FileTestCase(TestCase):
|
|||
"""
|
||||
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; "
|
||||
"please use setup_files() instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
self.setup_files()
|
||||
|
||||
def tearDown(self): # pylint: disable=empty-docstring
|
||||
def tearDown(self):
|
||||
""" """
|
||||
self.teardown_files()
|
||||
|
||||
|
@ -80,7 +80,7 @@ class FileTestCase(TestCase):
|
|||
"""
|
||||
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; "
|
||||
"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,empty-docstring
|
||||
def mkdir(self, dirname): # pylint: disable=unused-argument
|
||||
""" """
|
||||
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): # pylint: disable=empty-docstring
|
||||
def setUp(self):
|
||||
""" """
|
||||
self.setup_config()
|
||||
|
||||
|
@ -155,7 +155,7 @@ class ConfigTestCase(FileTestCase):
|
|||
self.config = self.make_config()
|
||||
self.app = self.config.get_app()
|
||||
|
||||
def tearDown(self): # pylint: disable=empty-docstring
|
||||
def tearDown(self):
|
||||
""" """
|
||||
self.teardown_config()
|
||||
|
||||
|
@ -165,7 +165,7 @@ class ConfigTestCase(FileTestCase):
|
|||
"""
|
||||
self.teardown_files()
|
||||
|
||||
def make_config(self, **kwargs): # pylint: disable=empty-docstring
|
||||
def make_config(self, **kwargs):
|
||||
""" """
|
||||
return WuttaConfig(**kwargs)
|
||||
|
||||
|
@ -203,7 +203,7 @@ class DataTestCase(FileTestCase):
|
|||
teardown methods, as this class handles that automatically.
|
||||
"""
|
||||
|
||||
def setUp(self): # pylint: disable=empty-docstring
|
||||
def setUp(self):
|
||||
""" """
|
||||
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): # pylint: disable=empty-docstring
|
||||
def tearDown(self):
|
||||
""" """
|
||||
self.teardown_db()
|
||||
|
||||
|
@ -232,6 +232,6 @@ class DataTestCase(FileTestCase):
|
|||
"""
|
||||
self.teardown_files()
|
||||
|
||||
def make_config(self, **kwargs): # pylint: disable=empty-docstring
|
||||
def make_config(self, **kwargs):
|
||||
""" """
|
||||
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 Exception: # pylint: disable=broad-exception-caught
|
||||
except:
|
||||
if not ignore_errors:
|
||||
raise
|
||||
log.warning("failed to load entry point: %s", entry_point,
|
||||
|
|
|
@ -761,9 +761,6 @@ 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)
|
||||
|
@ -772,30 +769,3 @@ 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,27 +30,28 @@ class TestMessage(FileTestCase):
|
|||
def make_message(self, **kwargs):
|
||||
return mod.Message(**kwargs)
|
||||
|
||||
def test_get_recips(self):
|
||||
def test_set_recips(self):
|
||||
msg = self.make_message()
|
||||
self.assertEqual(msg.to, [])
|
||||
|
||||
# set as list
|
||||
recips = msg.get_recips(['sally@example.com'])
|
||||
self.assertEqual(recips, ['sally@example.com'])
|
||||
msg.set_recips('to', ['sally@example.com'])
|
||||
self.assertEqual(msg.to, ['sally@example.com'])
|
||||
|
||||
# set as tuple
|
||||
recips = msg.get_recips(('barney@example.com',))
|
||||
self.assertEqual(recips, ['barney@example.com'])
|
||||
msg.set_recips('to', ('barney@example.com',))
|
||||
self.assertEqual(msg.to, ['barney@example.com'])
|
||||
|
||||
# set as string
|
||||
recips = msg.get_recips('wilma@example.com')
|
||||
self.assertEqual(recips, ['wilma@example.com'])
|
||||
msg.set_recips('to', 'wilma@example.com')
|
||||
self.assertEqual(msg.to, ['wilma@example.com'])
|
||||
|
||||
# set as null
|
||||
recips = msg.get_recips(None)
|
||||
self.assertEqual(recips, [])
|
||||
msg.set_recips('to', None)
|
||||
self.assertEqual(msg.to, [])
|
||||
|
||||
# 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):
|
||||
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -12,9 +12,8 @@ extras = tests
|
|||
|
||||
[testenv:pylint]
|
||||
basepython = python3.11
|
||||
extras = db
|
||||
extras =
|
||||
deps = pylint
|
||||
prompt_toolkit
|
||||
commands = pylint wuttjamaican
|
||||
|
||||
[testenv:coverage]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue