acbox 0.0.0b0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acbox-0.0.0b0/PKG-INFO +40 -0
- acbox-0.0.0b0/README.md +17 -0
- acbox-0.0.0b0/pyproject.toml +116 -0
- acbox-0.0.0b0/setup.cfg +4 -0
- acbox-0.0.0b0/src/acbox/__init__.py +2 -0
- acbox-0.0.0b0/src/acbox/cli/__init__.py +0 -0
- acbox-0.0.0b0/src/acbox/cli/flags/__init__.py +0 -0
- acbox-0.0.0b0/src/acbox/cli/flags/base.py +44 -0
- acbox-0.0.0b0/src/acbox/cli/flags/logging.py +34 -0
- acbox-0.0.0b0/src/acbox/cli/parsers/__init__.py +0 -0
- acbox-0.0.0b0/src/acbox/cli/parsers/base.py +68 -0
- acbox-0.0.0b0/src/acbox/cli/parsers/parser.py +1 -0
- acbox-0.0.0b0/src/acbox/cli/parsers/simple.py +47 -0
- acbox-0.0.0b0/src/acbox/cli/script.py +125 -0
- acbox-0.0.0b0/src/acbox/cli/shared.py +34 -0
- acbox-0.0.0b0/src/acbox/cli2.py +59 -0
- acbox-0.0.0b0/src/acbox/dictionaries.py +39 -0
- acbox-0.0.0b0/src/acbox/fileops.py +62 -0
- acbox-0.0.0b0/src/acbox/git.py +34 -0
- acbox-0.0.0b0/src/acbox/loader.py +52 -0
- acbox-0.0.0b0/src/acbox/maincli.py +8 -0
- acbox-0.0.0b0/src/acbox/packer.py +25 -0
- acbox-0.0.0b0/src/acbox/run1.py +141 -0
- acbox-0.0.0b0/src/acbox/runner.py +86 -0
- acbox-0.0.0b0/src/acbox/ureporting.py +135 -0
- acbox-0.0.0b0/src/acbox/utils.py +41 -0
- acbox-0.0.0b0/src/acbox.egg-info/PKG-INFO +40 -0
- acbox-0.0.0b0/src/acbox.egg-info/SOURCES.txt +40 -0
- acbox-0.0.0b0/src/acbox.egg-info/dependency_links.txt +1 -0
- acbox-0.0.0b0/src/acbox.egg-info/entry_points.txt +2 -0
- acbox-0.0.0b0/src/acbox.egg-info/requires.txt +6 -0
- acbox-0.0.0b0/src/acbox.egg-info/top_level.txt +1 -0
- acbox-0.0.0b0/tests/test_cli.py +65 -0
- acbox-0.0.0b0/tests/test_cli_shared.py +176 -0
- acbox-0.0.0b0/tests/test_dictionaries.py +109 -0
- acbox-0.0.0b0/tests/test_fileops.py +18 -0
- acbox-0.0.0b0/tests/test_internals.py +32 -0
- acbox-0.0.0b0/tests/test_loader.py +19 -0
- acbox-0.0.0b0/tests/test_run1.py +2 -0
- acbox-0.0.0b0/tests/test_runner.py +39 -0
- acbox-0.0.0b0/tests/test_services_git.py +18 -0
- acbox-0.0.0b0/tests/test_utils.py +27 -0
acbox-0.0.0b0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acbox
|
|
3
|
+
Version: 0.0.0b0
|
|
4
|
+
Summary: Collection of small tools
|
|
5
|
+
Author-email: Antonio Cavallo <a.cavallo@cavallinux.eu>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/cav71/acbox
|
|
8
|
+
Project-URL: Issues, https://github.com/cav71/acbox
|
|
9
|
+
Project-URL: Documentation, https://github.com/cav71/acbox
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: click
|
|
19
|
+
Requires-Dist: filetype
|
|
20
|
+
Requires-Dist: rich
|
|
21
|
+
Requires-Dist: pyyaml
|
|
22
|
+
Provides-Extra: extra
|
|
23
|
+
|
|
24
|
+
## ACBox my little toolbox
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Development
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
python3.13 -m venv .venv
|
|
31
|
+
source .venv/bin/activate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
python -m pip install --upgrade pip
|
|
36
|
+
python -m pip install --group dev
|
|
37
|
+
pre-commit install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Ready.
|
acbox-0.0.0b0/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "acbox"
|
|
7
|
+
version = "0.0.0b0"
|
|
8
|
+
description = "Collection of small tools"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
|
|
12
|
+
requires-python = ">= 3.10"
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Antonio Cavallo", email = "a.cavallo@cavallinux.eu" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"click",
|
|
29
|
+
"filetype",
|
|
30
|
+
"rich",
|
|
31
|
+
"pyyaml",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"build",
|
|
37
|
+
"lxml",
|
|
38
|
+
"mypy",
|
|
39
|
+
"pre-commit",
|
|
40
|
+
"pytest",
|
|
41
|
+
"pytest-asyncio",
|
|
42
|
+
"pytest-cov",
|
|
43
|
+
"pytest-html",
|
|
44
|
+
"ruff",
|
|
45
|
+
"types-python-dateutil",
|
|
46
|
+
"types-pyyaml"
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
extra = []
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Source = "https://github.com/cav71/acbox"
|
|
55
|
+
Issues = "https://github.com/cav71/acbox"
|
|
56
|
+
Documentation = "https://github.com/cav71/acbox"
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
acbox = "acbox.maincli:main"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
namespaces = false
|
|
64
|
+
|
|
65
|
+
[tool.ruff]
|
|
66
|
+
target-version = "py39"
|
|
67
|
+
line-length = 140
|
|
68
|
+
src = ["src/acbox"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.format]
|
|
71
|
+
quote-style = "double"
|
|
72
|
+
|
|
73
|
+
[tool.ruff.lint]
|
|
74
|
+
ignore = []
|
|
75
|
+
select = ["F", "E", "W", "Q", "I001"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint.isort]
|
|
78
|
+
known-first-party = ["acbox"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
disallow_untyped_defs = false
|
|
83
|
+
follow_imports = "normal"
|
|
84
|
+
ignore_missing_imports = true
|
|
85
|
+
pretty = true
|
|
86
|
+
show_column_numbers = true
|
|
87
|
+
show_error_codes = true
|
|
88
|
+
warn_no_return = false
|
|
89
|
+
warn_unused_ignores = true
|
|
90
|
+
exclude = [
|
|
91
|
+
"docs/conf\\.py",
|
|
92
|
+
"^docs/\\.*",
|
|
93
|
+
"^build/\\.*",
|
|
94
|
+
"^support/\\.*",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.coverage.run]
|
|
98
|
+
branch = true
|
|
99
|
+
|
|
100
|
+
[tool.coverage.paths]
|
|
101
|
+
source = [
|
|
102
|
+
"src/",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
[tool.coverage.report]
|
|
106
|
+
exclude_lines = [
|
|
107
|
+
"no cov",
|
|
108
|
+
"if __name__ == .__main__.:",
|
|
109
|
+
"if TYPE_CHECKING:",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
[tool.pytest.ini_options]
|
|
113
|
+
markers = [
|
|
114
|
+
"manual: marks tests unsafe for auto-run (eg. better run them manually)",
|
|
115
|
+
]
|
|
116
|
+
asyncio_default_fixture_loop_scope = "function"
|
acbox-0.0.0b0/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ArgumentTypeBase:
|
|
5
|
+
class _NA:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
def __init__(self, *args, **kwargs):
|
|
9
|
+
self.args = args
|
|
10
|
+
self.kwargs = kwargs
|
|
11
|
+
self._default = self._NA
|
|
12
|
+
self.__name__ = self.__class__.__name__
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def default(self):
|
|
16
|
+
return self._default
|
|
17
|
+
|
|
18
|
+
@default.setter
|
|
19
|
+
def default(self, value):
|
|
20
|
+
if value is ArgumentTypeBase._NA:
|
|
21
|
+
self._default = ArgumentTypeBase._NA
|
|
22
|
+
else:
|
|
23
|
+
self._default = self._validate(value)
|
|
24
|
+
return self._default
|
|
25
|
+
|
|
26
|
+
def __call__(self, txt):
|
|
27
|
+
self._value = None
|
|
28
|
+
self._value = self._validate(txt)
|
|
29
|
+
return self
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def value(self):
|
|
33
|
+
return getattr(self, "_value", self.default)
|
|
34
|
+
|
|
35
|
+
def _validate(self, value):
|
|
36
|
+
try:
|
|
37
|
+
return self.validate(value)
|
|
38
|
+
except argparse.ArgumentTypeError as exc:
|
|
39
|
+
if not hasattr(self, "_value"):
|
|
40
|
+
raise RuntimeError(f"cannot use {value=} as default: {exc.args[0]}")
|
|
41
|
+
raise
|
|
42
|
+
|
|
43
|
+
def validate(self, txt):
|
|
44
|
+
raise NotImplementedError("need to implement the .validate(self, txt) method")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..parsers.base import ArgumentParserBase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_arguments_logging(parser: ArgumentParserBase, baselevel: int = logging.INFO) -> None:
|
|
9
|
+
group = parser.add_argument_group("Logging", "Logging related options")
|
|
10
|
+
group.add_argument("-v", "--verbose", dest="managed-loglevel", action="append_const", const=1, help="report verbose logging")
|
|
11
|
+
group.add_argument("-q", "--quiet", dest="managed-loglevel", action="append_const", const=-1, help="report quiet logging")
|
|
12
|
+
|
|
13
|
+
levelmap = [
|
|
14
|
+
logging.DEBUG,
|
|
15
|
+
logging.INFO,
|
|
16
|
+
logging.WARNING,
|
|
17
|
+
logging.ERROR,
|
|
18
|
+
logging.CRITICAL,
|
|
19
|
+
logging.FATAL,
|
|
20
|
+
]
|
|
21
|
+
if baselevel not in levelmap:
|
|
22
|
+
raise IndexError(f"cannot find level {baselevel} in: {levelmap}")
|
|
23
|
+
|
|
24
|
+
def setup_logging(config: dict[str, Any]) -> None:
|
|
25
|
+
logging.basicConfig(**config)
|
|
26
|
+
|
|
27
|
+
def callback(args: argparse.Namespace):
|
|
28
|
+
config = {}
|
|
29
|
+
count = levelmap.index(baselevel) - sum(getattr(args, "managed-loglevel") or [0])
|
|
30
|
+
config["level"] = levelmap[min(max(count, 0), len(levelmap) - 1)]
|
|
31
|
+
setup_logging(config)
|
|
32
|
+
delattr(args, "managed-loglevel")
|
|
33
|
+
|
|
34
|
+
parser.callbacks.append(callback)
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
from ..flags.base import ArgumentTypeBase
|
|
7
|
+
from ..shared import ArgsCallback, check_default_constructor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArgumentParserBase(argparse.ArgumentParser):
|
|
11
|
+
def __init__(self, modules: list[types.ModuleType], *args, **kwargs):
|
|
12
|
+
super().__init__(*args, **kwargs)
|
|
13
|
+
self.modules = modules
|
|
14
|
+
self.callbacks: list[ArgsCallback | None] = []
|
|
15
|
+
|
|
16
|
+
def _post_process(self, options):
|
|
17
|
+
for name in dir(options):
|
|
18
|
+
if isinstance(getattr(options, name), ArgumentTypeBase):
|
|
19
|
+
fallback = getattr(options, name).value
|
|
20
|
+
setattr(
|
|
21
|
+
options,
|
|
22
|
+
name,
|
|
23
|
+
None if fallback is ArgumentTypeBase._NA else fallback,
|
|
24
|
+
)
|
|
25
|
+
for callback in self.callbacks:
|
|
26
|
+
options = callback(options) or options
|
|
27
|
+
return options
|
|
28
|
+
|
|
29
|
+
def parse_known_args(self, args=None, namespace=None):
|
|
30
|
+
options, argv = super().parse_known_args(args, namespace)
|
|
31
|
+
return self._post_process(options), argv
|
|
32
|
+
|
|
33
|
+
def parse_args(self, args=None, namespace=None):
|
|
34
|
+
options = super().parse_args(args, namespace)
|
|
35
|
+
for name in dir(options):
|
|
36
|
+
if isinstance(getattr(options, name), ArgumentTypeBase):
|
|
37
|
+
fallback = getattr(options, name).value
|
|
38
|
+
setattr(
|
|
39
|
+
options,
|
|
40
|
+
name,
|
|
41
|
+
None if fallback is ArgumentTypeBase._NA else fallback,
|
|
42
|
+
)
|
|
43
|
+
return options
|
|
44
|
+
|
|
45
|
+
def add_argument(self, *args, **kwargs):
|
|
46
|
+
typ = kwargs.get("type")
|
|
47
|
+
obj = None
|
|
48
|
+
if isinstance(typ, type) and issubclass(typ, ArgumentTypeBase):
|
|
49
|
+
check_default_constructor(typ)
|
|
50
|
+
obj = typ()
|
|
51
|
+
if isinstance(typ, ArgumentTypeBase):
|
|
52
|
+
obj = typ
|
|
53
|
+
if obj is not None:
|
|
54
|
+
obj.default = kwargs.get("default", ArgumentTypeBase._NA)
|
|
55
|
+
kwargs["default"] = obj
|
|
56
|
+
kwargs["type"] = obj
|
|
57
|
+
super().add_argument(*args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def error(self, message):
|
|
60
|
+
try:
|
|
61
|
+
super().error(message)
|
|
62
|
+
except SystemExit:
|
|
63
|
+
# gh-121018
|
|
64
|
+
raise argparse.ArgumentError(None, message)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def get_parser(cls, modules: list[types.ModuleType], **kwargs):
|
|
68
|
+
raise NotImplementedError("implement this method")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
|
|
6
|
+
from .base import ArgumentParserBase
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def log_sys_info(modules: list[types.ModuleType]):
|
|
12
|
+
log.debug("interpreter: %s", sys.executable)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ArgumentParser(ArgumentParserBase):
|
|
16
|
+
def parse_args(self, args=None, namespace=None):
|
|
17
|
+
options = super().parse_args(args, namespace)
|
|
18
|
+
|
|
19
|
+
# reserver attributes
|
|
20
|
+
for reserved in [
|
|
21
|
+
"modules",
|
|
22
|
+
"error",
|
|
23
|
+
]:
|
|
24
|
+
if not hasattr(options, reserved):
|
|
25
|
+
continue
|
|
26
|
+
raise RuntimeError(f"cannot add an argument with dest='{reserved}'")
|
|
27
|
+
options.error = self.error
|
|
28
|
+
options.modules = self.modules
|
|
29
|
+
|
|
30
|
+
for callback in self.callbacks:
|
|
31
|
+
if not callback:
|
|
32
|
+
continue
|
|
33
|
+
options = callback(options) or options
|
|
34
|
+
|
|
35
|
+
log_sys_info(self.modules)
|
|
36
|
+
return options
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_parser(cls, modules: list[types.ModuleType], **kwargs):
|
|
40
|
+
class Formatter(
|
|
41
|
+
argparse.RawTextHelpFormatter,
|
|
42
|
+
argparse.RawDescriptionHelpFormatter,
|
|
43
|
+
argparse.ArgumentDefaultsHelpFormatter,
|
|
44
|
+
):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return cls(modules, formatter_class=Formatter, **kwargs)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from .parsers.base import ArgumentParserBase
|
|
13
|
+
from .parsers.simple import ArgumentParser
|
|
14
|
+
from .shared import AbortCliError, AbortWrongArgumentError
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextlib.contextmanager
|
|
20
|
+
def setup(
|
|
21
|
+
function: Callable,
|
|
22
|
+
add_arguments: (Callable[[ArgumentParserBase], None] | Callable[[argparse.ArgumentParser], None] | None) = None,
|
|
23
|
+
process_args: (Callable[[argparse.Namespace], argparse.Namespace | None] | None) = None,
|
|
24
|
+
):
|
|
25
|
+
sig = inspect.signature(function)
|
|
26
|
+
module = inspect.getmodule(function)
|
|
27
|
+
|
|
28
|
+
# the cli decorated function might have two special parameters:
|
|
29
|
+
# - args this will receive non-parsed arguments (eg from nargs="*")
|
|
30
|
+
# - parser escape hatch to the parser object (probably never used)
|
|
31
|
+
if "args" in sig.parameters and "parser" in sig.parameters:
|
|
32
|
+
raise RuntimeError(f"function '{module}.{function.__name__}' cannot take args and parser at the same time")
|
|
33
|
+
|
|
34
|
+
# doc is taken from the function itself or the containing module
|
|
35
|
+
description, _, epilog = (function.__doc__ or module.__doc__ or "").strip().partition("\n")
|
|
36
|
+
epilog = f"{description}\n{'-' * len(description)}\n{epilog}"
|
|
37
|
+
description = ""
|
|
38
|
+
|
|
39
|
+
# extract parser info/fallbacks from all these modules
|
|
40
|
+
modules = [
|
|
41
|
+
sys.modules[__name__],
|
|
42
|
+
]
|
|
43
|
+
if module:
|
|
44
|
+
modules.append(module)
|
|
45
|
+
|
|
46
|
+
parser = ArgumentParser.get_parser(modules, description=description, epilog=epilog)
|
|
47
|
+
if add_arguments and (callbacks := add_arguments(parser)):
|
|
48
|
+
if isinstance(callbacks, list):
|
|
49
|
+
parser.callbacks.extend(callbacks)
|
|
50
|
+
else:
|
|
51
|
+
parser.callbacks.append(callbacks)
|
|
52
|
+
|
|
53
|
+
kwargs = {}
|
|
54
|
+
if "parser" in sig.parameters:
|
|
55
|
+
kwargs["parser"] = parser
|
|
56
|
+
|
|
57
|
+
t0 = time.monotonic()
|
|
58
|
+
success = "completed"
|
|
59
|
+
errormsg = ""
|
|
60
|
+
show_timing = True
|
|
61
|
+
# breakpoint()
|
|
62
|
+
try:
|
|
63
|
+
if "parser" not in sig.parameters:
|
|
64
|
+
args = parser.parse_args()
|
|
65
|
+
if process_args:
|
|
66
|
+
args = process_args(args) or args
|
|
67
|
+
|
|
68
|
+
if "args" in sig.parameters:
|
|
69
|
+
kwargs["args"] = args
|
|
70
|
+
yield sig.bind(**kwargs)
|
|
71
|
+
|
|
72
|
+
except argparse.ArgumentError:
|
|
73
|
+
sys.exit(2)
|
|
74
|
+
pass
|
|
75
|
+
except AbortCliError as exc:
|
|
76
|
+
show_timing = False
|
|
77
|
+
if exc.args:
|
|
78
|
+
print(str(exc), file=sys.stderr)
|
|
79
|
+
sys.exit(2)
|
|
80
|
+
except AbortWrongArgumentError as exc:
|
|
81
|
+
show_timing = False
|
|
82
|
+
parser.print_usage(sys.stderr)
|
|
83
|
+
print(f"{parser.prog}: error: {exc.args[0]}", file=sys.stderr)
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
except SystemExit as exc:
|
|
86
|
+
show_timing = False
|
|
87
|
+
sys.exit(exc.code)
|
|
88
|
+
except Exception:
|
|
89
|
+
log.exception("un-handled exception")
|
|
90
|
+
success = "failed with an exception"
|
|
91
|
+
finally:
|
|
92
|
+
if show_timing:
|
|
93
|
+
delta = round(time.monotonic() - t0, 2)
|
|
94
|
+
log.debug("task %s in %.2fs", success, delta)
|
|
95
|
+
if errormsg:
|
|
96
|
+
parser.error(errormsg)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def cli(
|
|
100
|
+
add_arguments: (Callable[[ArgumentParserBase], Any] | Callable[[argparse.ArgumentParser], Any] | None) = None,
|
|
101
|
+
process_args: (Callable[[argparse.Namespace], argparse.Namespace | None] | None) = None,
|
|
102
|
+
):
|
|
103
|
+
def _cli1(function):
|
|
104
|
+
module = inspect.getmodule(function)
|
|
105
|
+
|
|
106
|
+
if inspect.iscoroutinefunction(function):
|
|
107
|
+
|
|
108
|
+
@functools.wraps(function)
|
|
109
|
+
async def _cli2(*args, **kwargs):
|
|
110
|
+
with setup(function, add_arguments, process_args) as ba:
|
|
111
|
+
return await function(*ba.args, **ba.kwargs)
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
|
|
115
|
+
@functools.wraps(function)
|
|
116
|
+
def _cli2(*args, **kwargs):
|
|
117
|
+
with setup(function, add_arguments, process_args) as ba:
|
|
118
|
+
return function(*ba.args, **ba.kwargs)
|
|
119
|
+
|
|
120
|
+
_cli2.attributes = {
|
|
121
|
+
"doc": function.__doc__ or module.__doc__ or "",
|
|
122
|
+
}
|
|
123
|
+
return _cli2
|
|
124
|
+
|
|
125
|
+
return _cli1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
# The return type of add_arguments
|
|
9
|
+
|
|
10
|
+
if sys.version_info >= (3, 10):
|
|
11
|
+
ArgsCallback = Callable[[argparse.Namespace], None | argparse.Namespace]
|
|
12
|
+
else:
|
|
13
|
+
ArgsCallback = Callable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CliBaseError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbortCliError(CliBaseError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AbortWrongArgumentError(CliBaseError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_default_constructor(klass: type):
|
|
29
|
+
signature = inspect.signature(klass.__init__) # type: ignore[misc]
|
|
30
|
+
for name, value in signature.parameters.items():
|
|
31
|
+
if name in {"self", "args", "kwargs"}:
|
|
32
|
+
continue
|
|
33
|
+
if value.default is inspect.Signature.empty:
|
|
34
|
+
raise RuntimeError(f"the {klass}() cannot be called without arguments")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from argparse import Namespace
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
TypeFn = Callable[[Namespace], None]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def add_loglevel(fn: TypeFn) -> TypeFn:
|
|
12
|
+
fn = click.option("-v", "--verbose", count=True)(fn)
|
|
13
|
+
fn = click.option("-q", "--quiet", count=True)(fn)
|
|
14
|
+
return fn
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def process_loglevel(options: Namespace, verbose_flag: bool = False) -> Namespace:
|
|
18
|
+
level = max(min(options.__dict__.pop("verbose") - options.__dict__.pop("quiet"), 1), -1)
|
|
19
|
+
|
|
20
|
+
# console = Console(theme=Theme({"log.time": "cyan"}))
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level={-1: logging.WARNING, 0: logging.INFO, 1: logging.DEBUG}[level],
|
|
23
|
+
# datefmt="[%X]",
|
|
24
|
+
# handlers=[RichHandler(console=console, rich_tracebacks=True)]
|
|
25
|
+
)
|
|
26
|
+
if verbose_flag:
|
|
27
|
+
options.verbose = level > 0
|
|
28
|
+
return options
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def clickwrapper(
|
|
32
|
+
add_arguments: Callable[[TypeFn], TypeFn] | None = None,
|
|
33
|
+
process_options: Callable[[Namespace], None | Namespace] | None = None,
|
|
34
|
+
verbose_flag: bool = False,
|
|
35
|
+
) -> Callable[[TypeFn], None]:
|
|
36
|
+
def _clickwrapper(fn: TypeFn):
|
|
37
|
+
fn = add_loglevel(fn)
|
|
38
|
+
if add_arguments:
|
|
39
|
+
fn = add_arguments(fn)
|
|
40
|
+
|
|
41
|
+
@functools.wraps(fn)
|
|
42
|
+
def __clickwrapper(*args, **kwargs):
|
|
43
|
+
options = Namespace(**kwargs)
|
|
44
|
+
options = process_loglevel(options, verbose_flag=verbose_flag) or options
|
|
45
|
+
if hasattr(options, "error"):
|
|
46
|
+
raise RuntimeError("you have an error option")
|
|
47
|
+
|
|
48
|
+
def error(msg):
|
|
49
|
+
raise click.UsageError(msg)
|
|
50
|
+
|
|
51
|
+
options.error = error
|
|
52
|
+
if process_options:
|
|
53
|
+
options = process_options(options) or options
|
|
54
|
+
|
|
55
|
+
return fn(options)
|
|
56
|
+
|
|
57
|
+
return __clickwrapper
|
|
58
|
+
|
|
59
|
+
return _clickwrapper
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, MutableMapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
DELETE = object()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def dictupdate(data: MutableMapping[str, Any], updates: Iterable[tuple[str, Any]]) -> list[tuple[str, Any]]:
|
|
10
|
+
undo = []
|
|
11
|
+
for path, value in updates:
|
|
12
|
+
cur = data
|
|
13
|
+
loc = path.split(".")
|
|
14
|
+
for i in range(len(loc) - 1):
|
|
15
|
+
if loc[i] not in cur:
|
|
16
|
+
cur[loc[i]] = dict()
|
|
17
|
+
undo.append((".".join(loc[: i + 1]), DELETE))
|
|
18
|
+
cur = cur[loc[i]]
|
|
19
|
+
|
|
20
|
+
orig = cur.get(loc[-1], DELETE)
|
|
21
|
+
undo.append((path, orig))
|
|
22
|
+
if value is DELETE:
|
|
23
|
+
del cur[loc[-1]]
|
|
24
|
+
else:
|
|
25
|
+
cur[loc[-1]] = value
|
|
26
|
+
|
|
27
|
+
return undo
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dictrollback(data: MutableMapping[str, Any], undo: list[tuple[str, Any]]) -> None:
|
|
31
|
+
for path, value in reversed(undo):
|
|
32
|
+
cur = data
|
|
33
|
+
loc = path.split(".")
|
|
34
|
+
for c in loc[:-1]:
|
|
35
|
+
cur = cur[c]
|
|
36
|
+
if value is DELETE:
|
|
37
|
+
del cur[loc[-1]]
|
|
38
|
+
else:
|
|
39
|
+
cur[loc[-1]] = value
|