Compare commits

..

7 Commits

19 changed files with 858 additions and 453 deletions

11
.flake8 Normal file

@ -0,0 +1,11 @@
[flake8]
max-line-length=120
ignore =
E121,
E123,
E126,
E226,
E24,
E704,
W605
exclude = ./tests

1
.gitignore vendored

@ -2,4 +2,5 @@ build
dist
*.egg-info
__pycache__
.idea

37
.pre-commit-config.yaml Normal file

@ -0,0 +1,37 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=10240']
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
# isort -- sorts imports
- repo: https://github.com/timothycrosley/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", "--filter-files"]
# Flake8
#- repo: https://github.com/pycqa/flake8
# rev: '7.0.0'
# hooks:
# - id: flake8
# Black
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.2.0
hooks:
- id: black
language_version: python3.8
# Poetry
- repo: https://github.com/python-poetry/poetry
rev: 1.8.2
hooks:
- id: poetry-lock

1
.python-version Normal file

@ -0,0 +1 @@
3.8.19

0
CHANGELOG.md Normal file

@ -1,6 +1,6 @@
App Skellington
Copyright (c) 2020 Mathew Guest
Copyright (c) 2024 Mathew Guest
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation

@ -10,7 +10,7 @@ Application framework for Python, features include:
Principles:
- Lend to creating beautiful, easy to read and understand code in the application.
- Minimize coupling of applications to this framework.
- Compatable with Linux, Windows, and Mac. Try to be compatible as possible otherwise.
- Compatible with Linux, Windows, and Mac. Try to be compatible as possible otherwise.
- Try to be compatible with alternate Python runtimes such as PyPy and older python environments. \*WIP
Application Configuration
@ -47,8 +47,8 @@ argument.
Debug - Turn on Logging
-----------------------
Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns
on AppSkellington-level logging. For example,
Set 'APPSKELLINGTON_DEBUG' environment variable to any value which turns
on AppSkellington logger. For example,
APPSKELLINGTON_DEBUG=1 <executable>
@ -61,6 +61,28 @@ Tests
-----
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
Development
-----------
Clone the repo:
```commandline
git clone https://git-mirror.zavage.net/zavage-software/app_skellington.git
```
Verify your desired python environment.
Poetry install to install dependencies and the program in your desired python environment
```commandline
poetry install
# verify no errors
```
Install pre-commit hooks:
```commandline
pre-commit install
```
Begin development.
License
-------
I'm releasing this software under one of the most permissive
@ -72,4 +94,3 @@ Notes
-----
See official website: https://zavage-software.com
Please report bugs, improvements, or feedback!

@ -1,8 +1,4 @@
import logging
import sys
from .app_container import *
from .cfg import *
from .cli import *
from .log import *

@ -3,24 +3,28 @@ import os
import sys
# Check and gracefully fail if the user needs to install a 3rd-party dep.
libnames = ['appdirs', 'configobj', 'colorlog']
libnames = ["appdirs", "configobj", "colorlog"]
def check_env_has_dependencies(libnames):
rc = True
for libname in libnames:
try:
__import__(libname)
except ModuleNotFoundError as ex:
print('Missing third-party library: ', ex, file=sys.stderr)
print("Missing third-party library: ", ex, file=sys.stderr)
rc = False
return rc
if not check_env_has_dependencies(libnames):
print('Unable to load program without installed dependencies', file=sys.stderr)
raise ImportError('python environment needs third-party dependencies installed')
print("Unable to load program without installed dependencies", file=sys.stderr)
raise ImportError("python environment needs third-party dependencies installed")
# Logger for before the application and logging config is loaded
# - used to log before logging is configured
_log_fmt = '%(levelname)-7s:%(message)s'
_logger_name = 'skell'
_log_fmt = "%(levelname)-7s:%(message)s"
_logger_name = "skell"
_bootstrap_logger = logging.getLogger(_logger_name)
# NOTE(MG) Logger monkey-patch:
@ -29,13 +33,13 @@ _bootstrap_logger = logging.getLogger(_logger_name)
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG
# environment variable the first time, as app_skellington module
# is imported. See cfg.py
if os.environ.get('APPSKELLINGTON_DEBUG', None):
_bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels
if os.environ.get("APPSKELLINGTON_DEBUG", None):
_bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels
fmt = logging.Formatter(_log_fmt)
handler = logging.StreamHandler()
handler.setFormatter(fmt)
_bootstrap_logger.addHandler(handler)
_bootstrap_logger.debug('log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.')
_bootstrap_logger.debug("log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.")
# Logging is by default off, excepting CRITICAL
else:
@ -45,4 +49,3 @@ _bootstrap_logger.propagate = False
# NOTE(MG) Pretty sure the logger has the default handler too at this point.
# It's been related to some issues with the logger double-printing messages.
_bootstrap_logger.addHandler(logging.NullHandler())

@ -1,9 +1,9 @@
from __future__ import print_function
import inspect
import os
import sys
from . import _util
def eprint(*args, **kwargs):
"""
@ -11,6 +11,7 @@ def eprint(*args, **kwargs):
"""
print(*args, file=sys.stderr, **kwargs)
def filename_to_abspath(filename):
"""
Converts a filename to it's absolute path. If it's already an
@ -18,6 +19,7 @@ def filename_to_abspath(filename):
"""
return os.path.abspath(filename)
def does_file_exist(filepath):
"""
Because the file can be deleted or created immediately after execution of
@ -26,31 +28,31 @@ def does_file_exist(filepath):
instant in execution.
"""
try:
fp = open(filepath, 'r')
open(filepath, "r")
return True
except FileNotFoundError as ex:
return False
def ensure_dir_exists(dirpath):
if dirpath is None:
return
if dirpath == '':
if dirpath == "":
return
os.makedirs(dirpath, exist_ok=True)
def get_root_asset(filepath):
"""
Attempts to locate a resource or asset shipped with the application.
Searches starting at the root module (__main__) which should be the
python file initially invoked.
"""
module_root =\
os.path.abspath(
os.path.dirname(
sys.modules['__main__'].__file__))
module_root = os.path.abspath(os.path.dirname(sys.modules["__main__"].__file__))
path = os.path.join(module_root, filepath)
return path
def get_asset(module, filepath):
"""
Attempts to locate a resource or asset shipped with the application.
@ -75,7 +77,7 @@ def get_asset(module, filepath):
elif isinstance(module, module):
module_file = module.__file__
else:
raise Exception('Invalid Usage')
raise Exception("Invalid Usage")
try:
root = module_file
@ -90,6 +92,7 @@ def get_asset(module, filepath):
path = os.path.join(root, filepath)
return path
def register_class_as_commands(app, submenu, cls_object):
"""
Registers commands for each class method. e.g.: pass in the CLI
@ -105,7 +108,7 @@ def register_class_as_commands(app, submenu, cls_object):
for m in members:
name = m[0]
ref = m[1]
if inspect.isfunction(ref) and not name.startswith('_'):
if inspect.isfunction(ref) and not name.startswith("_"):
cls_method = ref
constructor = app._inject_service_dependencies(cls_constructor)
sig = inspect.signature(cls_method)
@ -114,9 +117,10 @@ def register_class_as_commands(app, submenu, cls_object):
docstring = inspect.getdoc(cls_method)
submenu.register_command(func, name, sig, docstring)
def create_func(constructor, cls_method):
def func(*args, **kwargs):
cmd_class_instance = constructor()
return cls_method(cmd_class_instance, *args, **kwargs)
return func
return func

@ -1,34 +1,37 @@
import appdirs
import collections
import functools
import inspect
import os
import sys
import appdirs
from . import (
_util,
cfg,
cli,
log,
)
# Application scaffolding:
from ._bootstrap import _bootstrap_logger
from . import log
from . import _util
from . import cli
from . import cfg
import logging
# These two variables affect the directory paths for
# config files and logging.
DEFAULT_APP_NAME = ''
DEFAULT_APP_AUTHOR = ''
DEFAULT_APP_NAME = ""
DEFAULT_APP_AUTHOR = ""
class ApplicationContext:
"""
Container for application-wide state; i.e. app configuration and loggers.
"""
def __init__(self, config, log):
self.config = config
self.log = log
self.parsed_argv = None
self.parsed_argv_unknown = None
class ApplicationContainer:
"""
Generalized application functionality. Used for linking components and modules of the application
@ -40,14 +43,10 @@ class ApplicationContainer:
Override appname and appauthor arguments to direct config and log
directories.
"""
def __init__(
self,
configspec_filepath=None,
configini_filepath=None,
*args, **kwargs
):
self.appname = kwargs.get('appname') or DEFAULT_APP_NAME
self.appauthor = kwargs.get('appauthor') or DEFAULT_APP_AUTHOR
def __init__(self, configspec_filepath=None, configini_filepath=None, *args, **kwargs):
self.appname = kwargs.get("appname") or DEFAULT_APP_NAME
self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR
# Instantiate application context which contains
# global state, configuration, loggers, and runtime args.
@ -57,29 +56,28 @@ class ApplicationContainer:
logger = log.LoggingLayer(self.appname, self.appauthor)
# Try and load logging configuration if provided
log_config = config.get('logging')
log_config = config.get("logging")
if log_config is not None:
logger.configure_logging(log_config)
else:
logger.configure_logging()
self.ctx = ApplicationContext(config, logger)
# Reference to root_app avail. in context
self.ctx.root_app = self
# Reference to context service avail. in root_app
self['ctx'] = lambda: self.ctx
self["ctx"] = lambda: self.ctx
self.cli = cli.CommandTree() # Command-line interface
self.cli = cli.CommandTree() # Command-line interface
# Run methods if subclass implemented them:
if callable(getattr(self, '_cli_options', None)):
if callable(getattr(self, "_cli_options", None)):
self._cli_options()
if callable(getattr(self, '_services', None)):
if callable(getattr(self, "_services", None)):
self._services()
if callable(getattr(self, '_command_menu', None)):
if callable(getattr(self, "_command_menu", None)):
self._command_menu()
def __delitem__(self, service_name):
@ -99,11 +97,12 @@ class ApplicationContainer:
app['datalayer'] => returns the made-up "datalayer" service.
"""
try:
service_factory = self._dependencies[service_name] # Retrieve factory function
return service_factory() # Call factory() to return instance of service
service_factory = self._dependencies[service_name] # Retrieve factory function
return service_factory() # Call factory() to return instance of service
except KeyError as ex:
msg = 'failed to inject service: {}'.format(service_name)
msg = "failed to inject service: {}".format(service_name)
_bootstrap_logger.critical(msg)
_bootstrap_logger.debug(ex)
raise ServiceNotFound
def __setitem__(self, service_name, value):
@ -131,9 +130,7 @@ class ApplicationContainer:
dependencies.append(self[dep_name])
return model_constructor(*dependencies)
def _get_config_filepath(
self, app_name, app_author, config_filename='config.ini'
):
def _get_config_filepath(self, app_name, app_author, config_filename="config.ini") -> str:
"""
Attempt to find config.ini in the user's config directory.
@ -142,10 +139,10 @@ class ApplicationContainer:
"""
dirname = appdirs.user_config_dir(app_name, app_author)
filepath = os.path.join(dirname, config_filename)
_bootstrap_logger.info('default config filepath calculated to be: %s', filepath)
_bootstrap_logger.info("default config filepath calculated to be: %s", filepath)
return filepath
def _get_configspec_filepath(self, configspec_filename='config.spec'):
def _get_configspec_filepath(self, configspec_filename: str = "config.spec") -> str:
"""
Attempt to find config.spec inside the installed package directory.
"""
@ -161,12 +158,12 @@ class ApplicationContainer:
"""
sig = inspect.signature(constructor.__init__)
params = sig.parameters
params = [params[paramname].name for paramname in params] # Convert Param() type => str
cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
params = [params[paramname].name for paramname in params] # Convert Param() type => str
cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
return functools.partial(self._construct_model, constructor, *cls_dependencies)
def load_command(self):
def load_command(self) -> bool:
args, unk, success = self.cli.parse()
if not success:
return False
@ -181,24 +178,26 @@ class ApplicationContainer:
try:
self.cli.run_command()
except NoCommandSpecified as ex:
print('Failure: No command specified.')
print("Failure: No command specified.")
def interactive_shell(self):
pass
raise NotImplementedError()
def invoke_from_cli(self):
self.invoke_command()
def usage(self):
pass
raise NotImplementedError()
# Applications need a default usage
class ServiceNotFound(Exception):
"""
Application framework error: unable to find and inject dependency.
"""
pass
class NoCommandSpecified(Exception):
pass

@ -4,15 +4,12 @@
# ConfigObj module and it's recommended to use config.spec files to define
# your available configuration of the relevant application.
from . import _util
import configobj
import validate
from ._bootstrap import _bootstrap_logger
import appdirs
import argparse
import configobj
import os
import sys
import validate
class Config:
"""
@ -26,16 +23,11 @@ class Config:
"""
DEFAULT_CAPABILITIES = {
'allow_options_beyond_spec': True,
"allow_options_beyond_spec": True,
}
def __init__(
self,
configspec_filepath=None,
configini_filepath=None,
capabilities=None
):
self._config_obj = None # must be type configobj.ConfigObj()
def __init__(self, configspec_filepath=None, configini_filepath=None, capabilities=None):
self._config_obj = None # must be type configobj.ConfigObj()
self._configini_data = None
self._configini_filepath = None
self._configspec_data = None
@ -83,9 +75,7 @@ class Config:
@configspec_filepath.setter
def configspec_filepath(self, filepath):
if filepath is None:
_bootstrap_logger.debug(
'cfg - Clearing configspec'
)
_bootstrap_logger.debug("cfg - Clearing configspec")
self._configspec_filepath = None
self._configspec_data = None
self._has_changed_internally = True
@ -98,27 +88,22 @@ class Config:
self._configspec_filepath = filepath
self._configspec_data = data
self._has_changed_internally = True
_bootstrap_logger.debug(
'cfg - Set configspec and read contents: %s',
filepath
)
_bootstrap_logger.debug("cfg - Set configspec and read contents: %s", filepath)
self.load_config()
return
except OSError as ex:
_bootstrap_logger.critical(
'cfg - Failed to find config.spec: file not found (%s)',
filepath
)
raise OSError('Failed to read provided config.spec file')
_bootstrap_logger.critical("cfg - Failed to find config.spec: file not found (%s)", filepath)
raise OSError("Failed to read provided config.spec file")
self.load_config()
# TODO(MG) initial code was present but unreachable. is an error in coding.
# self.load_config()
def __contains__(self, key):
try:
has_item = key in self._config_obj
return has_item
except KeyError as ex:
pass
_bootstrap_logger.error("failed to __containers__ on key (%s): %s", key, ex)
def __delitem__(self, key):
"""
@ -128,7 +113,7 @@ class Config:
try:
del self[key]
except KeyError as ex:
pass
_bootstrap_logger.error("failed to __delitem__ on key (%s): %s", key, ex)
def __getitem__(self, key):
"""
@ -142,6 +127,7 @@ class Config:
# return self._config_obj[key].dict()
return self._config_obj[key]
except KeyError as ex:
_bootstrap_logger.error("failed to __getitem__ on key (%s): %s", key, ex)
raise
def __setitem__(self, key, value):
@ -162,11 +148,16 @@ class Config:
v = self.__getitem__(key)
return v
except KeyError as ex:
_bootstrap_logger.error("failed to retrieve config key (%s): %s", key, ex)
return default
def load_config(
self, configspec_filepath=None, configini_filepath=None
):
def load_config(self, configspec_filepath=None, configini_filepath=None) -> bool:
"""
Loads config from file, validating against configspec.
Returns:
bool: success status of command and validation.
"""
# Set new arguments if were passed in:
if configspec_filepath is not None:
self.configspec_filepath = configspec_filepath
@ -179,7 +170,7 @@ class Config:
rc = self._validate_config_against_spec()
if not rc:
if self._capability_enforce_strict_spec_validation:
raise RuntimeError('Failed to validate config.ini against spec.')
raise RuntimeError("Failed to validate config.ini against spec.")
return False
return True
@ -196,83 +187,91 @@ class Config:
# options
configspec=config_spec,
# encoding
interpolation='template'
interpolation="template",
# raise_errors
)
_bootstrap_logger.debug(
'cfg - Parsed configuration. config.spec = %s, config.ini = %s',
config_spec, config_ini
"cfg - Parsed configuration. config.spec = %s, config.ini = %s", config_spec, config_ini
)
return True
except configobj.ParseError as ex:
msg = 'cfg - Failed to load config: error in config.spec configuration: {}'.format(config_filepath)
msg = "cfg - Failed to load config: error in config.spec configuration: {}".format(config_filepath)
_bootstrap_logger.error(msg)
return False
except OSError as ex:
msg = 'cfg - Failed to load config: config.spec file not found.'
msg = "cfg - Failed to load config: config.spec file not found."
_bootstrap_logger.error(msg)
return False
except Exception as ex:
print(ex)
_bootstrap_logger.error(ex)
def _validate_config_against_spec(self):
config_spec = self.configspec_filepath
config_ini = self.configini_filepath
# Hack the configobj module to alter the interpolation for validate.py:
configobj.DEFAULT_INTERPOLATION = 'template'
configobj.DEFAULT_INTERPOLATION = "template"
# Validate config.ini against config.spec
try:
_bootstrap_logger.info('cfg - Validating config file against spec')
_bootstrap_logger.info("cfg - Validating config file against spec")
val = validate.Validator()
assert isinstance(self._config_obj, configobj.ConfigObj), 'expecting configobj.ConfigObj, received %s' % type(self._config_obj)
assert isinstance(
self._config_obj, configobj.ConfigObj
), "expecting configobj.ConfigObj, received %s" % type(self._config_obj)
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file
test_results = self._config_obj.validate(
val, copy=True, preserve_errors=True
)
test_results = self._config_obj.validate(val, copy=True, preserve_errors=True)
if test_results is True:
_bootstrap_logger.info(
'cfg- Successfully validated configuration against spec. input = %s, validation spec = %s',
config_ini, config_spec
"cfg- Successfully validated configuration against spec. input = %s, validation spec = %s",
config_ini,
config_spec,
)
return True
elif test_results is False:
_bootstrap_logger.debug(
'cfg - Potentially discovered invalid config.spec'
)
_bootstrap_logger.debug("cfg - Potentially discovered invalid config.spec")
else:
self._validate_parse_errors(test_results)
return False
except ValueError as ex:
_bootstrap_logger.error('cfg - Failed while validating config against spec. ')
_bootstrap_logger.error("cfg - Failed while validating config against spec. ")
_bootstrap_logger.debug(ex)
return False
def _validate_parse_errors(self, test_results):
_bootstrap_logger.critical('cfg - Config file failed validation.')
for (section_list, key, rslt) in configobj.flatten_errors(self._config_obj, test_results):
_bootstrap_logger.critical('cfg - Config error info: %s %s %s', section_list, key, rslt)
_bootstrap_logger.critical("cfg - Config file failed validation.")
for section_list, key, rslt in configobj.flatten_errors(self._config_obj, test_results):
_bootstrap_logger.critical("cfg - Config error info: %s %s %s", section_list, key, rslt)
if key is not None:
_bootstrap_logger.critical('cfg - Config failed validation: [%s].%s appears invalid. msg = %s', '.'.join(section_list), key, rslt)
_bootstrap_logger.critical(
"cfg - Config failed validation: [%s].%s appears invalid. msg = %s",
".".join(section_list),
key,
rslt,
)
else:
_bootstrap_logger.critical("cfg - Config failed validation: missing section, name = '%s'. msg = %s", '.'.join(section_list), rslt)
_bootstrap_logger.critical(
"cfg - Config failed validation: missing section, name = '%s'. msg = %s",
".".join(section_list),
rslt,
)
def print_config(self):
"""
Print configuration to stdout.
"""
print('config:')
print("config:")
self._config_obj.walk(print)
for section in self._config_obj.sections:
print(section)
for key in self._config_obj[section]:
print(' ', self._config_obj[section][key])
print(" ", self._config_obj[section][key])
class EnvironmentVariables:
def __init__(self):
raise NotImplementedError

@ -1,18 +1,17 @@
import argparse
import inspect
import logging
import re
import sys
import app_skellington
from ._bootstrap import _bootstrap_logger
from . import app_container
from ._bootstrap import _bootstrap_logger
# If explicit fail is enabled, any command with at least one unknown
# argument will be rejected entirely. If not enabled, unknown arguments
# will be ignored.
EXPLICIT_FAIL_ON_UNKNOWN_ARGS = True
class CommandTree:
"""
Command-line interface to hold a menu of commands. You can register
@ -51,11 +50,12 @@ class CommandTree:
the second. In the same way the -h, --help options print different docs
depending on where the help option was passed.
"""
def __init__(self):
self.root_parser = argparse.ArgumentParser()
self.submenu_param = None # submenu_param is the variable name
# of the root submenu argument, i.e. the arg
# in root_parser which selects the submenu.
self.submenu_param = None # submenu_param is the variable name
# of the root submenu argument, i.e. the arg
# in root_parser which selects the submenu.
self.entries = {}
# NOTE(MG) Implementation note:
# CommandTree uses only one of these internal structures (i.e. mutually exclusive),
@ -66,13 +66,13 @@ class CommandTree:
self._single_command = None
def print_tree(self):
raise NotImplemented
raise NotImplementedError()
def add_argument(self, *args, **kwargs):
"""
Adds an argument to the root parser.
"""
_bootstrap_logger.info('adding argument to root parser: %s and %s', args, kwargs)
_bootstrap_logger.info("adding argument to root parser: %s and %s", args, kwargs)
self.root_parser.add_argument(*args, **kwargs)
def init_submenu(self, param_name, is_required=False):
@ -83,39 +83,27 @@ class CommandTree:
# NOTE(MG) Fix for Python>=3.7,
# argparse.ArgumentParser added 'required' argument.
# Must also be written into SubMenu.create_submenu.
func_args = {
'dest': param_name,
'metavar': param_name,
'required': is_required
}
if (
sys.version_info.major == 3
and sys.version_info.minor < 7
):
func_args = {"dest": param_name, "metavar": param_name, "required": is_required}
if sys.version_info.major == 3 and sys.version_info.minor < 7:
if is_required:
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7')
del func_args['required']
_bootstrap_logger.warn("Unable to enforce required submenu: Requires >= Python 3.7")
del func_args["required"]
# END fix for Python<3.7
# Creates an argument as a slot in the underlying argparse.
subparsers = self.root_parser.add_subparsers(
**func_args
)
subparsers = self.root_parser.add_subparsers(**func_args)
submenu = SubMenu(self, subparsers, param_name)
submenu.submenu_path = ''
submenu.submenu_path = ""
submenu.var_name = param_name
_bootstrap_logger.info('Initialized root-level submenu: Parameter = \'%s\'', param_name)
_bootstrap_logger.info("Initialized root-level submenu: Parameter = '%s'", param_name)
self.entries[param_name] = submenu
self.submenu_param = param_name
return submenu
def register_command(
self, func, cmd_name=None, func_signature=None,
docstring=None
):
def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
"""
When no submenu functionality is desired, this links a single
command into underlying argparse options.
@ -128,7 +116,7 @@ class CommandTree:
pass
# print('func is method')
else:
raise Exception('bad value passed in for function')
raise Exception("bad value passed in for function")
if not cmd_name:
# safe try/except
@ -154,26 +142,19 @@ class CommandTree:
# For each paramter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry:
for key in params:
if key == 'self':
if key == "self":
continue
param = params[key]
if '=' in str(param):
if "=" in str(param):
if param.default is None:
helptext = 'default provided'
helptext = "default provided"
else:
helptext = "default = '{}'".format(param.default)
self.root_parser.add_argument(
key,
help=helptext,
nargs='?',
default=param.default
)
self.root_parser.add_argument(key, help=helptext, nargs="?", default=param.default)
else:
helptext = 'required'
self.root_parser.add_argument(
key,
help=helptext)
helptext = "required"
self.root_parser.add_argument(key, help=helptext)
# Build the CommandEntry structure
cmd = CommandEntry()
@ -184,7 +165,7 @@ class CommandTree:
cmd.callback = func
registered_name = cmd_name
_bootstrap_logger.info('registered command: %s', registered_name)
_bootstrap_logger.info("registered command: %s", registered_name)
# end copy-paste then editted from SubMenu.register_command
self._cmd_tree_is_single_command = True
@ -209,7 +190,7 @@ class CommandTree:
# 'failed to parse arguments: explicitly failing to be safe')
# return False, False
if hasattr(pargs, 'usage'):
if hasattr(pargs, "usage"):
pass
# print('found usage in app_skellington')
@ -222,19 +203,19 @@ class CommandTree:
def run_command(self, args=None):
args, unk, success = self.parse(args)
if not success:
_bootstrap_logger.info('cli - SystemExit: Perhaps user invoked --help')
_bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help")
return
if args is False and unk is False:
_bootstrap_logger.error('cli - Failed parsing args.')
_bootstrap_logger.error("cli - Failed parsing args.")
return False
_bootstrap_logger.info('cli - Received args from shell: %s', args)
_bootstrap_logger.info("cli - Received args from shell: %s", args)
args = vars(args)
cmd = self._lookup_command(args)
if cmd is None:
_bootstrap_logger.critical('cli - Failed to find command.')
_bootstrap_logger.critical("cli - Failed to find command.")
return False
return self._invoke_command(cmd, args)
@ -246,31 +227,31 @@ class CommandTree:
# the CommandTree with no SubMenu (submenu will be disabled
# in this case):
if self._cmd_tree_is_single_command:
assert self._cmd_tree_is_single_command is True, 'corrupt data structure in CommandMenu'
assert self._entries is None, 'corrupt data structure in CommandMenu'
assert isinstance(self._single_command, CommandEntry), 'corrupt data structure in CommandMenu'
assert self._cmd_tree_is_single_command is True, "corrupt data structure in CommandMenu"
assert self._entries is None, "corrupt data structure in CommandMenu"
assert isinstance(self._single_command, CommandEntry), "corrupt data structure in CommandMenu"
return self._single_command
# There is at least one submenu we need to go down:
else:
assert self._single_command is None, 'corrupt data structure in CommandMenu'
assert self._cmd_tree_is_single_command == False, 'corrupt data structure in CommandMenu'
assert self._single_command is None, "corrupt data structure in CommandMenu"
assert self._cmd_tree_is_single_command == False, "corrupt data structure in CommandMenu"
# Key or variable name used by argparse to store the submenu options
argparse_param = self.submenu_param # e.g.: submenu_root
argparse_param = self.submenu_param # e.g.: submenu_root
submenu = self.entries[argparse_param]
while True:
if argparse_param not in keys:
print('root menu parameter not found in args:', argparse_param)
input('<broken>')
print("root menu parameter not found in args:", argparse_param)
input("<broken>")
val = args.get(argparse_param)
_bootstrap_logger.debug('cli - argparse command is \'{}\' = {}'.format(argparse_param, val))
_bootstrap_logger.debug("cli - argparse command is '{}' = {}".format(argparse_param, val))
lookup = submenu.entries.get(val)
_bootstrap_logger.debug('cli - lookup, entries[{}] = {}'.format(val, lookup))
_bootstrap_logger.debug("cli - lookup, entries[{}] = {}".format(val, lookup))
# pop value
del args[argparse_param]
@ -283,7 +264,7 @@ class CommandTree:
# return self._invoke_command(lookup, args)
else:
raise app_container.NoCommandSpecified('No command specified.')
raise app_container.NoCommandSpecified("No command specified.")
def _invoke_command(self, cmd, args):
command_to_be_invoked = cmd.callback
@ -296,26 +277,24 @@ class CommandTree:
if param.name in args:
func_args.append(args[param.name])
_bootstrap_logger.info('cli - function: %s', func)
_bootstrap_logger.info('cli - function args: %s', func_args)
_bootstrap_logger.info("cli - function: %s", func)
_bootstrap_logger.info("cli - function args: %s", func_args)
return command_to_be_invoked(*func_args)
def _get_subparser(self):
return self.root_parser._subparsers._actions[1]
class SubMenu:
def __init__(self, parent, subparsers_obj, name):
self.parent = parent # Reference to root CommandTree
self.parent = parent # Reference to root CommandTree
self.subparsers_obj = subparsers_obj
self.name = name
self.submenu_path = None
self.entries = {}
def register_command(
self, func, cmd_name=None, func_signature=None,
docstring=None
):
def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
"""
Registers a command as an entry in this submenu. Provided function is
converted into argparse arguments and made available to the user.
@ -345,7 +324,7 @@ class SubMenu:
elif inspect.ismethod(func):
pass
else:
raise Exception('bad value passed in for function')
raise Exception("bad value passed in for function")
if not cmd_name:
# TODO(MG) Safer sanitation
@ -369,38 +348,30 @@ class SubMenu:
# Entry in local argparse._SubParsersAction
# type = ArgumentParser
child_node = self.subparsers_obj.add_parser(
cmd_name, # Note: cmd_name here will be the VALUE
# passed into the argparse arg VARIABLE NAME
# created when the SubMenu/argparse.add_subparsers()
# was created.
cmd_name, # Note: cmd_name here will be the VALUE
# passed into the argparse arg VARIABLE NAME
# created when the SubMenu/argparse.add_subparsers()
# was created.
help=help_text,
description=description_text
description=description_text,
)
# For each paramter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry:
for key in params:
if key == 'self':
if key == "self":
continue
param = params[key]
if '=' in str(param):
if "=" in str(param):
if param.default is None:
helptext = 'default provided'
helptext = "default provided"
else:
helptext = "default = '{}'".format(param.default)
child_node.add_argument(
key,
help=helptext,
nargs='?',
default=param.default
)
child_node.add_argument(key, help=helptext, nargs="?", default=param.default)
else:
helptext = 'required'
child_node.add_argument(
key,
help=helptext
)
helptext = "required"
child_node.add_argument(key, help=helptext)
# Build the CommandEntry structure
cmd = CommandEntry()
@ -410,15 +381,11 @@ class SubMenu:
# cmd.func_ref = None
cmd.callback = func
registered_name = '{}.{}'.format(
self.submenu_path,
cmd_name)
_bootstrap_logger.info('cli - registered command: %s', registered_name)
registered_name = "{}.{}".format(self.submenu_path, cmd_name)
_bootstrap_logger.info("cli - registered command: %s", registered_name)
self.entries[cmd_name] = cmd
def create_submenu(
self, var_name, cmd_entry_name=None, is_required=False
):
def create_submenu(self, var_name, cmd_entry_name=None, is_required=False):
"""
Creates a child-submenu.
@ -443,54 +410,37 @@ class SubMenu:
# Create an entry in self's submenu:
# type = ArgumentParser
entry_node = self.subparsers_obj.add_parser(
cmd_entry_name,
help='sub-submenu help',
description='sub-sub description')
cmd_entry_name, help="sub-submenu help", description="sub-sub description"
)
# NOTE(MG) Fix for Python>=3.7,
# argparse.ArgumentParser added 'required' argument.
# Must also be written into CommandTree.init_submenu
func_args = {
'dest': var_name,
'metavar': var_name,
'required': is_required
}
if (
sys.version_info.major == 3
and sys.version_info.minor < 7
):
func_args = {"dest": var_name, "metavar": var_name, "required": is_required}
if sys.version_info.major == 3 and sys.version_info.minor < 7:
if is_required:
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7')
del func_args['required']
_bootstrap_logger.warn("Unable to enforce required submenu: Requires >= Python 3.7")
del func_args["required"]
# END fix for Python<3.7
# Turn entry into a submenu of it's own:
# type = _SubParsersAction
subp_node = entry_node.add_subparsers(
**func_args
)
subp_node = entry_node.add_subparsers(**func_args)
submenu = SubMenu(
self.parent,
subp_node,
cmd_entry_name
)
submenu = SubMenu(self.parent, subp_node, cmd_entry_name)
submenu.var_name = var_name
submenu.submenu_path = '{}.{}'.format(self.submenu_path, cmd_entry_name)
submenu.submenu_path = "{}.{}".format(self.submenu_path, cmd_entry_name)
submenu_name = submenu.submenu_path
_bootstrap_logger.info('cli - registered submenu: %s', submenu_name)
_bootstrap_logger.info("cli - registered submenu: %s", submenu_name)
self.entries[cmd_entry_name] = submenu
return submenu
def __repr__(self):
return 'SubMenu({})<{}>'.format(
self.name,
','.join(['cmds'])
)
return "SubMenu({})<{}>".format(self.name, ",".join(["cmds"]))
class CommandEntry:
"""
@ -506,18 +456,20 @@ class CommandEntry:
arguments into argparse options (creating the documentation also). Similary,
it can convert from argparse options into a function call.
"""
def __init__(self):
self.argparse_node = None
self.cmd_name = None # Don't think we need. And needs to be changed
# from SubMenu
self.cmd_name = None # Don't think we need. And needs to be changed
# from SubMenu
self.menu_path = None
self.func_signature = None
self.func_ref = None
self.callback = None
def __repr__(self):
return 'CommandEntry<{}>'.format(self.cmd_name)
return "CommandEntry<{}>".format(self.cmd_name)
class HelpGenerator:
def __init__(self):
@ -531,10 +483,10 @@ class HelpGenerator:
"""
if doctext == None:
return doctext
regex = '(.*?)[.?!]'
regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
if match:
return match.group(1) + '.'
return match.group(1) + "."
return doctext
@staticmethod
@ -545,9 +497,8 @@ class HelpGenerator:
"""
if doctext == None:
return doctext
regex = '(.*?)[.?!]'
regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
if match:
return match.group(1) + '.'
return match.group(1) + "."
return doctext

@ -1,48 +1,41 @@
from ._bootstrap import _bootstrap_logger, _logger_name
from . import _util
import appdirs
import colorlog
import logging
import logging.config
import os
import appdirs
from . import _util
from ._bootstrap import _bootstrap_logger, _logger_name
DEFAULT_LOG_SETTINGS = {
'formatters': {
'colored': {
'class': 'colorlog.ColoredFormatter',
"formatters": {
"colored": {
"class": "colorlog.ColoredFormatter",
# 'format': '%(log_color)s%(levelname)-8s%(reset)s:%(log_color)s%(name)-5s%(reset)s:%(white)s%(message)s'
'format': '%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s',
"format": "%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s",
}
},
'handlers': {
'stderr': {
'class': 'logging.StreamHandler',
'level': 'debug',
'formatter': 'colored'
}
},
'loggers': {
'root': {
'handlers': ['stderr',],
'level': 'debug'
"handlers": {"stderr": {"class": "logging.StreamHandler", "level": "debug", "formatter": "colored"}},
"loggers": {
"root": {
"handlers": [
"stderr",
],
"level": "debug",
},
'app_skellington': {
"app_skellington": {
# 'handlers': ['stderr',],
'level': 'critical',
'propagate': 'false'
}
}
"level": "critical",
"propagate": "false",
},
},
}
class LoggingLayer:
def __init__(
self, appname=None, appauthor=None
):
self.appname = appname or ''
self.appauthor = appauthor or ''
def __init__(self, appname=None, appauthor=None):
self.appname = appname or ""
self.appauthor = appauthor or ""
self.loggers = {}
def __getitem__(self, k):
@ -71,17 +64,17 @@ class LoggingLayer:
noise for typical operation.
"""
if config_dict is None:
_bootstrap_logger.debug('log - No application logging configuration provided. Using default')
_bootstrap_logger.debug("log - No application logging configuration provided. Using default")
config_dict = DEFAULT_LOG_SETTINGS
self.transform_config(config_dict)
try:
_bootstrap_logger.debug('log - Log configuration: %s', config_dict)
_bootstrap_logger.debug("log - Log configuration: %s", config_dict)
logging.config.dictConfig(config_dict)
_bootstrap_logger.debug('log - Configured all logging')
_bootstrap_logger.debug("log - Configured all logging")
except Exception as ex:
print('unable to configure logging:', ex, type(ex))
print("unable to configure logging:", ex, type(ex))
def transform_config(self, config_dict):
"""
@ -89,48 +82,45 @@ class LoggingLayer:
parameters and the final config dictionary passed into the logging module.
"""
# Version should be hard-coded 1, per Python docs
if 'version' in config_dict:
if config_dict['version'] != 1:
if "version" in config_dict:
if config_dict["version"] != 1:
_bootstrap_logger.warn("logging['version'] must be '1' per Python docs")
config_dict['version'] = 1
config_dict["version"] = 1
self._add_own_logconfig(config_dict)
# Replace logger level strings with value integers from module
for handler in config_dict['handlers']:
d = config_dict['handlers'][handler]
self._convert_str_to_loglevel(d, 'level')
for handler in config_dict["handlers"]:
d = config_dict["handlers"][handler]
self._convert_str_to_loglevel(d, "level")
# Replace logger level strings with value integers from module
for logger in config_dict['loggers']:
d = config_dict['loggers'][logger]
self._convert_str_to_loglevel(d, 'level')
for logger in config_dict["loggers"]:
d = config_dict["loggers"][logger]
self._convert_str_to_loglevel(d, "level")
# Implementation note:
# app_skellington expects root logger configuration to be under 'root'
# instead of '' (python spec) because '' is not a valid name in ConfigObj.
try:
if config_dict['loggers'].get('root') is not None:
config_dict['loggers'][''] = config_dict['loggers']['root']
del config_dict['loggers']['root']
if config_dict["loggers"].get("root") is not None:
config_dict["loggers"][""] = config_dict["loggers"]["root"]
del config_dict["loggers"]["root"]
except Exception as ex:
_bootstrap_logger.warn('was not able to find and patch root logger configuration from arguments')
_bootstrap_logger.warn("was not able to find and patch root logger configuration from arguments")
# Evaluate the full filepath of the file handler
if 'file' not in config_dict['handlers']:
if "file" not in config_dict["handlers"]:
return
if os.path.abspath(config_dict['handlers']['file']['filename']) ==\
config_dict['handlers']['file']['filename']:
if os.path.abspath(config_dict["handlers"]["file"]["filename"]) == config_dict["handlers"]["file"]["filename"]:
# Path is already absolute
pass
else:
dirname = appdirs.user_log_dir(self.appname, self.appauthor)
_util.ensure_dir_exists(dirname)
log_filepath = os.path.join(dirname, config_dict['handlers']['file']['filename'])
config_dict['handlers']['file']['filename'] = log_filepath
log_filepath = os.path.join(dirname, config_dict["handlers"]["file"]["filename"])
config_dict["handlers"]["file"]["filename"] = log_filepath
def _add_own_logconfig(self, config_dict):
# NOTE(MG) Monkey-patch logger
@ -140,13 +130,11 @@ class LoggingLayer:
# variable the second time, when it's being reloaded as a
# logging configuration is read from config file.
# See _bootstrap.py
if os.environ.get('APPSKELLINGTON_DEBUG', None):
if _logger_name not in config_dict['loggers']:
config_dict['loggers'][_logger_name] = {
'level': 'debug', 'propagate': 'false'
}
if os.environ.get("APPSKELLINGTON_DEBUG", None):
if _logger_name not in config_dict["loggers"]:
config_dict["loggers"][_logger_name] = {"level": "debug", "propagate": "false"}
else:
config_dict['loggers'][_logger_name]['level'] = 'debug'
config_dict["loggers"][_logger_name]["level"] = "debug"
def _convert_str_to_loglevel(self, dict_, key):
"""
@ -164,16 +152,15 @@ class LoggingLayer:
s = dict_[key]
except KeyError as ex:
raise
if s == 'critical':
if s == "critical":
dict_[key] = logging.CRITICAL
elif s == 'error':
elif s == "error":
dict_[key] = logging.ERROR
elif s == 'warning':
elif s == "warning":
dict_[key] = logging.WARNING
elif s == 'info':
elif s == "info":
dict_[key] = logging.INFO
elif s == 'debug':
elif s == "debug":
dict_[key] = logging.DEBUG
elif s == 'all':
elif s == "all":
dict_[key] = logging.NOTSET

371
poetry.lock generated Normal file

@ -0,0 +1,371 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "black"
version = "24.4.2"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "filelock"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "identify"
version = "2.6.0"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "virtualenv"
version = "20.26.3"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "c20dac4ba2095b38a3009c1520b6cd1e9865cc50dff8559ddd767403898406c6"

49
pyproject.toml Normal file

@ -0,0 +1,49 @@
[tool.poetry]
name = "app_skellington"
version = "0.1.0"
description = "app_skellington CLI framework"
authors = [
"Mathew Guest <mathew.guest@davita.com>",
]
license = "Creative Commons"
readme = "README.md"
homepage = "https://zavage-software.com/portfolio/app_skellington"
repository = "https://git-mirror.zavage.net/zavage-software/app_skellington"
documentation = "https://git-mirror.zavage.net/zavage-software/app_skellington"
keywords = [""]
packages = [{ include = "app_skellington" }]
include = [
"README.md",
]
# [tool.poetry.scripts]
[tool.poetry.dependencies]
python = "^3.8"
# psycopg2-binary = "^2.9"
[tool.poetry.group.dev.dependencies]
black = "*"
pre-commit = "*"
isort = "*"
flake8 = "*"
#pytest = "^7.2"
#Sphinx = "^5.3.0"
#sphinx-rtd-theme = "^1.3.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
target-version = ['py38']
[tool.isort]
multi_line_output = 3
combine_as_imports = true
include_trailing_comma = true
force_grid_wrap = 3
ensure_newline_before_comments = true

@ -15,61 +15,52 @@
# $ pip uninstall app_skellington
from setuptools import setup
import os
__project__ = 'app_skellington'
__version__ = '0.1.1'
__description__ = 'A high-powered command line menu framework.'
from setuptools import setup
__project__ = "app_skellington"
__version__ = "0.1.1"
__description__ = "A high-powered command line menu framework."
long_description = __description__
readme_filepath = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'README.md'
)
with open(readme_filepath, encoding='utf-8') as fp:
readme_filepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md")
with open(readme_filepath, encoding="utf-8") as fp:
long_description = fp.read()
setup(
name = __project__,
version = __version__,
description = 'A high-powered command line menu framework.',
long_description = long_description,
author = 'Mathew Guest',
author_email = 't3h.zavage@gmail.com',
url = 'https://git-mirror.zavage.net/Mirror/app_skellington',
license = 'MIT',
python_requires = '>=3',
classifiers = [
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Framework :: Pytest',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
'Operating System :: MacOS',
'Operating System :: Microsoft',
'Operating System :: Microsoft :: Windows',
'Operating System :: OS Independent',
'Operating System :: POSIX',
'Operating System :: POSIX :: Linux',
'Topic :: Software Development :: Libraries',
'Topic :: Utilities'
name=__project__,
version=__version__,
description="A high-powered command line menu framework.",
long_description=long_description,
author="Mathew Guest",
author_email="mat@zavage.net",
url="https://git-mirror.zavage.net/zavage-software/app_skellington",
license="MIT",
python_requires=">=3",
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Framework :: Pytest",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: MacOS",
"Operating System :: Microsoft",
"Operating System :: Microsoft :: Windows",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
],
# Third-party dependencies; will be automatically installed
install_requires = (
'appdirs',
'configobj',
'colorlog',
install_requires=(
"appdirs",
"configobj",
"colorlog",
),
# Local packages to be installed (our packages)
packages = (
'app_skellington',
),
packages=("app_skellington",),
)

@ -1,45 +1,43 @@
from app_skellington.cfg import Config
from app_skellington import _util
import pytest
from app_skellington import _util
from app_skellington.cfg import Config
@pytest.fixture
def sample_configspec_filepath():
return _util.get_asset(__name__, 'sample_config.spec')
return _util.get_asset(__name__, "sample_config.spec")
@pytest.fixture
def sample_configini_filepath():
return _util.get_asset(__name__, 'sample_config.ini')
return _util.get_asset(__name__, "sample_config.ini")
@pytest.fixture
def sample_full_configspec_filepath():
return _util.get_asset(__name__, 'sample_config_full.spec')
return _util.get_asset(__name__, "sample_config_full.spec")
@pytest.fixture
def sample_full_configini_filepath():
return _util.get_asset(__name__, 'sample_config_full.ini')
return _util.get_asset(__name__, "sample_config_full.ini")
@pytest.fixture
def sample_invalid_configspec_filepath():
return _util.get_asset(__name__, 'sample_config_invalid.spec')
return _util.get_asset(__name__, "sample_config_invalid.spec")
class TestConfig_e2e:
def test_allows_reading_ini_and_no_spec(
self, sample_configini_filepath
):
cfg = Config(
configini_filepath=sample_configini_filepath
)
assert cfg['root_option'] == 'root_option_val', 'expecting default from config.spec (didnt get)'
assert cfg['app']['sub_option'] == 'sub_option_val', 'expecting default for sub option'
def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath):
cfg = Config(configini_filepath=sample_configini_filepath)
assert cfg["root_option"] == "root_option_val", "expecting default from config.spec (didnt get)"
assert cfg["app"]["sub_option"] == "sub_option_val", "expecting default for sub option"
def test_allows_reading_spec_and_no_ini(
self, sample_configspec_filepath
):
cfg = Config(
configspec_filepath=sample_configspec_filepath
)
assert cfg['root_option'] == 'def_string', 'expecting default from config.spec (didnt get)'
def test_allows_reading_spec_and_no_ini(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath)
assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
# NOTE(MG) Changed the functionality to not do it this way.
# def test_constructor_fails_with_invalid_spec(
@ -50,48 +48,35 @@ class TestConfig_e2e:
# configspec_filepath=sample_invalid_configspec_filepath
# )
def test_allows_options_beyond_spec(
self, sample_configspec_filepath
):
cfg = Config(
configspec_filepath=sample_configspec_filepath
)
cfg['foo'] = 'test my value'
assert cfg['foo'] == 'test my value'
def test_allows_options_beyond_spec(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath)
cfg["foo"] = "test my value"
assert cfg["foo"] == "test my value"
cfg['app']['bar'] = 'another value'
assert cfg['app']['bar'] == 'another value'
cfg["app"]["bar"] = "another value"
assert cfg["app"]["bar"] == "another value"
# def test_can_read_config_file_mutiple_times(self):
# pass
def test_can_override_config_file_manually(
self, sample_configini_filepath
):
cfg = Config(
configini_filepath=sample_configini_filepath
)
cfg['root_option'] = 'newval'
assert cfg['root_option'] == 'newval'
def test_can_override_config_file_manually(self, sample_configini_filepath):
cfg = Config(configini_filepath=sample_configini_filepath)
cfg["root_option"] = "newval"
assert cfg["root_option"] == "newval"
cfg['app']['sub_option'] = 'another_new_val'
assert cfg['app']['sub_option'] == 'another_new_val', 'expecting default for sub option'
cfg["app"]["sub_option"] = "another_new_val"
assert cfg["app"]["sub_option"] == "another_new_val", "expecting default for sub option"
def test_can_set_option_without_config(self):
cfg = Config()
cfg['foo'] = 'test my value'
assert cfg['foo'] == 'test my value'
cfg["foo"] = "test my value"
assert cfg["foo"] == "test my value"
cfg['app'] = {}
cfg['app']['bar'] = 'another value'
assert cfg['app']['bar'] == 'another value'
def test_uses_spec_as_defaults(
self, sample_configspec_filepath
):
cfg = Config(
configspec_filepath=sample_configspec_filepath
)
assert cfg['root_option'] == 'def_string', 'expecting default from config.spec (didnt get)'
assert cfg['app']['sub_option'] == 'def_sub', 'expecting default for sub option'
cfg["app"] = {}
cfg["app"]["bar"] = "another value"
assert cfg["app"]["bar"] == "another value"
def test_uses_spec_as_defaults(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath)
assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"

@ -1,8 +1,7 @@
from app_skellington.cli import CommandTree
class TestCli_e2e:
def test_null_constructor_works(self):
x = CommandTree()
assert True == True