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.
Files changed (42) hide show
  1. autotools/autocaps/commands.py +21 -0
  2. autotools/autocolor/__init__.py +0 -0
  3. autotools/autocolor/commands.py +60 -0
  4. autotools/autocolor/core.py +99 -0
  5. autotools/autoconvert/__init__.py +0 -0
  6. autotools/autoconvert/commands.py +79 -0
  7. autotools/autoconvert/conversion/__init__.py +0 -0
  8. autotools/autoconvert/conversion/convert_audio.py +24 -0
  9. autotools/autoconvert/conversion/convert_image.py +29 -0
  10. autotools/autoconvert/conversion/convert_text.py +101 -0
  11. autotools/autoconvert/conversion/convert_video.py +25 -0
  12. autotools/autoconvert/core.py +54 -0
  13. autotools/autoip/commands.py +38 -1
  14. autotools/autoip/core.py +99 -42
  15. autotools/autolower/commands.py +21 -0
  16. autotools/autonote/__init__.py +0 -0
  17. autotools/autonote/commands.py +70 -0
  18. autotools/autonote/core.py +106 -0
  19. autotools/autopassword/commands.py +39 -1
  20. autotools/autotest/commands.py +36 -6
  21. autotools/autotodo/__init__.py +87 -0
  22. autotools/autotodo/commands.py +115 -0
  23. autotools/autotodo/core.py +567 -0
  24. autotools/autounit/__init__.py +0 -0
  25. autotools/autounit/commands.py +55 -0
  26. autotools/autounit/core.py +36 -0
  27. autotools/autozip/__init__.py +0 -0
  28. autotools/autozip/commands.py +88 -0
  29. autotools/autozip/core.py +107 -0
  30. autotools/cli.py +66 -62
  31. autotools/utils/commands.py +141 -10
  32. autotools/utils/smoke.py +246 -0
  33. autotools/utils/text.py +57 -0
  34. open_autotools-0.0.5.dist-info/METADATA +100 -0
  35. open_autotools-0.0.5.dist-info/RECORD +54 -0
  36. {open_autotools-0.0.4rc2.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
  37. open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
  38. open_autotools-0.0.4rc2.dist-info/METADATA +0 -84
  39. open_autotools-0.0.4rc2.dist-info/RECORD +0 -30
  40. open_autotools-0.0.4rc2.dist-info/entry_points.txt +0 -6
  41. {open_autotools-0.0.4rc2.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
  42. {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 autocaps, autolower, autopassword, autoip, autotest
17
- from .utils.performance import init_metrics, finalize_metrics, get_metrics, should_enable_metrics, track_step
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
- A suite of automated tools for various tasks:\n
34
- - autocaps: Convert text to uppercase\n
35
- - autolower: Convert text to lowercase\n
36
- - autopassword: Generate secure passwords and encryption keys\n
37
- - autoip: Display network information and run diagnostics\n
38
- - test: Run the test suite\n
39
- \n
40
- Run 'autotools COMMAND --help' for more information on each command.\n
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
- # EXECUTES COMMAND WITH PERFORMANCE TRACKING
58
- def _execute_with_metrics(ctx, original_callback, *args, **kwargs):
59
- metrics = get_metrics()
60
- # REMOVE 'perf' FROM kwargs IF PRESENT (IT'S NOT PART OF THE ORIGINAL CALLBACK SIGNATURE)
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
- # WRAPS COMMANDS WITH PERFORMANCE TRACKING
81
- def _wrap_command_with_metrics(cmd):
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
- cli.add_command(_wrap_command_with_metrics(autocaps))
107
- cli.add_command(_wrap_command_with_metrics(autolower))
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()
@@ -1,13 +1,144 @@
1
- from ..autocaps.commands import autocaps
2
- from ..autolower.commands import autolower
3
- from ..autopassword.commands import autopassword
4
- from ..autoip.commands import autoip
5
- from ..autotest.commands import autotest
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
- 'autocaps',
9
- 'autolower',
10
- 'autopassword',
11
- 'autoip',
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)