3 Commits
17 changed files with 667 additions and 420 deletions
+10 -4
View File
@@ -17,10 +17,10 @@ repos:
args: ["--profile", "black", "--filter-files"] args: ["--profile", "black", "--filter-files"]
# Flake8 # Flake8
- repo: https://github.com/pycqa/flake8 #- repo: https://github.com/pycqa/flake8
rev: '7.0.0' # rev: '7.0.0'
hooks: # hooks:
- id: flake8 # - id: flake8
# Black # Black
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster # Using this mirror lets us use mypyc-compiled black, which is about 2x faster
@@ -29,3 +29,9 @@ repos:
hooks: hooks:
- id: black - id: black
language_version: python3.8 language_version: python3.8
# Poetry
- repo: https://github.com/python-poetry/poetry
rev: 1.8.2
hooks:
- id: poetry-lock
+1 -2
View File
@@ -1,2 +1 @@
3.8.20 3.8.19
3.8.8
-22
View File
@@ -1,22 +0,0 @@
# Changelog
## [0.2.2] (2020-07-19)
Second release is focused on cleanup, documentation.
* flake8, black, isort - all warnings and errors fixed.
* Documentation improved.
* Build and deploy process documented for developers.
* Poetry removed, replaced with modern pyproject.toml and setuptools.build_meta
for automatic version management.
## [0.1.0] (2020-07-19)
First release to PyPi.
* Basic functionality. Probably not the best code.
* Sub-menus supported, multi-level through CLI.
* Config through ConfigObj ini. Define spec file and input config.ini
* Colored Logging
* Services can be defined and provided to classes
+20 -15
View File
@@ -1,20 +1,25 @@
App Skellington App Skellington
MIT No Attribution
Copyright (c) 2024 Mathew Guest Copyright (c) 2024 Mathew Guest
Permission is hereby granted, free of charge, to any person obtaining a Permission is hereby granted, free of charge, to any person
copy of this software and associated documentation files (the obtaining a copy of this software and associated documentation
"Software"), to deal in the Software without restriction, including files (the "Software"), to deal in the Software without
without limitation the rights to use, copy, modify, merge, publish, restriction, including without limitation the rights to use,
distribute, sublicense, and/or sell copies of the Software, and to permit copy, modify, merge, publish, distribute, sublicense, and/or sell
persons to whom the Software is furnished to do so. copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
`NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+39 -123
View File
@@ -1,10 +1,5 @@
--- app_skellington
gitea: none ===============
include_toc: true
---
**app_skellington**
Application framework for Python, features include: Application framework for Python, features include:
- Pain-free multi-level command menu: Expose public class methods as commands available to user. - Pain-free multi-level command menu: Expose public class methods as commands available to user.
- Simple to define services and automatic dependency injection based on name (with custom invocation as an option). \*WIP - Simple to define services and automatic dependency injection based on name (with custom invocation as an option). \*WIP
@@ -18,28 +13,8 @@ Principles:
- Compatible 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 - Try to be compatible with alternate Python runtimes such as PyPy and older python environments. \*WIP
# Python Package Index (PyPi) - Installation Instruction Application Configuration
-------------------------
This is the project page for PyPi: The Python Package Index for community third-party libraries
https://pypi.org/project/app-skellington/
```
pip install app_skellington # install
pip uninstall app_skellington # uninstall
pip download app_skellington # download .whl wheel files for redistributable install
pip install -U app_skellington # upgrade
pip list # list install packages in environment
pip index version app_skellington # enumerate available distributions in pypi for package
```
# Description
This is a library. There is nothing to run by itself. It would be helpful to have a sample application that uses
it or something, but I don't have that ready at the moment.
# Application Configuration
Site configurations are supported through ConfigObj. There is a config.spec Site configurations are supported through ConfigObj. There is a config.spec
in the src directory which is a validation file; it contains the accepted in the src directory which is a validation file; it contains the accepted
parameter names, types, and limits for configurable options in the parameter names, types, and limits for configurable options in the
@@ -57,21 +32,23 @@ parameters are added into the config file.
Linux: Linux:
* /home/\<user\>/.config/\<app_name\>/config.ini /home/\<user\>/.config/\<app_name\>/config.ini
* /home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
/home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
Windows: Windows:
* C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\config.ini C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\config.ini
* C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\Logs\\\<app_name\>.log
C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\Logs\\\<app_name\>.log
Application configuration can be overridden ad-hoc through the --config <filename> Application configuration can be overridden ad-hoc through the --config <filename>
argument. argument.
# Debug - Turn on Logging Debug - Turn on Logging
-----------------------
Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns Set 'APPSKELLINGTON_DEBUG' environment variable to any value which turns
on AppSkellington-level logging. For example, on AppSkellington logger. For example,
APPSKELLINGTON_DEBUG=1 <executable> APPSKELLINGTON_DEBUG=1 <executable>
@@ -80,32 +57,23 @@ or
export APPSKELLINGTON_DEBUG=1 export APPSKELLINGTON_DEBUG=1
<executable> <executable>
# Tests Tests
-----
Tests are a WIP and not fully built out yet. Recommendation is to run 'pytest' in the 'tests' directory. Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
# Development
I recommend pyenv to install a reliable, controlled python of preferred version locally.
```
curl https://pyenv.run | bash
# Add to .bashrc or similar for different shells:
tee -a "$HOME"/.profile <<'EOF'
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
EOF
```
* reference https://github.com/pyenv/pyenv
* Use pyenv to install desired python version, and/or create any virtual environments you desire
Development
-----------
Clone the repo: Clone the repo:
```commandline ```commandline
git clone https://git-repos.zavage.net/zavage-software/app_skellington.git 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: Install pre-commit hooks:
@@ -113,68 +81,16 @@ Install pre-commit hooks:
pre-commit install pre-commit install
``` ```
Build: Begin development.
```
python -m build
```
Install: License
-------
I'm releasing this software under one of the most permissive
licenses, the MIT software license. This applies to this source repository
and all files within it. Alternatively, you are permitted you
to use any of this under the GPL to the fullest legal extent allowed.
``` Notes
pip install . -----
``` See official website: https://zavage-software.com
Please report bugs, improvements, or feedback!
Formatting and Linters:
```
black app_skellington
isort app_skellington
flake8 app_skellington
```
Publish:
```
# Push latest commit, or on commit ready to publish:
git push
# Create a tag with the desired version number and push:
git tag -a v0.2.0 -m "0.2.0 provides modern pyproject.toml build with setuptools, versioning, and publishing"
git push origin v0.2.0
# Build the wheel:
python -m build
# Publish to pypi:
twine check dist/*
twine upload dist/*
```
* Reference https://packaging.python.org/en/latest/overview/
# Version
setuptools_scm will infer the version based on the latest tag in your Git history.
Ensure you are tagging your commits with meaningful version numbers like v1.0.0, v1.1.0, etc.
You can view the current version number with the command:
python -m setuptools_scm
# License
See [license](LICENSE.txt)
MIT no attribution required - https://opensource.org/license/mit-0
* Allows commercial use.
* Allows modifications and closed-source derivatives.
* Fully interoperable with nearly all other open-source licenses, including GPL (when combined properly).
# See Also
* Project page: https://zavage-software.com/portfolio/app_skellington
* Please report bugs, improvements, or feedback!
* Contact: mat@zavage.net
* Packing and distribution conforms to PEP 621 https://peps.python.org/pep-0621/
* Reference https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/
+4 -19
View File
@@ -1,19 +1,4 @@
# flake8: noqa from .app_container import *
from .cfg import *
from .cfg import Config, EnvironmentVariables from .cli import *
from .log import *
from .app_container import (
DEFAULT_APP_AUTHOR,
DEFAULT_APP_NAME,
ApplicationContainer,
ApplicationContext,
)
from ._util import ServiceNotFound, NoCommandSpecified
from .cli import (
EXPLICIT_FAIL_ON_UNKNOWN_ARGS,
CommandEntry,
CommandTree,
HelpGenerator,
SubMenu,
)
from .log import LoggingLayer
+1 -3
View File
@@ -39,9 +39,7 @@ if os.environ.get("APPSKELLINGTON_DEBUG", None):
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(fmt) handler.setFormatter(fmt)
_bootstrap_logger.addHandler(handler) _bootstrap_logger.addHandler(handler)
_bootstrap_logger.debug( _bootstrap_logger.debug("log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.")
"log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging."
)
# Logging is by default off, excepting CRITICAL # Logging is by default off, excepting CRITICAL
else: else:
+2 -14
View File
@@ -30,7 +30,7 @@ def does_file_exist(filepath):
try: try:
open(filepath, "r") open(filepath, "r")
return True return True
except FileNotFoundError: except FileNotFoundError as ex:
return False return False
@@ -86,7 +86,7 @@ def get_asset(module, filepath):
root = os.path.realpath(root) root = os.path.realpath(root)
root = os.path.dirname(os.path.abspath(root)) root = os.path.dirname(os.path.abspath(root))
except Exception: except Exception as ex:
raise raise
path = os.path.join(root, filepath) path = os.path.join(root, filepath)
@@ -124,15 +124,3 @@ def create_func(constructor, cls_method):
return cls_method(cmd_class_instance, *args, **kwargs) return cls_method(cmd_class_instance, *args, **kwargs)
return func return func
class ServiceNotFound(Exception):
"""
Application framework error: unable to find and inject dependency.
"""
pass
class NoCommandSpecified(Exception):
pass
-20
View File
@@ -1,20 +0,0 @@
# file generated by setuptools-scm
# don't change, don't track in version control
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple, Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
VERSION_TUPLE = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
__version__ = version = "0.3.0.dev7+gd155176.d20250223"
__version_tuple__ = version_tuple = (0, 3, 0, "dev7", "gd155176.d20250223")
+33 -23
View File
@@ -4,10 +4,12 @@ import os
import appdirs import appdirs
from ._util import ServiceNotFound, NoCommandSpecified, get_root_asset from . import (
from . import log _util,
from .cfg import Config cfg,
from .cli import CommandTree cli,
log,
)
# Application scaffolding: # Application scaffolding:
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
@@ -42,9 +44,7 @@ class ApplicationContainer:
directories. directories.
""" """
def __init__( def __init__(self, configspec_filepath=None, configini_filepath=None, *args, **kwargs):
self, configspec_filepath=None, configini_filepath=None, *args, **kwargs
):
self.appname = kwargs.get("appname") or DEFAULT_APP_NAME self.appname = kwargs.get("appname") or DEFAULT_APP_NAME
self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR
@@ -52,7 +52,7 @@ class ApplicationContainer:
# global state, configuration, loggers, and runtime args. # global state, configuration, loggers, and runtime args.
self._dependencies = {} self._dependencies = {}
config = Config(configspec_filepath, configini_filepath) config = cfg.Config(configspec_filepath, configini_filepath)
logger = log.LoggingLayer(self.appname, self.appauthor) logger = log.LoggingLayer(self.appname, self.appauthor)
# Try and load logging configuration if provided # Try and load logging configuration if provided
@@ -70,7 +70,7 @@ class ApplicationContainer:
# Reference to context service avail. in root_app # Reference to context service avail. in root_app
self["ctx"] = lambda: self.ctx self["ctx"] = lambda: self.ctx
self.cli = CommandTree() # Command-line interface self.cli = cli.CommandTree() # Command-line interface
# Run methods if subclass implemented them: # Run methods if subclass implemented them:
if callable(getattr(self, "_cli_options", None)): if callable(getattr(self, "_cli_options", None)):
@@ -86,7 +86,7 @@ class ApplicationContainer:
""" """
try: try:
del self._dependencies[service_name] del self._dependencies[service_name]
except KeyError: except KeyError as ex:
pass pass
def __getitem__(self, service_name): def __getitem__(self, service_name):
@@ -97,13 +97,12 @@ class ApplicationContainer:
app['datalayer'] => returns the made-up "datalayer" service. app['datalayer'] => returns the made-up "datalayer" service.
""" """
try: try:
service_factory = self._dependencies[ service_factory = self._dependencies[service_name] # Retrieve factory function
service_name
] # Retrieve factory function
return service_factory() # Call factory() to return instance of service return service_factory() # Call factory() to return instance of service
except KeyError: 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.critical(msg)
_bootstrap_logger.debug(ex)
raise ServiceNotFound raise ServiceNotFound
def __setitem__(self, service_name, value): def __setitem__(self, service_name, value):
@@ -131,7 +130,7 @@ class ApplicationContainer:
dependencies.append(self[dep_name]) dependencies.append(self[dep_name])
return model_constructor(*dependencies) 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. Attempt to find config.ini in the user's config directory.
@@ -143,11 +142,11 @@ class ApplicationContainer:
_bootstrap_logger.info("default config filepath calculated to be: %s", filepath) _bootstrap_logger.info("default config filepath calculated to be: %s", filepath)
return 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. Attempt to find config.spec inside the installed package directory.
""" """
return get_root_asset(configspec_filename) return _util.get_root_asset(configspec_filename)
def _inject_service_dependencies(self, constructor): def _inject_service_dependencies(self, constructor):
""" """
@@ -159,14 +158,12 @@ class ApplicationContainer:
""" """
sig = inspect.signature(constructor.__init__) sig = inspect.signature(constructor.__init__)
params = sig.parameters params = sig.parameters
params = [ params = [params[paramname].name for paramname in params] # Convert Param() type => str
params[paramname].name for paramname in params
] # Convert Param() type => str
cls_dependencies = params[1:] # Skip 'self' parameter on class methods. cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
return functools.partial(self._construct_model, constructor, *cls_dependencies) return functools.partial(self._construct_model, constructor, *cls_dependencies)
def load_command(self): def load_command(self) -> bool:
args, unk, success = self.cli.parse() args, unk, success = self.cli.parse()
if not success: if not success:
return False return False
@@ -180,14 +177,27 @@ class ApplicationContainer:
return False return False
try: try:
self.cli.run_command() self.cli.run_command()
except NoCommandSpecified: except NoCommandSpecified as ex:
print("Failure: No command specified.") print("Failure: No command specified.")
def interactive_shell(self):
raise NotImplementedError()
def invoke_from_cli(self): def invoke_from_cli(self):
self.invoke_command() self.invoke_command()
def usage(self): def usage(self):
pass raise NotImplementedError()
# Applications need a default usage # Applications need a default usage
class ServiceNotFound(Exception):
"""
Application framework error: unable to find and inject dependency.
"""
pass
class NoCommandSpecified(Exception):
pass
+32 -41
View File
@@ -26,9 +26,7 @@ class Config:
"allow_options_beyond_spec": True, "allow_options_beyond_spec": True,
} }
def __init__( def __init__(self, configspec_filepath=None, configini_filepath=None, capabilities=None):
self, configspec_filepath=None, configini_filepath=None, capabilities=None
):
self._config_obj = None # must be type configobj.ConfigObj() self._config_obj = None # must be type configobj.ConfigObj()
self._configini_data = None self._configini_data = None
self._configini_filepath = None self._configini_filepath = None
@@ -90,25 +88,22 @@ class Config:
self._configspec_filepath = filepath self._configspec_filepath = filepath
self._configspec_data = data self._configspec_data = data
self._has_changed_internally = True self._has_changed_internally = True
_bootstrap_logger.debug( _bootstrap_logger.debug("cfg - Set configspec and read contents: %s", filepath)
"cfg - Set configspec and read contents: %s", filepath
)
self.load_config() self.load_config()
return return
except OSError: except OSError as ex:
_bootstrap_logger.critical( _bootstrap_logger.critical("cfg - Failed to find config.spec: file not found (%s)", filepath)
"cfg - Failed to find config.spec: file not found (%s)", filepath
)
raise OSError("Failed to read provided config.spec file") 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): def __contains__(self, key):
try: try:
has_item = key in self._config_obj has_item = key in self._config_obj
return has_item return has_item
except KeyError: except KeyError as ex:
pass _bootstrap_logger.error("failed to __containers__ on key (%s): %s", key, ex)
def __delitem__(self, key): def __delitem__(self, key):
""" """
@@ -117,8 +112,8 @@ class Config:
""" """
try: try:
del self[key] del self[key]
except KeyError: except KeyError as ex:
pass _bootstrap_logger.error("failed to __delitem__ on key (%s): %s", key, ex)
def __getitem__(self, key): def __getitem__(self, key):
""" """
@@ -131,7 +126,8 @@ class Config:
else: else:
# return self._config_obj[key].dict() # return self._config_obj[key].dict()
return self._config_obj[key] return self._config_obj[key]
except KeyError: except KeyError as ex:
_bootstrap_logger.error("failed to __getitem__ on key (%s): %s", key, ex)
raise raise
def __setitem__(self, key, value): def __setitem__(self, key, value):
@@ -151,10 +147,17 @@ class Config:
try: try:
v = self.__getitem__(key) v = self.__getitem__(key)
return v return v
except KeyError: except KeyError as ex:
_bootstrap_logger.error("failed to retrieve config key (%s): %s", key, ex)
return default 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: # Set new arguments if were passed in:
if configspec_filepath is not None: if configspec_filepath is not None:
self.configspec_filepath = configspec_filepath self.configspec_filepath = configspec_filepath
@@ -188,23 +191,20 @@ class Config:
# raise_errors # raise_errors
) )
_bootstrap_logger.debug( _bootstrap_logger.debug(
"cfg - Parsed configuration. config.spec = %s, config.ini = %s", "cfg - Parsed configuration. config.spec = %s, config.ini = %s", config_spec, config_ini
config_spec,
config_ini,
) )
return True return True
except configobj.ParseError: except configobj.ParseError as ex:
msg = f"cfg - Failed to load config: error in config.spec configuration: {self.configspec_filepath}" msg = "cfg - Failed to load config: error in config.spec configuration: {}".format(config_filepath)
_bootstrap_logger.error(msg) _bootstrap_logger.error(msg)
return False return False
except OSError: 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) _bootstrap_logger.error(msg)
return False return False
except Exception as ex: except Exception as ex:
_bootstrap_logger.error(ex) _bootstrap_logger.error(ex)
return False
def _validate_config_against_spec(self): def _validate_config_against_spec(self):
config_spec = self.configspec_filepath config_spec = self.configspec_filepath
@@ -221,9 +221,7 @@ class Config:
self._config_obj, configobj.ConfigObj self._config_obj, configobj.ConfigObj
), "expecting configobj.ConfigObj, received %s" % type(self._config_obj) ), "expecting configobj.ConfigObj, received %s" % type(self._config_obj)
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file # NOTE(MG) copy arg below instructs configobj to use defaults from spec file
test_results = self._config_obj.validate( test_results = self._config_obj.validate(val, copy=True, preserve_errors=True)
val, copy=True, preserve_errors=True
)
if test_results is True: if test_results is True:
_bootstrap_logger.info( _bootstrap_logger.info(
"cfg- Successfully validated configuration against spec. input = %s, validation spec = %s", "cfg- Successfully validated configuration against spec. input = %s, validation spec = %s",
@@ -233,27 +231,20 @@ class Config:
return True return True
elif test_results is False: elif test_results is False:
_bootstrap_logger.debug( _bootstrap_logger.debug("cfg - Potentially discovered invalid config.spec")
"cfg - Potentially discovered invalid config.spec"
)
else: else:
self._validate_parse_errors(test_results) self._validate_parse_errors(test_results)
return False return False
except ValueError: except ValueError as ex:
_bootstrap_logger.error( _bootstrap_logger.error("cfg - Failed while validating config against spec. ")
"cfg - Failed while validating config against spec. " _bootstrap_logger.debug(ex)
)
return False return False
def _validate_parse_errors(self, test_results): def _validate_parse_errors(self, test_results):
_bootstrap_logger.critical("cfg - Config file failed validation.") _bootstrap_logger.critical("cfg - Config file failed validation.")
for section_list, key, rslt in configobj.flatten_errors( for section_list, key, rslt in configobj.flatten_errors(self._config_obj, test_results):
self._config_obj, test_results _bootstrap_logger.critical("cfg - Config error info: %s %s %s", section_list, key, rslt)
):
_bootstrap_logger.critical(
"cfg - Config error info: %s %s %s", section_list, key, rslt
)
if key is not None: if key is not None:
_bootstrap_logger.critical( _bootstrap_logger.critical(
"cfg - Config failed validation: [%s].%s appears invalid. msg = %s", "cfg - Config failed validation: [%s].%s appears invalid. msg = %s",
+34 -56
View File
@@ -3,7 +3,7 @@ import inspect
import re import re
import sys import sys
from ._util import NoCommandSpecified from . import app_container
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
# If explicit fail is enabled, any command with at least one unknown # If explicit fail is enabled, any command with at least one unknown
@@ -41,12 +41,12 @@ class CommandTree:
./scriptname --option="value" [submenu] [command] ./scriptname --option="value" [submenu] [command]
is different from is different than
./scriptname [submenu] [command] --option="value" ./scriptname [submenu] [command] --option="value"
in that option is being applied to the application in the first example and in that option is being applied to the application in the first example and
applied to the command (under the submenu command group) in applied to the refresh_datasets command (under the nhsn command group) in
the second. In the same way the -h, --help options print different docs the second. In the same way the -h, --help options print different docs
depending on where the help option was passed. depending on where the help option was passed.
""" """
@@ -66,15 +66,13 @@ class CommandTree:
self._single_command = None self._single_command = None
def print_tree(self): def print_tree(self):
raise NotImplementedError raise NotImplementedError()
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
""" """
Adds an argument to the root parser. Adds an argument to the root parser.
""" """
_bootstrap_logger.info( _bootstrap_logger.info("adding argument to root parser: %s and %s", args, kwargs)
"adding argument to root parser: %s and %s", args, kwargs
)
self.root_parser.add_argument(*args, **kwargs) self.root_parser.add_argument(*args, **kwargs)
def init_submenu(self, param_name, is_required=False): def init_submenu(self, param_name, is_required=False):
@@ -82,7 +80,15 @@ class CommandTree:
Creates a root-level submenu with no entries. SubMenu node is Creates a root-level submenu with no entries. SubMenu node is
returned which can have submenus and commands attached to it. returned which can have submenus and commands attached to it.
""" """
# 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} 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"]
# END fix for Python<3.7
# Creates an argument as a slot in the underlying argparse. # 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)
@@ -91,17 +97,13 @@ class CommandTree:
submenu.submenu_path = "" submenu.submenu_path = ""
submenu.var_name = param_name submenu.var_name = param_name
_bootstrap_logger.info( _bootstrap_logger.info("Initialized root-level submenu: Parameter = '%s'", param_name)
"Initialized root-level submenu: Parameter = '%s'", param_name
)
self.entries[param_name] = submenu self.entries[param_name] = submenu
self.submenu_param = param_name self.submenu_param = param_name
return submenu return submenu
def register_command( def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
self, func, cmd_name=None, func_signature=None, docstring=None
):
""" """
When no submenu functionality is desired, this links a single When no submenu functionality is desired, this links a single
command into underlying argparse options. command into underlying argparse options.
@@ -131,13 +133,13 @@ class CommandTree:
# help is displayed next to the command in the submenu enumeration or # help is displayed next to the command in the submenu enumeration or
# list of commands: # list of commands:
HelpGenerator.generate_help_from_sig(docstring) help_text = HelpGenerator.generate_help_from_sig(docstring)
# description is displayed when querying help for the specific command: # description is displayed when querying help for the specific command:
HelpGenerator.generate_description_from_sig(docstring) description_text = HelpGenerator.generate_description_from_sig(docstring)
# end copy-paste from SubMenu.register_command # end copy-paste from SubMenu.register_command
# begin copy-paste then edited from SubMenu.register_command # begin copy-paste then editted from SubMenu.register_command
# For each parameter in the function create an argparse argument in # For each paramter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry: # the child ArgumentParser created for this menu entry:
for key in params: for key in params:
if key == "self": if key == "self":
@@ -149,9 +151,7 @@ class CommandTree:
helptext = "default provided" helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
self.root_parser.add_argument( self.root_parser.add_argument(key, help=helptext, nargs="?", default=param.default)
key, help=helptext, nargs="?", default=param.default
)
else: else:
helptext = "required" helptext = "required"
self.root_parser.add_argument(key, help=helptext) self.root_parser.add_argument(key, help=helptext)
@@ -166,7 +166,7 @@ class CommandTree:
registered_name = cmd_name registered_name = cmd_name
_bootstrap_logger.info("registered command: %s", registered_name) _bootstrap_logger.info("registered command: %s", registered_name)
# end copy-paste then edited from SubMenu.register_command # end copy-paste then editted from SubMenu.register_command
self._cmd_tree_is_single_command = True self._cmd_tree_is_single_command = True
self._single_command = cmd self._single_command = cmd
@@ -197,16 +197,10 @@ class CommandTree:
return pargs, unk, True return pargs, unk, True
# Note: SystemExit is raised when '-h' argument is supplied. # Note: SystemExit is raised when '-h' argument is supplied.
except SystemExit: except SystemExit as ex:
return None, None, False return None, None, False
def run_command(self, args=None): def run_command(self, args=None):
"""
Args:
args (list[str]): Direct from STDIN
Raises:
NoCommandSpecified for invalid command.
"""
args, unk, success = self.parse(args) args, unk, success = self.parse(args)
if not success: if not success:
_bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help") _bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help")
@@ -222,7 +216,7 @@ class CommandTree:
cmd = self._lookup_command(args) cmd = self._lookup_command(args)
if cmd is None: if cmd is None:
_bootstrap_logger.critical("cli - Failed to find command.") _bootstrap_logger.critical("cli - Failed to find command.")
raise NoCommandSpecified return False
return self._invoke_command(cmd, args) return self._invoke_command(cmd, args)
@@ -233,22 +227,16 @@ class CommandTree:
# the CommandTree with no SubMenu (submenu will be disabled # the CommandTree with no SubMenu (submenu will be disabled
# in this case): # in this case):
if self._cmd_tree_is_single_command: if self._cmd_tree_is_single_command:
assert ( assert self._cmd_tree_is_single_command is True, "corrupt data structure in CommandMenu"
self._cmd_tree_is_single_command is True
), "corrupt data structure in CommandMenu"
assert self._entries is None, "corrupt data structure in CommandMenu" assert self._entries is None, "corrupt data structure in CommandMenu"
assert isinstance( assert isinstance(self._single_command, CommandEntry), "corrupt data structure in CommandMenu"
self._single_command, CommandEntry
), "corrupt data structure in CommandMenu"
return self._single_command return self._single_command
# There is at least one submenu we need to go down: # There is at least one submenu we need to go down:
else: else:
assert self._single_command is None, "corrupt data structure in CommandMenu" assert self._single_command is None, "corrupt data structure in CommandMenu"
assert ( assert self._cmd_tree_is_single_command == False, "corrupt data structure in CommandMenu"
self._cmd_tree_is_single_command is False
), "corrupt data structure in CommandMenu"
# Key or variable name used by argparse to store the submenu options # 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
@@ -260,14 +248,10 @@ class CommandTree:
input("<broken>") input("<broken>")
val = args.get(argparse_param) val = args.get(argparse_param)
_bootstrap_logger.debug( _bootstrap_logger.debug("cli - argparse command is '{}' = {}".format(argparse_param, val))
"cli - argparse command is '{}' = {}".format(argparse_param, val)
)
lookup = submenu.entries.get(val) lookup = submenu.entries.get(val)
_bootstrap_logger.debug( _bootstrap_logger.debug("cli - lookup, entries[{}] = {}".format(val, lookup))
"cli - lookup, entries[{}] = {}".format(val, lookup)
)
# pop value # pop value
del args[argparse_param] del args[argparse_param]
@@ -280,7 +264,7 @@ class CommandTree:
# return self._invoke_command(lookup, args) # return self._invoke_command(lookup, args)
else: else:
raise NoCommandSpecified("No command specified.") raise app_container.NoCommandSpecified("No command specified.")
def _invoke_command(self, cmd, args): def _invoke_command(self, cmd, args):
command_to_be_invoked = cmd.callback command_to_be_invoked = cmd.callback
@@ -310,9 +294,7 @@ class SubMenu:
self.entries = {} self.entries = {}
def register_command( def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
self, func, cmd_name=None, func_signature=None, docstring=None
):
""" """
Registers a command as an entry in this submenu. Provided function is Registers a command as an entry in this submenu. Provided function is
converted into argparse arguments and made available to the user. converted into argparse arguments and made available to the user.
@@ -386,9 +368,7 @@ class SubMenu:
helptext = "default provided" helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
child_node.add_argument( child_node.add_argument(key, help=helptext, nargs="?", default=param.default)
key, help=helptext, nargs="?", default=param.default
)
else: else:
helptext = "required" helptext = "required"
child_node.add_argument(key, help=helptext) child_node.add_argument(key, help=helptext)
@@ -439,9 +419,7 @@ class SubMenu:
func_args = {"dest": var_name, "metavar": var_name, "required": is_required} func_args = {"dest": var_name, "metavar": var_name, "required": is_required}
if sys.version_info.major == 3 and sys.version_info.minor < 7: if sys.version_info.major == 3 and sys.version_info.minor < 7:
if is_required: if is_required:
_bootstrap_logger.warn( _bootstrap_logger.warn("Unable to enforce required submenu: Requires >= Python 3.7")
"Unable to enforce required submenu: Requires >= Python 3.7"
)
del func_args["required"] del func_args["required"]
# END fix for Python<3.7 # END fix for Python<3.7
@@ -503,7 +481,7 @@ class HelpGenerator:
The 'help' text is displayed next to the command when enumerating The 'help' text is displayed next to the command when enumerating
the submenu commands. the submenu commands.
""" """
if doctext is None: if doctext == None:
return doctext return doctext
regex = "(.*?)[.?!]" regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL) match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
@@ -517,7 +495,7 @@ class HelpGenerator:
The 'description' paragraph is provided when the user requests help The 'description' paragraph is provided when the user requests help
on a specific command. on a specific command.
""" """
if doctext is None: if doctext == None:
return doctext return doctext
regex = "(.*?)[.?!]" regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL) match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
+8 -26
View File
@@ -15,13 +15,7 @@ DEFAULT_LOG_SETTINGS = {
"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": { "handlers": {"stderr": {"class": "logging.StreamHandler", "level": "debug", "formatter": "colored"}},
"stderr": {
"class": "logging.StreamHandler",
"level": "debug",
"formatter": "colored",
}
},
"loggers": { "loggers": {
"root": { "root": {
"handlers": [ "handlers": [
@@ -70,9 +64,7 @@ class LoggingLayer:
noise for typical operation. noise for typical operation.
""" """
if config_dict is None: if config_dict is None:
_bootstrap_logger.debug( _bootstrap_logger.debug("log - No application logging configuration provided. Using default")
"log - No application logging configuration provided. Using default"
)
config_dict = DEFAULT_LOG_SETTINGS config_dict = DEFAULT_LOG_SETTINGS
self.transform_config(config_dict) self.transform_config(config_dict)
@@ -114,27 +106,20 @@ class LoggingLayer:
if config_dict["loggers"].get("root") is not None: if config_dict["loggers"].get("root") is not None:
config_dict["loggers"][""] = config_dict["loggers"]["root"] config_dict["loggers"][""] = config_dict["loggers"]["root"]
del config_dict["loggers"]["root"] del config_dict["loggers"]["root"]
except Exception: except Exception as ex:
_bootstrap_logger.warn( _bootstrap_logger.warn("was not able to find and patch root logger configuration from arguments")
"was not able to find and patch root logger configuration from arguments"
)
# Evaluate the full filepath of the file handler # Evaluate the full filepath of the file handler
if "file" not in config_dict["handlers"]: if "file" not in config_dict["handlers"]:
return return
if ( if os.path.abspath(config_dict["handlers"]["file"]["filename"]) == config_dict["handlers"]["file"]["filename"]:
os.path.abspath(config_dict["handlers"]["file"]["filename"])
== config_dict["handlers"]["file"]["filename"]
):
# Path is already absolute # Path is already absolute
pass pass
else: else:
dirname = appdirs.user_log_dir(self.appname, self.appauthor) dirname = appdirs.user_log_dir(self.appname, self.appauthor)
_util.ensure_dir_exists(dirname) _util.ensure_dir_exists(dirname)
log_filepath = os.path.join( log_filepath = os.path.join(dirname, config_dict["handlers"]["file"]["filename"])
dirname, config_dict["handlers"]["file"]["filename"]
)
config_dict["handlers"]["file"]["filename"] = log_filepath config_dict["handlers"]["file"]["filename"] = log_filepath
def _add_own_logconfig(self, config_dict): def _add_own_logconfig(self, config_dict):
@@ -147,10 +132,7 @@ class LoggingLayer:
# See _bootstrap.py # See _bootstrap.py
if os.environ.get("APPSKELLINGTON_DEBUG", None): if os.environ.get("APPSKELLINGTON_DEBUG", None):
if _logger_name not in config_dict["loggers"]: if _logger_name not in config_dict["loggers"]:
config_dict["loggers"][_logger_name] = { config_dict["loggers"][_logger_name] = {"level": "debug", "propagate": "false"}
"level": "debug",
"propagate": "false",
}
else: else:
config_dict["loggers"][_logger_name]["level"] = "debug" config_dict["loggers"][_logger_name]["level"] = "debug"
@@ -168,7 +150,7 @@ class LoggingLayer:
""" """
try: try:
s = dict_[key] s = dict_[key]
except KeyError: except KeyError as ex:
raise raise
if s == "critical": if s == "critical":
dict_[key] = logging.CRITICAL dict_[key] = logging.CRITICAL
Generated
+371
View 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"
+41 -37
View File
@@ -1,45 +1,49 @@
[build-system] [tool.poetry]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "app_skellington" name = "app_skellington"
dynamic = ["version"] version = "0.1.0"
license = {file = "LICENSE.txt"}
readme = "README.md"
description = "app_skellington CLI framework" description = "app_skellington CLI framework"
requires-python = ">=3.8"
dependencies = [
"appdirs",
"configobj",
"colorlog"
]
authors = [ authors = [
{name = "Mathew Guest", email = "mat@zavage.net"} "Mathew Guest <mathew.guest@davita.com>",
] ]
keywords = ["cli", "logging", "application"] license = "Creative Commons"
classifiers = [ readme = "README.md"
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
[project.urls]
homepage = "https://zavage-software.com/portfolio/app_skellington" homepage = "https://zavage-software.com/portfolio/app_skellington"
repository = "https://git-repos.zavage.net/Zavage-Software/app_skellington" repository = "https://git-mirror.zavage.net/zavage-software/app_skellington"
documentation = "https://git-repos.zavage.net/Zavage-Software/app_skellington" documentation = "https://git-mirror.zavage.net/zavage-software/app_skellington"
keywords = [""]
packages = [{ include = "app_skellington" }]
[project.optional-dependencies] include = [
dev = [ "README.md",
"black",
"pre-commit",
"isort",
"flake8"
] ]
[tool.setuptools_scm] # [tool.poetry.scripts]
version_file = "app_skellington/_version.py"
version_scheme = "release-branch-semver"
local_scheme = "node-and-date" [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
Executable
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python
#
# Usage:
#
# First, enable the python environment you want to install to, or if installing
# system-wide then ensure you're logged in with sufficient permissions
# (admin or root to install to system directories)
#
# installation:
#
# $ ./setup.py install
#
# de-installation:
#
# $ pip uninstall app_skellington
import os
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:
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="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",
),
# Local packages to be installed (our packages)
packages=("app_skellington",),
)
+5 -15
View File
@@ -32,18 +32,12 @@ def sample_invalid_configspec_filepath():
class TestConfig_e2e: class TestConfig_e2e:
def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath): def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath):
cfg = Config(configini_filepath=sample_configini_filepath) cfg = Config(configini_filepath=sample_configini_filepath)
assert ( assert cfg["root_option"] == "root_option_val", "expecting default from config.spec (didnt get)"
cfg["root_option"] == "root_option_val" assert cfg["app"]["sub_option"] == "sub_option_val", "expecting default for sub option"
), "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): def test_allows_reading_spec_and_no_ini(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath) cfg = Config(configspec_filepath=sample_configspec_filepath)
assert ( assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
cfg["root_option"] == "def_string"
), "expecting default from config.spec (didnt get)"
# NOTE(MG) Changed the functionality to not do it this way. # NOTE(MG) Changed the functionality to not do it this way.
# def test_constructor_fails_with_invalid_spec( # def test_constructor_fails_with_invalid_spec(
@@ -71,9 +65,7 @@ class TestConfig_e2e:
assert cfg["root_option"] == "newval" assert cfg["root_option"] == "newval"
cfg["app"]["sub_option"] = "another_new_val" cfg["app"]["sub_option"] = "another_new_val"
assert ( assert cfg["app"]["sub_option"] == "another_new_val", "expecting default for sub option"
cfg["app"]["sub_option"] == "another_new_val"
), "expecting default for sub option"
def test_can_set_option_without_config(self): def test_can_set_option_without_config(self):
cfg = Config() cfg = Config()
@@ -86,7 +78,5 @@ class TestConfig_e2e:
def test_uses_spec_as_defaults(self, sample_configspec_filepath): def test_uses_spec_as_defaults(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath) cfg = Config(configspec_filepath=sample_configspec_filepath)
assert ( assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
cfg["root_option"] == "def_string"
), "expecting default from config.spec (didnt get)"
assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"