Compare commits

..

10 Commits
v0.2.1 ... main

9 changed files with 111 additions and 42 deletions

@ -1 +1,2 @@
3.8.19
3.8.20
3.8.8

22
CHANGELOG.md Normal file

@ -0,0 +1,22 @@
# 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

@ -1,5 +1,9 @@
app_skellington
===============
---
gitea: none
include_toc: true
---
**app_skellington**
Application framework for Python, features include:
- Pain-free multi-level command menu: Expose public class methods as commands available to user.
@ -14,10 +18,26 @@ Principles:
- 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
# PyPi Hosted Link
# Python Package Index (PyPi) - Installation Instruction
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
@ -62,7 +82,7 @@ or
# Tests
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
Tests are a WIP and not fully built out yet. Recommendation is to run 'pytest' in the 'tests' directory.
# Development
@ -81,6 +101,7 @@ 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
Clone the repo:
```commandline
@ -135,8 +156,14 @@ twine upload dist/*
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.
@ -147,7 +174,7 @@ MIT no attribution required - https://opensource.org/license/mit-0
* Project page: https://zavage-software.com/portfolio/app_skellington
* Please report bugs, improvements, or feedback!
* Contact: mathew@zavage.net
* 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/

@ -1,14 +1,14 @@
# flake8: noqa
from .cfg import Config, EnvironmentVariables
from .app_container import (
DEFAULT_APP_AUTHOR,
DEFAULT_APP_NAME,
ApplicationContainer,
ApplicationContext,
NoCommandSpecified,
ServiceNotFound,
)
from .cfg import Config, EnvironmentVariables
from ._util import ServiceNotFound, NoCommandSpecified
from .cli import (
EXPLICIT_FAIL_ON_UNKNOWN_ARGS,
CommandEntry,

@ -124,3 +124,15 @@ def create_func(constructor, cls_method):
return cls_method(cmd_class_instance, *args, **kwargs)
return func
class ServiceNotFound(Exception):
"""
Application framework error: unable to find and inject dependency.
"""
pass
class NoCommandSpecified(Exception):
pass

@ -0,0 +1,20 @@
# 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")

@ -4,7 +4,10 @@ import os
import appdirs
from . import _util, cfg, cli, log
from ._util import ServiceNotFound, NoCommandSpecified, get_root_asset
from . import log
from .cfg import Config
from .cli import CommandTree
# Application scaffolding:
from ._bootstrap import _bootstrap_logger
@ -49,7 +52,7 @@ class ApplicationContainer:
# global state, configuration, loggers, and runtime args.
self._dependencies = {}
config = cfg.Config(configspec_filepath, configini_filepath)
config = Config(configspec_filepath, configini_filepath)
logger = log.LoggingLayer(self.appname, self.appauthor)
# Try and load logging configuration if provided
@ -67,7 +70,7 @@ class ApplicationContainer:
# Reference to context service avail. in root_app
self["ctx"] = lambda: self.ctx
self.cli = cli.CommandTree() # Command-line interface
self.cli = CommandTree() # Command-line interface
# Run methods if subclass implemented them:
if callable(getattr(self, "_cli_options", None)):
@ -144,7 +147,7 @@ class ApplicationContainer:
"""
Attempt to find config.spec inside the installed package directory.
"""
return _util.get_root_asset(configspec_filename)
return get_root_asset(configspec_filename)
def _inject_service_dependencies(self, constructor):
"""
@ -180,9 +183,6 @@ class ApplicationContainer:
except NoCommandSpecified:
print("Failure: No command specified.")
def interactive_shell(self):
pass
def invoke_from_cli(self):
self.invoke_command()
@ -191,13 +191,3 @@ class ApplicationContainer:
# Applications need a default usage
class ServiceNotFound(Exception):
"""
Application framework error: unable to find and inject dependency.
"""
pass
class NoCommandSpecified(Exception):
pass

@ -3,7 +3,7 @@ import inspect
import re
import sys
from . import app_container
from ._util import NoCommandSpecified
from ._bootstrap import _bootstrap_logger
# If explicit fail is enabled, any command with at least one unknown
@ -41,12 +41,12 @@ class CommandTree:
./scriptname --option="value" [submenu] [command]
is different than
is different from
./scriptname [submenu] [command] --option="value"
in that option is being applied to the application in the first example and
applied to the refresh_datasets command (under the nhsn command group) in
applied to the command (under the submenu command group) in
the second. In the same way the -h, --help options print different docs
depending on where the help option was passed.
"""
@ -82,17 +82,7 @@ class CommandTree:
Creates a root-level submenu with no entries. SubMenu node is
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}
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.
subparsers = self.root_parser.add_subparsers(**func_args)
@ -211,6 +201,12 @@ class CommandTree:
return None, None, False
def run_command(self, args=None):
"""
Args:
args (list[str]): Direct from STDIN
Raises:
NoCommandSpecified for invalid command.
"""
args, unk, success = self.parse(args)
if not success:
_bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help")
@ -226,7 +222,7 @@ class CommandTree:
cmd = self._lookup_command(args)
if cmd is None:
_bootstrap_logger.critical("cli - Failed to find command.")
return False
raise NoCommandSpecified
return self._invoke_command(cmd, args)
@ -284,7 +280,7 @@ class CommandTree:
# return self._invoke_command(lookup, args)
else:
raise app_container.NoCommandSpecified("No command specified.")
raise NoCommandSpecified("No command specified.")
def _invoke_command(self, cmd, args):
command_to_be_invoked = cmd.callback

@ -40,5 +40,6 @@ dev = [
]
[tool.setuptools_scm]
version_scheme = "guess-next-dev"
version_file = "app_skellington/_version.py"
version_scheme = "release-branch-semver"
local_scheme = "node-and-date"