Open-AutoTools 0.0.4rc2__py3-none-any.whl → 0.0.5__py3-none-any.whl
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.
- autotools/autocaps/commands.py +21 -0
- autotools/autocolor/__init__.py +0 -0
- autotools/autocolor/commands.py +60 -0
- autotools/autocolor/core.py +99 -0
- autotools/autoconvert/__init__.py +0 -0
- autotools/autoconvert/commands.py +79 -0
- autotools/autoconvert/conversion/__init__.py +0 -0
- autotools/autoconvert/conversion/convert_audio.py +24 -0
- autotools/autoconvert/conversion/convert_image.py +29 -0
- autotools/autoconvert/conversion/convert_text.py +101 -0
- autotools/autoconvert/conversion/convert_video.py +25 -0
- autotools/autoconvert/core.py +54 -0
- autotools/autoip/commands.py +38 -1
- autotools/autoip/core.py +99 -42
- autotools/autolower/commands.py +21 -0
- autotools/autonote/__init__.py +0 -0
- autotools/autonote/commands.py +70 -0
- autotools/autonote/core.py +106 -0
- autotools/autopassword/commands.py +39 -1
- autotools/autotest/commands.py +36 -6
- autotools/autotodo/__init__.py +87 -0
- autotools/autotodo/commands.py +115 -0
- autotools/autotodo/core.py +567 -0
- autotools/autounit/__init__.py +0 -0
- autotools/autounit/commands.py +55 -0
- autotools/autounit/core.py +36 -0
- autotools/autozip/__init__.py +0 -0
- autotools/autozip/commands.py +88 -0
- autotools/autozip/core.py +107 -0
- autotools/cli.py +66 -62
- autotools/utils/commands.py +141 -10
- autotools/utils/smoke.py +246 -0
- autotools/utils/text.py +57 -0
- open_autotools-0.0.5.dist-info/METADATA +100 -0
- open_autotools-0.0.5.dist-info/RECORD +54 -0
- {open_autotools-0.0.4rc2.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
- open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
- open_autotools-0.0.4rc2.dist-info/METADATA +0 -84
- open_autotools-0.0.4rc2.dist-info/RECORD +0 -30
- open_autotools-0.0.4rc2.dist-info/entry_points.txt +0 -6
- {open_autotools-0.0.4rc2.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {open_autotools-0.0.4rc2.dist-info → open_autotools-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from .core import autounit_convert
|
|
3
|
+
from ..utils.loading import LoadingAnimation
|
|
4
|
+
from ..utils.updates import check_for_updates
|
|
5
|
+
|
|
6
|
+
# TOOL CATEGORY (USED BY 'autotools smoke')
|
|
7
|
+
TOOL_CATEGORY = 'Conversion'
|
|
8
|
+
|
|
9
|
+
# SMOKE TEST CASES (USED BY 'autotools smoke')
|
|
10
|
+
SMOKE_TESTS = [
|
|
11
|
+
{'name': 'length-meters-feet', 'args': ['100', 'meter', 'feet']},
|
|
12
|
+
{'name': 'volume-liters-gallons', 'args': ['10', 'liter', 'gallon']},
|
|
13
|
+
{'name': 'weight-kg-lb', 'args': ['50', 'kilogram', 'pound']},
|
|
14
|
+
{'name': 'temperature-celsius-fahrenheit', 'args': ['25', 'celsius', 'fahrenheit']},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
# CLI COMMAND TO CONVERT MEASUREMENT UNITS
|
|
18
|
+
@click.command()
|
|
19
|
+
@click.argument('value', nargs=1)
|
|
20
|
+
@click.argument('from_unit', nargs=1)
|
|
21
|
+
@click.argument('to_unit', nargs=1)
|
|
22
|
+
def autounit(value, from_unit, to_unit):
|
|
23
|
+
"""
|
|
24
|
+
CONVERTS MEASUREMENT UNITS (EXAMPLE: METERS TO FEET, LITERS TO GALLONS).
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
SUPPORTS VARIOUS UNIT CATEGORIES:
|
|
28
|
+
- LENGTH: meter, feet, inch, kilometer, mile, centimeter, millimeter, yard
|
|
29
|
+
- VOLUME: liter, gallon, milliliter, fluid_ounce, cup, pint, quart
|
|
30
|
+
- WEIGHT: kilogram, pound, gram, ounce, ton, stone
|
|
31
|
+
- TEMPERATURE: celsius, fahrenheit, kelvin
|
|
32
|
+
- AND MANY MORE...
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
EXAMPLES:
|
|
36
|
+
autounit 100 meter feet
|
|
37
|
+
autounit 10 liter gallon
|
|
38
|
+
autounit 50 kilogram pound
|
|
39
|
+
autounit 25 celsius fahrenheit
|
|
40
|
+
autounit 5 kilometer mile
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with LoadingAnimation():
|
|
45
|
+
result = autounit_convert(value, from_unit, to_unit)
|
|
46
|
+
click.echo(result)
|
|
47
|
+
update_msg = check_for_updates()
|
|
48
|
+
if update_msg:
|
|
49
|
+
click.echo(update_msg)
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
click.echo(click.style(f"ERROR: {str(e)}", fg='red'), err=True)
|
|
52
|
+
raise click.Abort()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
click.echo(click.style(f"UNEXPECTED ERROR: {str(e)}", fg='red'), err=True)
|
|
55
|
+
raise click.Abort()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pyperclip
|
|
2
|
+
from pint import UnitRegistry
|
|
3
|
+
|
|
4
|
+
# INITIALIZE UNIT REGISTRY
|
|
5
|
+
ureg = UnitRegistry()
|
|
6
|
+
|
|
7
|
+
# CONVERTS MEASUREMENT UNITS
|
|
8
|
+
def autounit_convert(value, from_unit, to_unit):
|
|
9
|
+
try:
|
|
10
|
+
if isinstance(value, str):
|
|
11
|
+
value = value.strip()
|
|
12
|
+
value = value.replace(',', '')
|
|
13
|
+
|
|
14
|
+
value_float = float(value)
|
|
15
|
+
quantity = ureg.Quantity(value_float, from_unit)
|
|
16
|
+
result = quantity.to(to_unit)
|
|
17
|
+
result_value = result.magnitude
|
|
18
|
+
|
|
19
|
+
if abs(result_value) >= 1000 or abs(result_value) < 0.01: result_str = f"{result_value:.6g}"
|
|
20
|
+
elif abs(result_value) >= 1: result_str = f"{result_value:.4f}".rstrip('0').rstrip('.')
|
|
21
|
+
else: result_str = f"{result_value:.6f}".rstrip('0').rstrip('.')
|
|
22
|
+
|
|
23
|
+
output = f"{result_str} {to_unit}"
|
|
24
|
+
|
|
25
|
+
try: pyperclip.copy(output)
|
|
26
|
+
except pyperclip.PyperclipException: pass
|
|
27
|
+
|
|
28
|
+
return output
|
|
29
|
+
|
|
30
|
+
except ValueError as e:
|
|
31
|
+
raise ValueError(f"INVALID VALUE OR UNIT: {str(e)}")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
error_msg = str(e)
|
|
34
|
+
if "cannot convert" in error_msg.lower() or "incompatible" in error_msg.lower():
|
|
35
|
+
raise ValueError(f"CANNOT CONVERT FROM '{from_unit}' TO '{to_unit}': INCOMPATIBLE UNITS")
|
|
36
|
+
raise ValueError(f"CONVERSION ERROR: {error_msg}")
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from .core import autozip_compress
|
|
4
|
+
from ..utils.loading import LoadingAnimation
|
|
5
|
+
from ..utils.updates import check_for_updates
|
|
6
|
+
|
|
7
|
+
# TOOL CATEGORY (USED BY 'autotools smoke')
|
|
8
|
+
TOOL_CATEGORY = 'Files'
|
|
9
|
+
|
|
10
|
+
# SMOKE TEST CASES (USED BY 'autotools smoke')
|
|
11
|
+
SMOKE_TESTS = [
|
|
12
|
+
{'name': 'zip-readme', 'args': ['README.md', '--output', '/tmp/autozip-smoke.zip']},
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# CLI COMMAND TO COMPRESS FILES AND DIRECTORIES
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.argument('sources', nargs=-1, required=True)
|
|
18
|
+
@click.option('--output', '-o', 'output_path', required=True,
|
|
19
|
+
help='OUTPUT ARCHIVE PATH (EXTENSION DETERMINES FORMAT)')
|
|
20
|
+
@click.option('--format', '-f', 'archive_format',
|
|
21
|
+
type=click.Choice(['zip', 'tar.gz', 'tar.bz2', 'tar.xz', 'tar'], case_sensitive=False),
|
|
22
|
+
help='ARCHIVE FORMAT (AUTO-DETECTED FROM OUTPUT EXTENSION IF NOT SPECIFIED)')
|
|
23
|
+
@click.option('--compression', '-c', 'compression_level', type=int, default=6,
|
|
24
|
+
help='COMPRESSION LEVEL (0-9, DEFAULT: 6)')
|
|
25
|
+
def autozip(sources, output_path, archive_format, compression_level):
|
|
26
|
+
"""
|
|
27
|
+
COMPRESSES FILES AND DIRECTORIES INTO VARIOUS ARCHIVE FORMATS.
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
SUPPORTED FORMATS:
|
|
31
|
+
- ZIP: .zip
|
|
32
|
+
- TAR.GZ: .tar.gz, .tgz
|
|
33
|
+
- TAR.BZ2: .tar.bz2, .tbz2
|
|
34
|
+
- TAR.XZ: .tar.xz, .txz
|
|
35
|
+
- TAR: .tar
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
EXAMPLES:
|
|
39
|
+
autozip file.txt dir/ -o archive.zip
|
|
40
|
+
autozip file1.txt file2.txt -o backup.tar.gz --compression 9
|
|
41
|
+
autozip project/ -o release.tar.bz2 --format tar.bz2
|
|
42
|
+
autozip data/ -o archive.tar.xz --compression 7
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# VALIDATE SOURCE PATHS
|
|
46
|
+
if not sources:
|
|
47
|
+
click.echo(click.style("ERROR: AT LEAST ONE SOURCE PATH IS REQUIRED", fg='red'), err=True)
|
|
48
|
+
click.echo(click.get_current_context().get_help())
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# VALIDATE COMPRESSION LEVEL
|
|
52
|
+
if compression_level < 0 or compression_level > 9:
|
|
53
|
+
click.echo(click.style("ERROR: COMPRESSION LEVEL MUST BE BETWEEN 0 AND 9", fg='red'), err=True)
|
|
54
|
+
raise click.Abort()
|
|
55
|
+
|
|
56
|
+
# COMPRESS FILES AND DIRECTORIES
|
|
57
|
+
try:
|
|
58
|
+
with LoadingAnimation():
|
|
59
|
+
result = autozip_compress(
|
|
60
|
+
list(sources),
|
|
61
|
+
output_path,
|
|
62
|
+
archive_format=archive_format,
|
|
63
|
+
compression_level=compression_level
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
click.echo(click.style(f"SUCCESS: CREATED ARCHIVE: {result}", fg='green'))
|
|
67
|
+
|
|
68
|
+
archive_size = Path(result).stat().st_size
|
|
69
|
+
size_mb = archive_size / (1024 * 1024)
|
|
70
|
+
|
|
71
|
+
if size_mb >= 1:
|
|
72
|
+
click.echo(f"ARCHIVE SIZE: {size_mb:.2f} MB")
|
|
73
|
+
else:
|
|
74
|
+
size_kb = archive_size / 1024
|
|
75
|
+
click.echo(f"ARCHIVE SIZE: {size_kb:.2f} KB")
|
|
76
|
+
|
|
77
|
+
update_msg = check_for_updates()
|
|
78
|
+
if update_msg: click.echo(update_msg)
|
|
79
|
+
|
|
80
|
+
except FileNotFoundError as e:
|
|
81
|
+
click.echo(click.style(f"ERROR: {str(e)}", fg='red'), err=True)
|
|
82
|
+
raise click.Abort()
|
|
83
|
+
except ValueError as e:
|
|
84
|
+
click.echo(click.style(f"ERROR: {str(e)}", fg='red'), err=True)
|
|
85
|
+
raise click.Abort()
|
|
86
|
+
except Exception as e:
|
|
87
|
+
click.echo(click.style(f"UNEXPECTED ERROR: {str(e)}", fg='red'), err=True)
|
|
88
|
+
raise click.Abort()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import zipfile
|
|
3
|
+
import tarfile
|
|
4
|
+
import lzma
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# COMPRESSES FILES/DIRECTORIES TO ZIP FORMAT
|
|
8
|
+
def _compress_zip(source_paths, output_path, compression_level=6):
|
|
9
|
+
compression_level = min(max(compression_level, 0), 9)
|
|
10
|
+
|
|
11
|
+
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compression_level) as zipf:
|
|
12
|
+
for source_path in source_paths:
|
|
13
|
+
source = Path(source_path)
|
|
14
|
+
if not source.exists():
|
|
15
|
+
raise FileNotFoundError(f"SOURCE PATH NOT FOUND: {source_path}")
|
|
16
|
+
|
|
17
|
+
if source.is_file():
|
|
18
|
+
zipf.write(source, source.name)
|
|
19
|
+
elif source.is_dir():
|
|
20
|
+
for root, dirs, files in os.walk(source):
|
|
21
|
+
for file in files:
|
|
22
|
+
file_path = Path(root) / file
|
|
23
|
+
arcname = file_path.relative_to(source)
|
|
24
|
+
zipf.write(file_path, arcname)
|
|
25
|
+
|
|
26
|
+
return output_path
|
|
27
|
+
|
|
28
|
+
# COMPRESSES FILES/DIRECTORIES TO TAR.GZ FORMAT
|
|
29
|
+
def _compress_tar_gz(source_paths, output_path, compression_level=6):
|
|
30
|
+
compression_level = min(max(compression_level, 1), 9)
|
|
31
|
+
|
|
32
|
+
with tarfile.open(output_path, 'w:gz', compresslevel=compression_level) as tar:
|
|
33
|
+
for source_path in source_paths:
|
|
34
|
+
source = Path(source_path)
|
|
35
|
+
if not source.exists():
|
|
36
|
+
raise FileNotFoundError(f"SOURCE PATH NOT FOUND: {source_path}")
|
|
37
|
+
tar.add(source, arcname=source.name)
|
|
38
|
+
|
|
39
|
+
return output_path
|
|
40
|
+
|
|
41
|
+
# COMPRESSES FILES/DIRECTORIES TO TAR.BZ2 FORMAT
|
|
42
|
+
def _compress_tar_bz2(source_paths, output_path, compression_level=6):
|
|
43
|
+
compression_level = min(max(compression_level, 1), 9)
|
|
44
|
+
|
|
45
|
+
with tarfile.open(output_path, 'w:bz2', compresslevel=compression_level) as tar:
|
|
46
|
+
for source_path in source_paths:
|
|
47
|
+
source = Path(source_path)
|
|
48
|
+
if not source.exists():
|
|
49
|
+
raise FileNotFoundError(f"SOURCE PATH NOT FOUND: {source_path}")
|
|
50
|
+
tar.add(source, arcname=source.name)
|
|
51
|
+
|
|
52
|
+
return output_path
|
|
53
|
+
|
|
54
|
+
# COMPRESSES FILES/DIRECTORIES TO TAR.XZ FORMAT
|
|
55
|
+
def _compress_tar_xz(source_paths, output_path, compression_level=6):
|
|
56
|
+
compression_level = min(max(compression_level, 0), 9)
|
|
57
|
+
|
|
58
|
+
import lzma
|
|
59
|
+
with lzma.open(output_path, 'wb', preset=compression_level) as lzma_file:
|
|
60
|
+
with tarfile.open(fileobj=lzma_file, mode='w') as tar:
|
|
61
|
+
for source_path in source_paths:
|
|
62
|
+
source = Path(source_path)
|
|
63
|
+
if not source.exists():
|
|
64
|
+
raise FileNotFoundError(f"SOURCE PATH NOT FOUND: {source_path}")
|
|
65
|
+
tar.add(source, arcname=source.name)
|
|
66
|
+
|
|
67
|
+
return output_path
|
|
68
|
+
|
|
69
|
+
# DETERMINES OUTPUT FORMAT FROM FILE EXTENSION
|
|
70
|
+
def _get_format_from_extension(output_path):
|
|
71
|
+
path_str = str(output_path).lower()
|
|
72
|
+
if path_str.endswith('.tar.gz') or path_str.endswith('.tgz'): return 'tar.gz'
|
|
73
|
+
elif path_str.endswith('.tar.bz2') or path_str.endswith('.tbz2'): return 'tar.bz2'
|
|
74
|
+
elif path_str.endswith('.tar.xz') or path_str.endswith('.txz'): return 'tar.xz'
|
|
75
|
+
elif path_str.endswith('.tar'): return 'tar'
|
|
76
|
+
elif path_str.endswith('.zip'): return 'zip'
|
|
77
|
+
else:
|
|
78
|
+
ext = Path(output_path).suffix.lower()
|
|
79
|
+
raise ValueError(f"UNSUPPORTED ARCHIVE FORMAT: {ext}\nSUPPORTED: .zip, .tar.gz, .tar.bz2, .tar.xz, .tar")
|
|
80
|
+
|
|
81
|
+
# COMPRESSES FILES/DIRECTORIES TO TAR FORMAT (UNCOMPRESSED)
|
|
82
|
+
def _compress_tar(source_paths, output_path):
|
|
83
|
+
with tarfile.open(output_path, 'w') as tar:
|
|
84
|
+
for source_path in source_paths:
|
|
85
|
+
source = Path(source_path)
|
|
86
|
+
if not source.exists():
|
|
87
|
+
raise FileNotFoundError(f"SOURCE PATH NOT FOUND: {source_path}")
|
|
88
|
+
tar.add(source, arcname=source.name)
|
|
89
|
+
|
|
90
|
+
return output_path
|
|
91
|
+
|
|
92
|
+
# COMPRESSES FILES AND DIRECTORIES INTO VARIOUS ARCHIVE FORMATS
|
|
93
|
+
def autozip_compress(source_paths, output_path, archive_format=None, compression_level=6):
|
|
94
|
+
if not source_paths:
|
|
95
|
+
raise ValueError("NO SOURCE PATHS PROVIDED")
|
|
96
|
+
|
|
97
|
+
output = Path(output_path)
|
|
98
|
+
if archive_format is None: archive_format = _get_format_from_extension(output_path)
|
|
99
|
+
archive_format = archive_format.lower()
|
|
100
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
if archive_format == 'zip': return _compress_zip(source_paths, str(output), compression_level)
|
|
103
|
+
elif archive_format in ('tar.gz', 'tgz'): return _compress_tar_gz(source_paths, str(output), compression_level)
|
|
104
|
+
elif archive_format in ('tar.bz2', 'tbz2'): return _compress_tar_bz2(source_paths, str(output), compression_level)
|
|
105
|
+
elif archive_format in ('tar.xz', 'txz'): return _compress_tar_xz(source_paths, str(output), compression_level)
|
|
106
|
+
elif archive_format == 'tar': return _compress_tar(source_paths, str(output))
|
|
107
|
+
else: raise ValueError(f"UNSUPPORTED FORMAT: {archive_format}\nSUPPORTED: zip, tar.gz, tar.bz2, tar.xz, tar")
|
autotools/cli.py
CHANGED
|
@@ -4,6 +4,7 @@ import base64
|
|
|
4
4
|
import argparse
|
|
5
5
|
import json as json_module
|
|
6
6
|
import sys
|
|
7
|
+
import os
|
|
7
8
|
|
|
8
9
|
from dotenv import load_dotenv
|
|
9
10
|
from datetime import datetime
|
|
@@ -13,8 +14,8 @@ from importlib.metadata import version as get_version, PackageNotFoundError
|
|
|
13
14
|
|
|
14
15
|
from .utils.version import print_version
|
|
15
16
|
from .utils.updates import check_for_updates
|
|
16
|
-
from .utils.commands import
|
|
17
|
-
from .utils.performance import init_metrics, finalize_metrics, get_metrics, should_enable_metrics
|
|
17
|
+
from .utils.commands import get_wrapped_tool_commands
|
|
18
|
+
from .utils.performance import init_metrics, finalize_metrics, get_metrics, should_enable_metrics
|
|
18
19
|
|
|
19
20
|
load_dotenv()
|
|
20
21
|
|
|
@@ -30,14 +31,22 @@ load_dotenv()
|
|
|
30
31
|
@click.pass_context
|
|
31
32
|
def cli(ctx, perf):
|
|
32
33
|
"""
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
\b
|
|
35
|
+
A suite of automated tools for various tasks:
|
|
36
|
+
- autocaps: Convert text to uppercase
|
|
37
|
+
- autolower: Convert text to lowercase
|
|
38
|
+
- autopassword: Generate secure passwords and encryption keys
|
|
39
|
+
- autoip: Display network information and run diagnostics
|
|
40
|
+
- autoconvert: Convert text, images, audio, and video between formats
|
|
41
|
+
- autocolor: Convert color codes between different formats (hex, RGB, HSL, etc)
|
|
42
|
+
- autounit: Convert measurement units (meters to feet, liters to gallons, etc)
|
|
43
|
+
- autozip: Compress files and directories into various archive formats (zip, tar.gz, etc)
|
|
44
|
+
- autotodo: Create and manages a simple task list in a markdown file
|
|
45
|
+
- autonote: Takes quick notes and saves them to a markdown file
|
|
46
|
+
- test: Run the test suite (development only)
|
|
47
|
+
|
|
48
|
+
\b
|
|
49
|
+
Run 'autotools COMMAND --help' for more information on each command.
|
|
41
50
|
"""
|
|
42
51
|
|
|
43
52
|
# INITIALIZE METRICS IF NEEDED
|
|
@@ -54,60 +63,16 @@ def cli(ctx, perf):
|
|
|
54
63
|
get_metrics().end_process()
|
|
55
64
|
finalize_metrics(ctx)
|
|
56
65
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
kwargs.pop('perf', None)
|
|
62
|
-
|
|
63
|
-
if not should_enable_metrics(ctx): return original_callback(*args, **kwargs)
|
|
64
|
-
|
|
65
|
-
if metrics.process_start is None:
|
|
66
|
-
init_metrics()
|
|
67
|
-
get_metrics().end_startup()
|
|
68
|
-
|
|
69
|
-
metrics.start_command()
|
|
70
|
-
cmd_name = ctx.invoked_subcommand or ctx.command.name or 'unknown'
|
|
71
|
-
try:
|
|
72
|
-
with track_step(f'command_{cmd_name}'): result = original_callback(*args, **kwargs)
|
|
73
|
-
metrics.end_command()
|
|
74
|
-
return result
|
|
75
|
-
finally:
|
|
76
|
-
if metrics.process_end is None:
|
|
77
|
-
metrics.end_process()
|
|
78
|
-
finalize_metrics(ctx)
|
|
66
|
+
# DISCOVER AND REGISTER ALL TOOL COMMANDS
|
|
67
|
+
_wrapped_tool_commands = get_wrapped_tool_commands()
|
|
68
|
+
for _tool_name in sorted(_wrapped_tool_commands):
|
|
69
|
+
_cmd = _wrapped_tool_commands[_tool_name]
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
import inspect
|
|
83
|
-
original_callback = cmd.callback
|
|
84
|
-
|
|
85
|
-
# ADD --perf OPTION TO THE COMMAND SO IT CAN BE USED DIRECTLY ON SUBCOMMANDS
|
|
86
|
-
has_perf_option = any(param.opts == ['--perf'] for param in cmd.params if isinstance(param, click.Option))
|
|
87
|
-
if not has_perf_option:
|
|
88
|
-
perf_option = click.Option(['--perf'], is_flag=True, help='Display performance metrics')
|
|
89
|
-
cmd.params.append(perf_option)
|
|
90
|
-
|
|
91
|
-
sig = inspect.signature(original_callback)
|
|
92
|
-
expects_ctx = 'ctx' in sig.parameters
|
|
93
|
-
|
|
94
|
-
if expects_ctx:
|
|
95
|
-
@click.pass_context
|
|
96
|
-
def wrapped_callback(ctx, *args, **kwargs):
|
|
97
|
-
return _execute_with_metrics(ctx, original_callback, ctx, *args, **kwargs)
|
|
98
|
-
else:
|
|
99
|
-
def wrapped_callback(*args, **kwargs):
|
|
100
|
-
ctx = click.get_current_context()
|
|
101
|
-
return _execute_with_metrics(ctx, original_callback, *args, **kwargs)
|
|
102
|
-
|
|
103
|
-
cmd.callback = wrapped_callback
|
|
104
|
-
return cmd
|
|
71
|
+
if _tool_name == 'autotest': cli.add_command(_cmd, name='test')
|
|
72
|
+
else: cli.add_command(_cmd)
|
|
105
73
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
cli.add_command(_wrap_command_with_metrics(autopassword))
|
|
109
|
-
cli.add_command(_wrap_command_with_metrics(autoip))
|
|
110
|
-
cli.add_command(_wrap_command_with_metrics(autotest), name='test')
|
|
74
|
+
# EXPOSE TOOL COMMANDS FOR console_scripts ENTRYPOINTS (setup.py)
|
|
75
|
+
for _tool_name, _cmd in _wrapped_tool_commands.items(): globals()[_tool_name] = _cmd
|
|
111
76
|
|
|
112
77
|
# DISPLAYS COMMAND OPTIONS
|
|
113
78
|
def _display_command_options(cmd_obj):
|
|
@@ -153,4 +118,43 @@ def autotools():
|
|
|
153
118
|
click.echo(click.style("\nUpdate Available:", fg='red', bold=True))
|
|
154
119
|
click.echo(update_msg)
|
|
155
120
|
|
|
121
|
+
# LISTS TOOL SUBCOMMANDS (MACHINE-FRIENDLY)
|
|
122
|
+
@cli.command(name='list-tools')
|
|
123
|
+
@click.option('--json', 'as_json', is_flag=True, help='OUTPUT JSON')
|
|
124
|
+
def list_tools(as_json):
|
|
125
|
+
tools = []
|
|
126
|
+
for tool_name, cmd in get_wrapped_tool_commands().items():
|
|
127
|
+
public_name = 'test' if tool_name == 'autotest' else (cmd.name or tool_name)
|
|
128
|
+
tools.append(public_name)
|
|
129
|
+
|
|
130
|
+
tools = sorted(set(tools))
|
|
131
|
+
if as_json: click.echo(json_module.dumps(tools))
|
|
132
|
+
else: click.echo("\n".join(tools))
|
|
133
|
+
|
|
134
|
+
# RUNS SMOKE TESTS FOR ALL DISCOVERED TOOLS (USED BY docker/run_tests.sh)
|
|
135
|
+
@cli.command()
|
|
136
|
+
@click.option('--workdir', type=click.Path(file_okay=False, dir_okay=True), default=None, help='WORK DIRECTORY FOR TEMP FILES')
|
|
137
|
+
@click.option('--timeout', type=int, default=30, help='PER-CASE TIMEOUT (SECONDS)')
|
|
138
|
+
@click.option('--include', 'include_tools', multiple=True, help='ONLY RUN THESE TOOLS (REPEATABLE)')
|
|
139
|
+
@click.option('--exclude', 'exclude_tools', multiple=True, help='SKIP THESE TOOLS (REPEATABLE)')
|
|
140
|
+
@click.option('--json', 'as_json', is_flag=True, help='OUTPUT JSON RESULTS')
|
|
141
|
+
@click.option('--verbose/--quiet', default=(os.getenv('VERBOSE', '1') == '1'), help='SHOW COMMAND OUTPUT')
|
|
142
|
+
def smoke(workdir, timeout, include_tools, exclude_tools, as_json, verbose):
|
|
143
|
+
from .utils.smoke import run_smoke
|
|
144
|
+
|
|
145
|
+
results = run_smoke(
|
|
146
|
+
workdir=workdir,
|
|
147
|
+
timeout_s=timeout,
|
|
148
|
+
include=set(include_tools),
|
|
149
|
+
exclude=set(exclude_tools),
|
|
150
|
+
verbose=verbose,
|
|
151
|
+
platform=os.getenv('PLATFORM') or 'Unknown Platform',
|
|
152
|
+
print_table=not as_json
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if as_json: click.echo(json_module.dumps(results, ensure_ascii=False))
|
|
156
|
+
|
|
157
|
+
failed = [r for r in results if r.get('status') != 'OK']
|
|
158
|
+
raise SystemExit(1 if failed else 0)
|
|
159
|
+
|
|
156
160
|
if __name__ == '__main__': cli()
|
autotools/utils/commands.py
CHANGED
|
@@ -1,13 +1,144 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from
|
|
1
|
+
import click
|
|
2
|
+
import inspect
|
|
3
|
+
import importlib
|
|
4
|
+
import pkgutil
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
from typing import Dict, Iterable, List, Tuple
|
|
7
|
+
|
|
8
|
+
from .performance import init_metrics, finalize_metrics, get_metrics, should_enable_metrics, track_step
|
|
6
9
|
|
|
7
10
|
__all__ = [
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'autotest'
|
|
11
|
+
'discover_tool_command_entries',
|
|
12
|
+
'get_wrapped_tool_commands',
|
|
13
|
+
'register_commands',
|
|
14
|
+
'get_tool_category'
|
|
13
15
|
]
|
|
16
|
+
|
|
17
|
+
# PACKAGES THAT SHOULD NEVER BE TREATED AS TOOLS
|
|
18
|
+
_EXCLUDED_TOOL_PACKAGES = {'utils', '__pycache__'}
|
|
19
|
+
|
|
20
|
+
# ITERATES ALL TOP-LEVEL TOOL PACKAGES (autotools/<tool>/)
|
|
21
|
+
def _iter_tool_packages() -> Iterable[str]:
|
|
22
|
+
import autotools as autotools_pkg
|
|
23
|
+
for module_info in pkgutil.iter_modules(autotools_pkg.__path__):
|
|
24
|
+
if not module_info.ispkg: continue
|
|
25
|
+
name = module_info.name
|
|
26
|
+
if name.startswith('_'): continue
|
|
27
|
+
if name in _EXCLUDED_TOOL_PACKAGES: continue
|
|
28
|
+
if name == 'cli': continue
|
|
29
|
+
yield name
|
|
30
|
+
|
|
31
|
+
# IMPORTS autotools.<tool>.commands IF IT EXISTS
|
|
32
|
+
def _import_tool_commands_module(tool_name: str) -> ModuleType | None:
|
|
33
|
+
full_name = f'autotools.{tool_name}.commands'
|
|
34
|
+
try:
|
|
35
|
+
return importlib.import_module(full_name)
|
|
36
|
+
except ModuleNotFoundError as e:
|
|
37
|
+
if e.name == full_name: return None
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
# EXTRACTS ALL CLICK COMMAND OBJECTS FROM A MODULE
|
|
41
|
+
def _extract_click_commands(mod: ModuleType) -> List[click.Command]:
|
|
42
|
+
commands: List[click.Command] = []
|
|
43
|
+
for value in mod.__dict__.values():
|
|
44
|
+
if isinstance(value, click.core.Command): commands.append(value)
|
|
45
|
+
return commands
|
|
46
|
+
|
|
47
|
+
# SELECTS THE APPROPRIATE COMMAND FROM A LIST OF COMMANDS FOR A GIVEN TOOL NAME
|
|
48
|
+
def _select_command_for_tool(cmds: List[click.Command], tool_name: str, mod_name: str) -> click.Command:
|
|
49
|
+
selected = None
|
|
50
|
+
for c in cmds:
|
|
51
|
+
if c.name == tool_name:
|
|
52
|
+
selected = c
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
if selected is None:
|
|
56
|
+
if len(cmds) == 1:
|
|
57
|
+
selected = cmds[0]
|
|
58
|
+
else:
|
|
59
|
+
names = ', '.join(sorted({c.name or '<unnamed>' for c in cmds}))
|
|
60
|
+
raise RuntimeError(f"MULTIPLE CLICK COMMANDS FOUND IN {mod_name}: {names}")
|
|
61
|
+
|
|
62
|
+
return selected
|
|
63
|
+
|
|
64
|
+
# DISCOVERS TOOL COMMANDS AS (MODULE, CLICK COMMAND) BY TOOL PACKAGE NAME
|
|
65
|
+
def discover_tool_command_entries() -> Dict[str, Tuple[ModuleType, click.Command]]:
|
|
66
|
+
entries: Dict[str, Tuple[ModuleType, click.Command]] = {}
|
|
67
|
+
for tool_name in _iter_tool_packages():
|
|
68
|
+
mod = _import_tool_commands_module(tool_name)
|
|
69
|
+
if mod is None: continue
|
|
70
|
+
cmds = _extract_click_commands(mod)
|
|
71
|
+
if not cmds: continue
|
|
72
|
+
|
|
73
|
+
selected = _select_command_for_tool(cmds, tool_name, mod.__name__)
|
|
74
|
+
entries[tool_name] = (mod, selected)
|
|
75
|
+
|
|
76
|
+
return entries
|
|
77
|
+
|
|
78
|
+
# RETURNS WRAPPED TOOL COMMANDS (USED BY CLI GROUP AND CONSOLE_SCRIPTS EXPORTS)
|
|
79
|
+
def get_wrapped_tool_commands() -> Dict[str, click.Command]:
|
|
80
|
+
wrapped: Dict[str, click.Command] = {}
|
|
81
|
+
for tool_name, (_mod, cmd) in discover_tool_command_entries().items():
|
|
82
|
+
wrapped[tool_name] = _wrap_command_with_metrics(cmd)
|
|
83
|
+
return wrapped
|
|
84
|
+
|
|
85
|
+
# EXECUTES COMMAND WITH PERFORMANCE TRACKING
|
|
86
|
+
def _execute_with_metrics(ctx, original_callback, *args, **kwargs):
|
|
87
|
+
metrics = get_metrics()
|
|
88
|
+
kwargs.pop('perf', None)
|
|
89
|
+
|
|
90
|
+
if not should_enable_metrics(ctx): return original_callback(*args, **kwargs)
|
|
91
|
+
if metrics.process_start is None:
|
|
92
|
+
init_metrics()
|
|
93
|
+
get_metrics().end_startup()
|
|
94
|
+
|
|
95
|
+
metrics.start_command()
|
|
96
|
+
cmd_name = ctx.invoked_subcommand or ctx.command.name or 'unknown'
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with track_step(f'command_{cmd_name}'): result = original_callback(*args, **kwargs)
|
|
100
|
+
metrics.end_command()
|
|
101
|
+
return result
|
|
102
|
+
finally:
|
|
103
|
+
if metrics.process_end is None:
|
|
104
|
+
metrics.end_process()
|
|
105
|
+
finalize_metrics(ctx)
|
|
106
|
+
|
|
107
|
+
# WRAPS COMMANDS WITH PERFORMANCE TRACKING
|
|
108
|
+
def _wrap_command_with_metrics(cmd):
|
|
109
|
+
if getattr(cmd, '_autotools_metrics_wrapped', False): return cmd
|
|
110
|
+
|
|
111
|
+
original_callback = cmd.callback
|
|
112
|
+
has_perf_option = any(param.opts == ['--perf'] for param in cmd.params if isinstance(param, click.Option))
|
|
113
|
+
|
|
114
|
+
if not has_perf_option:
|
|
115
|
+
perf_option = click.Option(['--perf'], is_flag=True, help='Display performance metrics')
|
|
116
|
+
cmd.params.append(perf_option)
|
|
117
|
+
|
|
118
|
+
sig = inspect.signature(original_callback)
|
|
119
|
+
expects_ctx = 'ctx' in sig.parameters
|
|
120
|
+
|
|
121
|
+
if expects_ctx:
|
|
122
|
+
@click.pass_context
|
|
123
|
+
def wrapped_callback(ctx, *args, **kwargs):
|
|
124
|
+
return _execute_with_metrics(ctx, original_callback, ctx, *args, **kwargs)
|
|
125
|
+
else:
|
|
126
|
+
def wrapped_callback(*args, **kwargs):
|
|
127
|
+
ctx = click.get_current_context()
|
|
128
|
+
return _execute_with_metrics(ctx, original_callback, *args, **kwargs)
|
|
129
|
+
|
|
130
|
+
cmd.callback = wrapped_callback
|
|
131
|
+
cmd._autotools_metrics_wrapped = True
|
|
132
|
+
return cmd
|
|
133
|
+
|
|
134
|
+
# EXTRACTS TOOL CATEGORY FROM MODULE (FALLBACK TO 'Other' IF NOT DEFINED)
|
|
135
|
+
def get_tool_category(mod: ModuleType) -> str:
|
|
136
|
+
return getattr(mod, 'TOOL_CATEGORY', 'Other')
|
|
137
|
+
|
|
138
|
+
# FUNCTION TO REGISTER ALL COMMANDS TO CLI GROUP
|
|
139
|
+
def register_commands(cli_group):
|
|
140
|
+
wrapped = get_wrapped_tool_commands()
|
|
141
|
+
for tool_name in sorted(wrapped):
|
|
142
|
+
cmd = wrapped[tool_name]
|
|
143
|
+
if tool_name == 'autotest': cli_group.add_command(cmd, name='test')
|
|
144
|
+
else: cli_group.add_command(cmd)
|