Open-AutoTools 0.0.4rc1__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 (44) 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 +39 -1
  14. autotools/autoip/core.py +100 -43
  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 +43 -12
  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/performance.py +67 -35
  33. autotools/utils/requirements.py +21 -0
  34. autotools/utils/smoke.py +246 -0
  35. autotools/utils/text.py +73 -0
  36. open_autotools-0.0.5.dist-info/METADATA +100 -0
  37. open_autotools-0.0.5.dist-info/RECORD +54 -0
  38. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
  39. open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
  40. open_autotools-0.0.4rc1.dist-info/METADATA +0 -103
  41. open_autotools-0.0.4rc1.dist-info/RECORD +0 -28
  42. open_autotools-0.0.4rc1.dist-info/entry_points.txt +0 -6
  43. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
  44. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/top_level.txt +0 -0
@@ -3,11 +3,17 @@ import gc
3
3
  import sys
4
4
  import time
5
5
  import click
6
- import resource
7
6
  import tracemalloc
8
7
  from contextlib import contextmanager
9
8
  from typing import Dict, List, Tuple, Optional
10
9
 
10
+ try:
11
+ import resource
12
+ RESOURCE_AVAILABLE = True
13
+ except ImportError:
14
+ resource = None
15
+ RESOURCE_AVAILABLE = False
16
+
11
17
  try:
12
18
  import psutil
13
19
  PSUTIL_AVAILABLE = True
@@ -19,9 +25,10 @@ ENABLE_PERFORMANCE_METRICS = False
19
25
  if os.getenv('AUTOTOOLS_DISABLE_PERF', '').lower() in ('1', 'true', 'yes'): ENABLE_PERFORMANCE_METRICS = False
20
26
 
21
27
  # FLAG TO ENABLE/DISABLE TRACEMALLOC (CAN BE SLOW IN PRODUCTION)
22
- # ENABLE BY DEFAULT IN TEST ENVIRONMENTS (DETECTED BY PRESENCE OF PYTEST OR TEST IN SYS.ARGV)
28
+ # ONLY ENABLE IF EXPLICITLY REQUESTED VIA ENV VAR OR IF PYTEST IS ACTUALLY RUNNING
29
+ # DO NOT ENABLE BASED ON ARGUMENT VALUES TO AVOID FALSE POSITIVES (EXAMPLE: "test" AS COMMAND ARGUMENT)
23
30
  _ENV_TRACEMALLOC = os.getenv('AUTOTOOLS_ENABLE_TRACEMALLOC', '').lower() in ('1', 'true', 'yes')
24
- _IS_TEST_ENV = 'pytest' in sys.modules or any('test' in arg.lower() or 'pytest' in arg.lower() for arg in sys.argv)
31
+ _IS_TEST_ENV = 'pytest' in sys.modules or any(arg.endswith('pytest') or arg.endswith('py.test') for arg in sys.argv)
25
32
  ENABLE_TRACEMALLOC = _ENV_TRACEMALLOC or _IS_TEST_ENV
26
33
 
27
34
  # PERFORMANCE METRICS COLLECTOR
@@ -133,47 +140,72 @@ class PerformanceMetrics:
133
140
 
134
141
  # RECORDS CPU USAGE AT START
135
142
  def _record_cpu_start(self):
136
- usage = resource.getrusage(resource.RUSAGE_SELF)
137
- self.cpu_user_start = usage.ru_utime
138
- self.cpu_sys_start = usage.ru_stime
143
+ if PSUTIL_AVAILABLE:
144
+ process = psutil.Process()
145
+ cpu_times = process.cpu_times()
146
+ self.cpu_user_start = cpu_times.user
147
+ self.cpu_sys_start = cpu_times.system
148
+ elif RESOURCE_AVAILABLE:
149
+ usage = resource.getrusage(resource.RUSAGE_SELF)
150
+ self.cpu_user_start = usage.ru_utime
151
+ self.cpu_sys_start = usage.ru_stime
152
+ else:
153
+ self.cpu_user_start = time.process_time()
154
+ self.cpu_sys_start = 0.0
139
155
 
140
156
  # RECORDS CPU USAGE AT END
141
157
  def _record_cpu_end(self):
142
- usage = resource.getrusage(resource.RUSAGE_SELF)
143
- self.cpu_user_end = usage.ru_utime
144
- self.cpu_sys_end = usage.ru_stime
158
+ if PSUTIL_AVAILABLE:
159
+ process = psutil.Process()
160
+ cpu_times = process.cpu_times()
161
+ self.cpu_user_end = cpu_times.user
162
+ self.cpu_sys_end = cpu_times.system
163
+ elif RESOURCE_AVAILABLE:
164
+ usage = resource.getrusage(resource.RUSAGE_SELF)
165
+ self.cpu_user_end = usage.ru_utime
166
+ self.cpu_sys_end = usage.ru_stime
167
+ else:
168
+ self.cpu_user_end = time.process_time()
169
+ self.cpu_sys_end = 0.0
145
170
 
146
171
  # RECORDS MEMORY USAGE AT START
147
172
  def _record_rss_start(self):
148
173
  if PSUTIL_AVAILABLE:
149
174
  process = psutil.Process()
150
175
  self.rss_start = process.memory_info().rss / (1024 * 1024) # MB
151
- else:
176
+ elif RESOURCE_AVAILABLE:
152
177
  usage = resource.getrusage(resource.RUSAGE_SELF)
153
178
  self.rss_start = usage.ru_maxrss / 1024 # MB (LINUX) OR KB (MACOS)
154
179
  if sys.platform == 'darwin':
155
180
  self.rss_start = self.rss_start / 1024 # CONVERT KB TO MB ON MACOS
181
+ else:
182
+ self.rss_start = 0.0
156
183
 
157
184
  # RECORDS MEMORY USAGE AT END
158
185
  def _record_rss_end(self):
159
- if PSUTIL_AVAILABLE:
160
- process = psutil.Process()
161
- mem_info = process.memory_info()
162
- self.rss_peak = mem_info.rss / (1024 * 1024) # MB
186
+ if PSUTIL_AVAILABLE: self._record_rss_end_psutil()
187
+ elif RESOURCE_AVAILABLE: self._record_rss_end_resource()
188
+ else: self.rss_peak = self.rss_start if self.rss_start is not None else 0.0
163
189
 
164
- try:
165
- if hasattr(process, 'memory_info_ex'):
166
- mem_ext = process.memory_info_ex()
167
- if hasattr(mem_ext, 'peak_wss'):
168
- self.rss_peak = max(self.rss_peak, mem_ext.peak_wss / (1024 * 1024))
169
- except Exception:
170
- pass
171
- else:
172
- usage = resource.getrusage(resource.RUSAGE_SELF)
173
- rss_current = usage.ru_maxrss / 1024
174
- if sys.platform == 'darwin':
175
- rss_current = rss_current / 1024
176
- self.rss_peak = max(self.rss_start, rss_current) if self.rss_start else rss_current
190
+ # RECORDS MEMORY USAGE AT END USING PSUTIL
191
+ def _record_rss_end_psutil(self):
192
+ process = psutil.Process()
193
+ mem_info = process.memory_info()
194
+ self.rss_peak = mem_info.rss / (1024 * 1024) # MB
195
+
196
+ try:
197
+ if hasattr(process, 'memory_info_ex'):
198
+ mem_ext = process.memory_info_ex()
199
+ if hasattr(mem_ext, 'peak_wss'): self.rss_peak = max(self.rss_peak, mem_ext.peak_wss / (1024 * 1024))
200
+ except Exception:
201
+ pass
202
+
203
+ # RECORDS MEMORY USAGE AT END USING RESOURCE
204
+ def _record_rss_end_resource(self):
205
+ usage = resource.getrusage(resource.RUSAGE_SELF)
206
+ rss_current = usage.ru_maxrss / 1024
207
+ if sys.platform == 'darwin': rss_current = rss_current / 1024
208
+ self.rss_peak = max(self.rss_start, rss_current) if self.rss_start else rss_current
177
209
 
178
210
  # RECORDS FILESYSTEM I/O AT START
179
211
  def _record_fs_start(self):
@@ -217,15 +249,15 @@ class PerformanceMetrics:
217
249
 
218
250
  # CALCULATES DURATION METRICS IN MILLISECONDS
219
251
  def _calculate_durations(self) -> Tuple[float, float, float]:
220
- total_duration_ms = (self.process_end - self.process_start) * 1000 if self.process_end and self.process_start else 0
221
- startup_duration_ms = (self.startup_end - self.startup_start) * 1000 if self.startup_end and self.startup_start else 0
222
- command_duration_ms = (self.command_end - self.command_start) * 1000 if self.command_end and self.command_start else 0
252
+ total_duration_ms = (self.process_end - self.process_start) * 1000 if self.process_end is not None and self.process_start is not None else 0
253
+ startup_duration_ms = (self.startup_end - self.startup_start) * 1000 if self.startup_end is not None and self.startup_start is not None else 0
254
+ command_duration_ms = (self.command_end - self.command_start) * 1000 if self.command_end is not None and self.command_start is not None else 0
223
255
  return total_duration_ms, startup_duration_ms, command_duration_ms
224
256
 
225
257
  # CALCULATES CPU TIME METRICS IN MILLISECONDS
226
258
  def _calculate_cpu_time(self) -> Tuple[float, float, float]:
227
- cpu_user_ms = (self.cpu_user_end - self.cpu_user_start) * 1000 if self.cpu_user_end and self.cpu_user_start else 0
228
- cpu_sys_ms = (self.cpu_sys_end - self.cpu_sys_start) * 1000 if self.cpu_sys_end and self.cpu_sys_start else 0
259
+ cpu_user_ms = (self.cpu_user_end - self.cpu_user_start) * 1000 if self.cpu_user_end is not None and self.cpu_user_start is not None else 0
260
+ cpu_sys_ms = (self.cpu_sys_end - self.cpu_sys_start) * 1000 if self.cpu_sys_end is not None and self.cpu_sys_start is not None else 0
229
261
  cpu_time_total_ms = cpu_user_ms + cpu_sys_ms
230
262
  return cpu_time_total_ms, cpu_user_ms, cpu_sys_ms
231
263
 
@@ -260,9 +292,9 @@ class PerformanceMetrics:
260
292
 
261
293
  # CALCULATES FILESYSTEM I/O METRICS
262
294
  def _calculate_fs_io(self) -> Tuple[int, int, int]:
263
- fs_bytes_read_total = self.fs_read_end - self.fs_read_start if self.fs_read_end and self.fs_read_start else 0
264
- fs_bytes_written_total = self.fs_write_end - self.fs_write_start if self.fs_write_end and self.fs_write_start else 0
265
- fs_ops_count = self.fs_ops_end - self.fs_ops_start if self.fs_ops_end and self.fs_ops_start else 0
295
+ fs_bytes_read_total = self.fs_read_end - self.fs_read_start if self.fs_read_end is not None and self.fs_read_start is not None else 0
296
+ fs_bytes_written_total = self.fs_write_end - self.fs_write_start if self.fs_write_end is not None and self.fs_write_start is not None else 0
297
+ fs_ops_count = self.fs_ops_end - self.fs_ops_start if self.fs_ops_end is not None and self.fs_ops_start is not None else 0
266
298
  return fs_bytes_read_total, fs_bytes_written_total, fs_ops_count
267
299
 
268
300
  # CALCULATES AND RETURNS ALL PERFORMANCE METRICS AS A DICTIONARY
@@ -0,0 +1,21 @@
1
+ import os
2
+
3
+ # READ REQUIREMENTS FROM A FILE AND RETURN AS A LIST
4
+ # HANDLES MISSING FILES GRACEFULLY BY RETURNING AN EMPTY LIST
5
+ # THE FILENAME IS RELATIVE TO THE PROJECT ROOT (WHERE SETUP.PY IS LOCATED)
6
+ def read_requirements(filename="requirements.txt"):
7
+ project_root = os.path.join(os.path.dirname(__file__), "..", "..")
8
+ requirements_path = os.path.join(os.path.abspath(project_root), filename)
9
+
10
+ try:
11
+ with open(requirements_path, "r", encoding="utf-8") as fh:
12
+ requirements = []
13
+
14
+ for line in fh:
15
+ line = line.strip()
16
+ if line.startswith("-r") or line.startswith("--requirement"): continue
17
+ if line and not line.startswith("#"): requirements.append(line)
18
+
19
+ return requirements
20
+ except FileNotFoundError:
21
+ return []
@@ -0,0 +1,246 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import click
5
+ import tempfile
6
+ import subprocess
7
+
8
+ from contextlib import contextmanager
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple
11
+ from .commands import discover_tool_command_entries, get_tool_category
12
+
13
+ # NORMALIZES SMOKE TEST DEFINITIONS TO A LIST OF (NAME, ARGS)
14
+ def _normalize_smoke_test_item(item: Any, default_name: str) -> Tuple[str, List[str]]:
15
+ if isinstance(item, dict):
16
+ name = item.get('name') or default_name
17
+ args = item.get('args') or []
18
+ if not isinstance(args, list):
19
+ raise TypeError("SMOKE_TESTS ITEM 'args' MUST BE A LIST")
20
+ return str(name), [str(x) for x in args]
21
+
22
+ if isinstance(item, (list, tuple)) and len(item) == 2 and isinstance(item[0], str):
23
+ name = item[0] or default_name
24
+ args = item[1] or []
25
+ if not isinstance(args, list):
26
+ raise TypeError("SMOKE_TESTS TUPLE SECOND ITEM MUST BE A LIST")
27
+ return str(name), [str(x) for x in args]
28
+
29
+ if isinstance(item, list): return default_name, [str(x) for x in item]
30
+ raise TypeError("SMOKE_TESTS ITEMS MUST BE dict OR (name, args) OR args-list")
31
+
32
+ def _normalize_smoke_tests(value: Any) -> List[Tuple[str, List[str]]]:
33
+ if not value: return []
34
+ if not isinstance(value, list):
35
+ raise TypeError("SMOKE_TESTS MUST BE A LIST")
36
+
37
+ tests: List[Tuple[str, List[str]]] = []
38
+ for idx, item in enumerate(value):
39
+ tests.append(_normalize_smoke_test_item(item, default_name=f"case{idx + 1}"))
40
+
41
+ return tests
42
+
43
+ # BUILDS A BASIC INVOCATION FOR A TOOL USING CLICK PARAMS (BEST-EFFORT)
44
+ def _build_default_case(tool: str, cmd: click.Command, tool_dir: Path) -> Tuple[str, List[str]]:
45
+ if tool == 'autocolor': return ('basic', ['#FF5733'])
46
+
47
+ if tool == 'autoconvert':
48
+ input_path = tool_dir / 'input.txt'
49
+ input_path.parent.mkdir(parents=True, exist_ok=True)
50
+ input_path.write_text('SMOKE TEST\n', encoding='utf-8')
51
+
52
+ output_path = tool_dir / 'output.json'
53
+ output_path.parent.mkdir(parents=True, exist_ok=True)
54
+ return ('txt-json', [str(input_path), str(output_path)])
55
+
56
+ required_opts = [p for p in cmd.params if isinstance(p, click.Option) and p.required]
57
+ args_params = [p for p in cmd.params if isinstance(p, click.Argument)]
58
+ argv: List[str] = []
59
+
60
+ for opt in required_opts:
61
+ flag = (opt.opts[0] if opt.opts else f'--{opt.name}')
62
+ if opt.is_flag:
63
+ argv.append(flag)
64
+ continue
65
+ argv.extend([flag, _value_for_param(opt, tool_dir)])
66
+
67
+ for arg in args_params:
68
+ count = 1
69
+ if isinstance(arg.nargs, int) and arg.nargs > 1: count = arg.nargs
70
+ for _ in range(count): argv.append(_value_for_param(arg, tool_dir))
71
+
72
+ return ('default', argv)
73
+
74
+ # GENERATES A VALUE FOR A CLICK PARAM (BEST-EFFORT)
75
+ def _value_for_param(param: click.Parameter, tool_dir: Path) -> str:
76
+ name = (param.name or '').lower()
77
+ if 'color' in name: return '#FF5733'
78
+
79
+ looks_like_path = any(k in name for k in ['file', 'path', 'source', 'input', 'output', 'dir', 'folder', 'archive'])
80
+ if isinstance(getattr(param, 'type', None), click.Path) or looks_like_path:
81
+ if any(k in name for k in ['input', 'source']):
82
+ p = tool_dir / f'{param.name or "input"}.txt'
83
+ p.parent.mkdir(parents=True, exist_ok=True)
84
+ p.write_text('SMOKE TEST\n', encoding='utf-8')
85
+ return str(p)
86
+
87
+ suffix = '.zip' if 'zip' in name or 'archive' in name or 'output' in name else '.out'
88
+ p = tool_dir / f'{param.name or "output"}{suffix}'
89
+ p.parent.mkdir(parents=True, exist_ok=True)
90
+ return str(p)
91
+
92
+ t = getattr(param, 'type', None)
93
+ if isinstance(t, click.Choice) and t.choices: return str(t.choices[0])
94
+
95
+ return 'test'
96
+
97
+ # VERBOSE HELPERS (KEEP _run_subprocess SIMPLE)
98
+ # FOR CMD
99
+ def _echo_cmd(argv: Sequence[str], verbose: bool) -> None:
100
+ if not verbose: return
101
+ click.echo(click.style(f"$ {' '.join(argv)}", fg='cyan', bold=True))
102
+
103
+ # FOR OUTPUT
104
+ def _echo_output(output: str, verbose: bool) -> None:
105
+ if not verbose: return
106
+ if not output.strip(): return
107
+ click.echo(output.rstrip())
108
+
109
+ # FOR DURATION
110
+ def _echo_duration(duration: float, verbose: bool) -> None:
111
+ if not verbose: return
112
+ click.echo(click.style(f"({duration:.2f}s)", fg='bright_black'))
113
+
114
+ # FOR TIMEOUT
115
+ def _echo_timeout(timeout_s: int, verbose: bool) -> None:
116
+ if not verbose: return
117
+ click.echo(click.style(f"TIMEOUT AFTER {timeout_s}s", fg='red', bold=True))
118
+
119
+ # FOR PERMISSION ERROR
120
+ def _echo_permission_error(err: Exception, verbose: bool) -> None:
121
+ if not verbose: return
122
+ click.echo(click.style(f"PERMISSION ERROR: {err}", fg='red', bold=True))
123
+
124
+ # RUNS ONE COMMAND AND RETURNS (STATUS, RC, DURATION_S, OUTPUT)
125
+ def _run_subprocess(argv: Sequence[str], timeout_s: int, verbose: bool) -> Tuple[str, int, float, str]:
126
+ start = time.perf_counter()
127
+ try:
128
+ completed = subprocess.run( list(argv), text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=timeout_s, env=dict(os.environ))
129
+ duration = time.perf_counter() - start
130
+ output = completed.stdout or ''
131
+ status = 'OK' if completed.returncode == 0 else 'X'
132
+
133
+ _echo_cmd(argv, verbose)
134
+ _echo_output(output, verbose)
135
+ _echo_duration(duration, verbose)
136
+
137
+ return status, completed.returncode, duration, output
138
+ except PermissionError as e:
139
+ duration = time.perf_counter() - start
140
+ if duration >= timeout_s:
141
+ _echo_cmd(argv, verbose)
142
+ _echo_timeout(timeout_s, verbose)
143
+ return 'TIMEOUT', 124, duration, ''
144
+
145
+ _echo_cmd(argv, verbose)
146
+ _echo_permission_error(e, verbose)
147
+ return 'X', 126, duration, str(e)
148
+ except subprocess.TimeoutExpired as e:
149
+ duration = time.perf_counter() - start
150
+ output = (e.stdout or '') if hasattr(e, 'stdout') else ''
151
+
152
+ _echo_cmd(argv, verbose)
153
+ _echo_timeout(timeout_s, verbose)
154
+
155
+ return 'TIMEOUT', 124, duration, output
156
+
157
+ # PRINTS A SUMMARY TABLE (SIMILAR TO docker/run_tests.sh)
158
+ def _print_summary(results: List[Dict[str, Any]], platform: str) -> None:
159
+ click.echo(f"\n=== Smoke Test Results Summary for {platform} ===")
160
+ click.echo("┌────────────────┬──────────────────┬──────────────┬────────┐")
161
+ click.echo("│ Category │ Tool │ Feature │ Status │")
162
+ click.echo("├────────────────┼──────────────────┼──────────────┼────────┤")
163
+
164
+ for r in results:
165
+ category = r.get('category', 'Other')
166
+ tool = r.get('tool', '')
167
+ feature = r.get('case', '')
168
+ status = r.get('status', '')
169
+ click.echo(f"│ {category:<12} │ {tool:<14} │ {feature:<12} │ {status:<6} │")
170
+
171
+ click.echo("└────────────────┴──────────────────┴──────────────┴────────┘")
172
+
173
+ # RETURNS THE PUBLIC CLI NAME FOR A TOOL PACKAGE
174
+ def _tool_public_name(tool_name: str) -> str:
175
+ return 'test' if tool_name == 'autotest' else tool_name
176
+
177
+ # DECIDES WHETHER A TOOL SHOULD RUN GIVEN include/exclude FILTERS
178
+ def _should_run_tool(tool_name: str, public_name: str, include: set[str], exclude: set[str]) -> bool:
179
+ if include and tool_name not in include and public_name not in include: return False
180
+ if tool_name in exclude or public_name in exclude: return False
181
+ return True
182
+
183
+ # CHOOSES SMOKE TESTS FOR A TOOL (MODULE-DEFINED OR DEFAULT)
184
+ def _get_smoke_tests(mod: Any, tool_name: str, cmd: click.Command, tool_dir: Path) -> List[Tuple[str, List[str]]]:
185
+ smoke_tests = _normalize_smoke_tests(getattr(mod, 'SMOKE_TESTS', None))
186
+ if smoke_tests: return smoke_tests
187
+ return [_build_default_case(tool_name, cmd, tool_dir)]
188
+
189
+ # WORKDIR CONTEXT (CLEANUP ONLY WHEN WE CREATED THE TEMP DIR)
190
+ @contextmanager
191
+ def _smoke_root(workdir: Optional[str]) -> Iterator[Path]:
192
+ if workdir:
193
+ root = Path(workdir)
194
+ root.mkdir(parents=True, exist_ok=True)
195
+ yield root
196
+ return
197
+
198
+ with tempfile.TemporaryDirectory(prefix='autotools_smoke_') as d: yield Path(d)
199
+
200
+ # RUNS ALL CASES FOR A TOOL AND RETURNS RESULT ROWS
201
+ def _run_tool_smoke(public_name: str, mod: Any, smoke_tests: List[Tuple[str, List[str]]], timeout_s: int, verbose: bool) -> List[Dict[str, Any]]:
202
+ rows: List[Dict[str, Any]] = []
203
+ for case_name, case_args in smoke_tests:
204
+ argv = [sys.executable, '-m', 'autotools.cli', public_name, *case_args]
205
+ status, rc, duration_s, output = _run_subprocess(argv, timeout_s=timeout_s, verbose=verbose)
206
+
207
+ rows.append({
208
+ 'category': get_tool_category(mod),
209
+ 'tool': public_name,
210
+ 'case': case_name,
211
+ 'status': status if status in ('OK', 'X') else 'X',
212
+ 'returncode': rc,
213
+ 'duration_s': round(duration_s, 3),
214
+ 'cmd': argv,
215
+ 'output': output if (verbose or status != 'OK') else '',
216
+ })
217
+
218
+ return rows
219
+
220
+ # MAIN SMOKE ENTRYPOINT
221
+ def run_smoke(workdir: Optional[str], timeout_s: int, include: set[str], exclude: set[str], verbose: bool, platform: str, print_table: bool = True) -> List[Dict[str, Any]]:
222
+ entries = discover_tool_command_entries()
223
+
224
+ if 'test' not in include and 'autotest' not in include: exclude = set(exclude) | {'autotest', 'test'}
225
+
226
+ with _smoke_root(workdir) as root:
227
+ run_root = root / f"smoke_{int(time.time())}"
228
+ run_root.mkdir(parents=True, exist_ok=True)
229
+
230
+ results: List[Dict[str, Any]] = []
231
+ for tool_name in sorted(entries):
232
+ mod, cmd = entries[tool_name]
233
+ public_name = _tool_public_name(tool_name)
234
+
235
+ if not _should_run_tool(tool_name, public_name, include, exclude): continue
236
+
237
+ tool_dir = run_root / tool_name
238
+ tool_dir.mkdir(parents=True, exist_ok=True)
239
+
240
+ smoke_tests = _get_smoke_tests(mod, tool_name, cmd, tool_dir)
241
+ results.extend(_run_tool_smoke(public_name, mod, smoke_tests, timeout_s=timeout_s, verbose=verbose))
242
+
243
+ if print_table: _print_summary(results, platform=platform)
244
+
245
+ return results
246
+
@@ -0,0 +1,73 @@
1
+ import sys
2
+ import os
3
+ import re
4
+ from typing import Any
5
+
6
+ # ENSURE TEXT IS SAFE TO WRITE TO THE CURRENT STDOUT ENCODING
7
+ # SOME WINDOWS TERMINALS USE LEGACY ENCODINGS THAT CANNOT ENCODE CERTAIN CHARACTERS
8
+ def safe_text(text: Any) -> Any:
9
+ if not isinstance(text, str): return text
10
+
11
+ encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
12
+
13
+ try:
14
+ text.encode(encoding)
15
+ return text
16
+ except Exception:
17
+ try: return text.encode(encoding, errors="replace").decode(encoding)
18
+ except Exception: return text.encode("ascii", errors="replace").decode("ascii")
19
+
20
+ # DETECTS IF RUNNING IN A CI/CD ENVIRONMENT
21
+ def is_ci_environment():
22
+ ci_vars = ['CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL', 'TRAVIS', 'CIRCLECI', 'APPVEYOR', 'BUILDKITE', 'TEAMCITY']
23
+ return any(os.getenv(var) for var in ci_vars)
24
+
25
+ # MASKS IPV4 ADDRESSES (EXAMPLE: 192.168.1.1 -> xxx.xxx.xxx.xxx)
26
+ def mask_ipv4(ip: str) -> str:
27
+ if not ip or not isinstance(ip, str): return ip
28
+ parts = ip.split('.')
29
+ if len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts): return 'xxx.xxx.xxx.xxx'
30
+ return ip
31
+
32
+ # MASKS IPV6 ADDRESSES (EXAMPLE: 2001:0db8::1 -> xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx)
33
+ def mask_ipv6(ip: str) -> str:
34
+ if not ip or not isinstance(ip, str): return ip
35
+ clean_ip = ip.split('%')[0]
36
+ if ':' in clean_ip: return 'xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx'
37
+ return ip
38
+
39
+ # MASKS ALL IP ADDRESSES IN A STRING
40
+ def mask_ips_in_text(text: str) -> str:
41
+ if not isinstance(text, str): return text
42
+
43
+ # MASK IPV4 ADDRESSES FIRST
44
+ ipv4_pattern = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
45
+ text = re.sub(ipv4_pattern, lambda m: mask_ipv4(m.group()) if all(0 <= int(p) <= 255 for p in m.group().split('.')) else m.group(), text)
46
+
47
+ # MASK IPV6 ADDRESSES - HANDLE ALL FORMATS INCLUDING COMPRESSED
48
+ # COMPRESSED AT START: ::1, ::8a2e:370:7334
49
+ ipv6_compressed_start = r'(?<![0-9a-fA-F:])::(?:[0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}(?![0-9a-fA-F:])'
50
+ text = re.sub(ipv6_compressed_start, lambda m: mask_ipv6(m.group()), text)
51
+
52
+ # COMPRESSED WITH :: IN MIDDLE: 2001:db8::1, 2001:db8::8a2e:370:7334
53
+ # FLEXIBLE PATTERN: ANY NUMBER OF HEX GROUPS BEFORE ::, THEN ::, THEN ANY NUMBER AFTER
54
+ ipv6_compressed_mid = r'(?<![0-9a-fA-F:])[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})+::[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})*(?![0-9a-fA-F:])'
55
+ text = re.sub(ipv6_compressed_mid, lambda m: mask_ipv6(m.group()), text)
56
+
57
+ # COMPRESSED AT END: 2001:db8::
58
+ ipv6_compressed_end = r'(?<![0-9a-fA-F:])[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4})+::(?![0-9a-fA-F:])'
59
+ text = re.sub(ipv6_compressed_end, lambda m: mask_ipv6(m.group()), text)
60
+
61
+ # FULL FORMAT: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
62
+ ipv6_full = r'(?<![0-9a-fA-F:])(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}(?![0-9a-fA-F:])'
63
+ text = re.sub(ipv6_full, lambda m: mask_ipv6(m.group()), text)
64
+
65
+ return text
66
+
67
+ # MASKS SENSITIVE INFORMATION IN TEXT (IPS, LOCATION DATA, ETC.)
68
+ def mask_sensitive_info(text: str, mask_ips: bool = True) -> str:
69
+ if not isinstance(text, str): return text
70
+ if mask_ips: text = mask_ips_in_text(text)
71
+ coord_pattern = r'-?\d+\.\d+,-?\d+\.\d+'
72
+ text = re.sub(coord_pattern, '[REDACTED]', text)
73
+ return text
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: Open-AutoTools
3
+ Version: 0.0.5
4
+ Summary: A suite of automated tools accessible via CLI with a simple `autotools` command
5
+ Home-page: https://github.com/BabylooPro/Open-AutoTools
6
+ Author: BabylooPro
7
+ Author-email: maxremy.dev@gmail.com
8
+ License: MIT
9
+ Project-URL: Bug Tracker, https://github.com/BabylooPro/Open-AutoTools/issues
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: click>=8.1.3
20
+ Requires-Dist: requests>=2.31.0
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: packaging>=23.0
23
+ Requires-Dist: halo>=0.0.31
24
+ Requires-Dist: pyperclip>=1.8.2
25
+ Requires-Dist: speedtest-cli>=2.1.3
26
+ Requires-Dist: psutil>=5.9.0
27
+ Requires-Dist: cryptography>=42.0.2
28
+ Requires-Dist: Pillow>=10.0.0
29
+ Requires-Dist: pydub>=0.25.1
30
+ Requires-Dist: moviepy>=1.0.3
31
+ Requires-Dist: pint>=0.23
32
+ Provides-Extra: dev
33
+ Provides-Extra: test
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: classifier
37
+ Dynamic: description
38
+ Dynamic: description-content-type
39
+ Dynamic: home-page
40
+ Dynamic: license
41
+ Dynamic: license-file
42
+ Dynamic: project-url
43
+ Dynamic: provides-extra
44
+ Dynamic: requires-dist
45
+ Dynamic: requires-python
46
+ Dynamic: summary
47
+
48
+ # OPEN-AUTOTOOLS
49
+
50
+ [PYPI_BADGE]: https://badge.fury.io/py/Open-AutoTools.svg
51
+ [PYPI_URL]: https://pypi.org/project/Open-AutoTools/
52
+ [PYTHON_BADGE]: https://img.shields.io/badge/Python-3.10+-blue.svg
53
+ [PYTHON_URL]: https://www.python.org/downloads/
54
+ [CHANGELOG_BADGE]: https://img.shields.io/badge/CHANGELOG-red.svg
55
+ [CHANGELOG_URL]: CHANGELOG.md
56
+ [TODO_BADGE]: https://img.shields.io/badge/TODO-purple.svg
57
+ [TODO_URL]: TODO.md
58
+
59
+ [![PyPI][PYPI_BADGE]][PYPI_URL] [![Python][PYTHON_BADGE]][PYTHON_URL] [![CHANGELOG][CHANGELOG_BADGE]][CHANGELOG_URL] [![TODO][TODO_BADGE]][TODO_URL]
60
+
61
+ Python CLI toolkit for everyday developer tasks. Boost productivity directly from your terminal.
62
+
63
+ https://github.com/user-attachments/assets/f959327b-b4ae-481d-8be0-c8957fb6ad36
64
+
65
+ ## Quick Install
66
+
67
+ ```bash
68
+ pip install open-autotools
69
+ ```
70
+
71
+ - **Platform**: Windows 10/11 ✓ | macOS ✓ | Linux ✓
72
+ - **Python**: 3.10 | 3.11 | 3.12 | 3.13 | 3.14
73
+
74
+ See [Installation Guide](docs/installation.md) for more details.
75
+
76
+ ## Tools
77
+
78
+ - **[AutoCaps](docs/tools/autocaps.md)** - Convert text to uppercase
79
+ - **[AutoLower](docs/tools/autolower.md)** - Convert text to lowercase
80
+ - **[AutoPassword](docs/tools/autopassword.md)** - Generate secure passwords and encryption keys
81
+ - **[AutoIP](docs/tools/autoip.md)** - Display network information and diagnostics
82
+ - **[AutoConvert](docs/tools/autoconvert.md)** - Convert text, images, audio, and video between formats
83
+ - **[AutoColor](docs/tools/autocolor.md)** - Convert color codes between different formats (hex, RGB, HSL, etc)
84
+ - **[AutoUnit](docs/tools/autounit.md)** - Convert measurement units (meters to feet, liters to gallons, etc)
85
+ - **[AutoZip](docs/tools/autozip.md)** - Compress files and directories into various archive formats (ZIP, TAR.GZ, TAR.BZ2, TAR.XZ, TAR)
86
+ - **[AutoTodo](docs/tools/autotodo.md)** - Create and manage a simple task list in a Markdown file
87
+ - **[AutoNote](docs/tools/autonote.md)** - Takes quick notes and saves them to a Markdown file
88
+ - **[Test Suite](docs/tools/autotest.md)** - Run the test suite (development only)
89
+
90
+ ## Documentation
91
+
92
+ - [Installation](docs/installation.md) - Install the CLI and verify your setup
93
+ - [Development](docs/development.md) - Dev environment, tooling, and contribution workflow
94
+ - [Testing](docs/testing.md) - Run tests, install test deps, and check coverage
95
+ - [Performance](docs/performance.md) - Performance metrics: what’s collected and how to enable them
96
+ - [Docker Support](docs/docker.md) - Run the toolkit and tests in Docker
97
+
98
+ ## License
99
+
100
+ MIT - see [LICENSE](LICENSE).
@@ -0,0 +1,54 @@
1
+ autotools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ autotools/cli.py,sha256=N05N7kJV1290lHhTMw5YT18t_G-aAUuaMTvNOig1aZs,6703
3
+ autotools/autocaps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ autotools/autocaps/commands.py,sha256=MzAseF0zQ7pm8gHZK-a1DI8a-9xMDNd1smOfvHVuAho,1058
5
+ autotools/autocaps/core.py,sha256=v0VGndr_6LAN7j5eQiUe1dGnP8GMRNsZmOoL-Z5hJbM,259
6
+ autotools/autocolor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ autotools/autocolor/commands.py,sha256=7qsUQLtsp3qjR5QkCXaFolN8mlKkxqf9h-zEUadOUls,2118
8
+ autotools/autocolor/core.py,sha256=kAR81-pNwOQiEXPB2hD4ungiBOy8fWwmpIZsRZeBEx8,3853
9
+ autotools/autoconvert/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ autotools/autoconvert/commands.py,sha256=3E_TIbHPKYooSWEJRaV_L7WyRlirlrt9EyncAB_ST04,3169
11
+ autotools/autoconvert/core.py,sha256=QD82THQOzu77j_aF1G8sOseP1byIqVqGYncJt50jnts,2278
12
+ autotools/autoconvert/conversion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ autotools/autoconvert/conversion/convert_audio.py,sha256=LSn1vx0R6oqHuYusuiuSGndterTBCvxe6OfJQJK2XgA,932
14
+ autotools/autoconvert/conversion/convert_image.py,sha256=aeWyYsU4JATe3ALD1OjB7wQTNcvjOK3lveEJQMnbws4,1258
15
+ autotools/autoconvert/conversion/convert_text.py,sha256=_q2qdZMIx8eMRUc_kdG0Cci2UqA3uvgo224ayV71l98,3466
16
+ autotools/autoconvert/conversion/convert_video.py,sha256=IptBrQsOHmD2_72MQmO3KzZXgz5g_DlaROytP0LJ6Bo,969
17
+ autotools/autoip/__init__.py,sha256=T_5hz9G4reFPXDucdzRoMFPYlAKwTPt9TejOpkRPgn0,23
18
+ autotools/autoip/commands.py,sha256=5kWfNugQTJRWuwgWN2cVB33h-nISxHsv1EiaL_0vQK4,2276
19
+ autotools/autoip/core.py,sha256=x4Fuq3yiyjkgoAGBWsaAHkmImpIpes3OtHM-RXOlitU,11369
20
+ autotools/autolower/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ autotools/autolower/commands.py,sha256=R5yzyVB50uRQFHiMske2h_1TSv2aKurfb2Hfhsog8kQ,1064
22
+ autotools/autolower/core.py,sha256=PLxP9eKoC_NY-ZGPgTxRLxNufv0VGrmOJjjSC_n1zog,259
23
+ autotools/autonote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ autotools/autonote/commands.py,sha256=srvlbG9F2KQH3c5gU_4N435bzkJrBSgqHckTMgICGdM,2787
25
+ autotools/autonote/core.py,sha256=bWTagr0XoM1CArB079m4Km_ZHHWZty3nfeGq_0MN28U,3525
26
+ autotools/autopassword/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
27
+ autotools/autopassword/commands.py,sha256=LAQWw7jhJj7OBTNOSHI_QXYaEjJiQuYydZg-28OjIFs,4378
28
+ autotools/autopassword/core.py,sha256=xSD4--oqEgnWQD4mlymUfL_qbH_7J8caqfenGCYBXcA,2481
29
+ autotools/autotest/__init__.py,sha256=G4bK86_araxN2j10W8wEX-ol9zXKNhXq8FtPcOj4_p4,55
30
+ autotools/autotest/commands.py,sha256=QntQrRkJnLavjaJ3AnkGTtn_C4tMwJzllbbOy00f960,8540
31
+ autotools/autotodo/__init__.py,sha256=P5LHjf1OTx78PfSSn2Ex7i7KIq94IF-rruWdV_sH3ak,2349
32
+ autotools/autotodo/commands.py,sha256=uu8jSSOhiq3qfQt_lLAJozqPXnn_OvFxR7FB62kfEts,5981
33
+ autotools/autotodo/core.py,sha256=EBkpkYanBJK8Uw7h5CEnIX044EOO86NdOk1LgQnnVTk,25301
34
+ autotools/autounit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ autotools/autounit/commands.py,sha256=-RO26l0tmCsU4XkzRfMylHpdpQCEnrty51CawTWYObU,1978
36
+ autotools/autounit/core.py,sha256=ty2j15-f3LoF6902aZq70TfXZR82YAb-PGrYMA3rbfY,1331
37
+ autotools/autozip/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
+ autotools/autozip/commands.py,sha256=AjlB3rWOBaz_6ghb5k7747jz1JPXxtqeUXliNWb4nmg,3283
39
+ autotools/autozip/core.py,sha256=ATVh9Trd5RlW7fha3jTeSE2MldA00Ii6r-NprjPCloE,4881
40
+ autotools/utils/__init__.py,sha256=2uAirI6ZbOwSFPSg5wuEjA0gMWf1XBJ4yP_WcGeND7M,183
41
+ autotools/utils/commands.py,sha256=V2EVGiAG2o4dlc7vqdkDA1lQLQJ7IL2QeGWqiTUqC0E,5272
42
+ autotools/utils/loading.py,sha256=yvXQI6FdIJBjRb40R2LTIgDxxNyiv_23p5ixYkQcGtg,752
43
+ autotools/utils/performance.py,sha256=wa9dL9NM8XzLz5yEkQvV9jjKXj8qAVWf6QrIzMyFqA0,17945
44
+ autotools/utils/requirements.py,sha256=hngCUozh_r9lGXk0CmL7H9JOpvk1whEZp9uaa-oXCrI,822
45
+ autotools/utils/smoke.py,sha256=QjdN3u1t1PPw8npxFBk1oUZVVsqZUsZfLgff4ZYg6Y8,10472
46
+ autotools/utils/text.py,sha256=IERFAD6Zu9dDz60kcnEomTmjKM3BzgDjDBNPDwJZADw,3286
47
+ autotools/utils/updates.py,sha256=FrwGGMI9rv-bUrsIpttRnBNXUAOlPpBb85GcYXa_o6A,1447
48
+ autotools/utils/version.py,sha256=ruKZcuLN77yO2x0AwjfyaHlncaHE-8GDzzSIBr-agTc,3082
49
+ open_autotools-0.0.5.dist-info/licenses/LICENSE,sha256=SpbSRxNWos2l0-geleCa6d0L9G_bOsZRkY4rB9OduJ0,1069
50
+ open_autotools-0.0.5.dist-info/METADATA,sha256=wu9AduzHU1TbCoHOVo7MNTXTE3qkSbIA4EQjY-9MP0k,3999
51
+ open_autotools-0.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
52
+ open_autotools-0.0.5.dist-info/entry_points.txt,sha256=lAzrnUr-_1qL4QHM7YgyVe11ipdmnbkvYYQWhkgDRHU,400
53
+ open_autotools-0.0.5.dist-info/top_level.txt,sha256=x5ZRvdQw7DQnVmR0YDqVSAuuS94KTHDmk6uIeW7YOPw,10
54
+ open_autotools-0.0.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5