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.
- 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 +39 -1
- autotools/autoip/core.py +100 -43
- 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 +43 -12
- 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/performance.py +67 -35
- autotools/utils/requirements.py +21 -0
- autotools/utils/smoke.py +246 -0
- autotools/utils/text.py +73 -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.4rc1.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.4rc1.dist-info/METADATA +0 -103
- open_autotools-0.0.4rc1.dist-info/RECORD +0 -28
- open_autotools-0.0.4rc1.dist-info/entry_points.txt +0 -6
- {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/top_level.txt +0 -0
autotools/utils/performance.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 []
|
autotools/utils/smoke.py
ADDED
|
@@ -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
|
+
|
autotools/utils/text.py
ADDED
|
@@ -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,,
|