Open-AutoTools 0.0.3rc4__py3-none-any.whl → 0.0.4__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 +3 -7
- autotools/autocaps/core.py +5 -4
- autotools/autoip/commands.py +8 -12
- autotools/autoip/core.py +151 -200
- autotools/autolower/commands.py +3 -7
- autotools/autolower/core.py +4 -3
- autotools/autopassword/commands.py +27 -33
- autotools/autopassword/core.py +32 -73
- autotools/autotest/__init__.py +2 -0
- autotools/autotest/commands.py +206 -0
- autotools/cli.py +123 -62
- autotools/utils/commands.py +13 -0
- autotools/utils/loading.py +14 -6
- autotools/utils/performance.py +424 -0
- autotools/utils/requirements.py +21 -0
- autotools/utils/text.py +16 -0
- autotools/utils/updates.py +30 -22
- autotools/utils/version.py +69 -63
- open_autotools-0.0.4.dist-info/METADATA +84 -0
- open_autotools-0.0.4.dist-info/RECORD +30 -0
- {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info}/WHEEL +1 -1
- {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info}/entry_points.txt +0 -3
- Open_AutoTools-0.0.3rc4.dist-info/METADATA +0 -308
- Open_AutoTools-0.0.3rc4.dist-info/RECORD +0 -44
- autotools/autocaps/tests/__init__.py +0 -1
- autotools/autocaps/tests/test_autocaps_core.py +0 -45
- autotools/autocaps/tests/test_autocaps_integration.py +0 -46
- autotools/autodownload/__init__.py +0 -0
- autotools/autodownload/commands.py +0 -38
- autotools/autodownload/core.py +0 -373
- autotools/autoip/tests/__init__.py +0 -1
- autotools/autoip/tests/test_autoip_core.py +0 -72
- autotools/autoip/tests/test_autoip_integration.py +0 -92
- autotools/autolower/tests/__init__.py +0 -1
- autotools/autolower/tests/test_autolower_core.py +0 -45
- autotools/autolower/tests/test_autolower_integration.py +0 -46
- autotools/autospell/__init__.py +0 -3
- autotools/autospell/commands.py +0 -123
- autotools/autospell/core.py +0 -222
- autotools/autotranslate/__init__.py +0 -3
- autotools/autotranslate/commands.py +0 -42
- autotools/autotranslate/core.py +0 -52
- autotools/test/__init__.py +0 -3
- autotools/test/commands.py +0 -120
- {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info/licenses}/LICENSE +0 -0
- {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import gc
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import click
|
|
6
|
+
import tracemalloc
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from typing import Dict, List, Tuple, Optional
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import resource
|
|
12
|
+
RESOURCE_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
resource = None
|
|
15
|
+
RESOURCE_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import psutil
|
|
19
|
+
PSUTIL_AVAILABLE = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
PSUTIL_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
# GLOBAL FLAG TO ENABLE/DISABLE PERFORMANCE METRICS
|
|
24
|
+
ENABLE_PERFORMANCE_METRICS = False
|
|
25
|
+
if os.getenv('AUTOTOOLS_DISABLE_PERF', '').lower() in ('1', 'true', 'yes'): ENABLE_PERFORMANCE_METRICS = False
|
|
26
|
+
|
|
27
|
+
# FLAG TO ENABLE/DISABLE TRACEMALLOC (CAN BE SLOW IN PRODUCTION)
|
|
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)
|
|
30
|
+
_ENV_TRACEMALLOC = os.getenv('AUTOTOOLS_ENABLE_TRACEMALLOC', '').lower() in ('1', 'true', 'yes')
|
|
31
|
+
_IS_TEST_ENV = 'pytest' in sys.modules or any(arg.endswith('pytest') or arg.endswith('py.test') for arg in sys.argv)
|
|
32
|
+
ENABLE_TRACEMALLOC = _ENV_TRACEMALLOC or _IS_TEST_ENV
|
|
33
|
+
|
|
34
|
+
# PERFORMANCE METRICS COLLECTOR
|
|
35
|
+
class PerformanceMetrics:
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.reset()
|
|
38
|
+
self.steps: List[Tuple[str, float]] = []
|
|
39
|
+
self._step_start: Optional[float] = None
|
|
40
|
+
self._current_step: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
def reset(self):
|
|
43
|
+
# TIMING METRICS
|
|
44
|
+
self.startup_start = None
|
|
45
|
+
self.startup_end = None
|
|
46
|
+
self.command_start = None
|
|
47
|
+
self.command_end = None
|
|
48
|
+
self.process_start = None
|
|
49
|
+
self.process_end = None
|
|
50
|
+
|
|
51
|
+
# CPU METRICS
|
|
52
|
+
self.cpu_user_start = None
|
|
53
|
+
self.cpu_sys_start = None
|
|
54
|
+
self.cpu_user_end = None
|
|
55
|
+
self.cpu_sys_end = None
|
|
56
|
+
|
|
57
|
+
# MEMORY METRICS
|
|
58
|
+
self.rss_start = None
|
|
59
|
+
self.rss_peak = None
|
|
60
|
+
|
|
61
|
+
# ALLOCATION TRACKING
|
|
62
|
+
self.tracemalloc_started = False
|
|
63
|
+
self.alloc_start = None
|
|
64
|
+
self.alloc_end = None
|
|
65
|
+
|
|
66
|
+
# GARBAGE COLLECTION METRICS
|
|
67
|
+
self.gc_start_stats = None
|
|
68
|
+
self.gc_end_stats = None
|
|
69
|
+
|
|
70
|
+
# FILESYSTEM I/O METRICS
|
|
71
|
+
self.fs_read_start = None
|
|
72
|
+
self.fs_write_start = None
|
|
73
|
+
self.fs_read_end = None
|
|
74
|
+
self.fs_write_end = None
|
|
75
|
+
self.fs_ops_start = None
|
|
76
|
+
self.fs_ops_end = None
|
|
77
|
+
|
|
78
|
+
# STEP TRACKING
|
|
79
|
+
self.steps = []
|
|
80
|
+
self._step_start = None
|
|
81
|
+
self._current_step = None
|
|
82
|
+
|
|
83
|
+
# STARTS PROCESS-LEVEL METRICS TRACKING
|
|
84
|
+
def start_process(self):
|
|
85
|
+
self.process_start = time.perf_counter()
|
|
86
|
+
self._record_cpu_start()
|
|
87
|
+
self._record_rss_start()
|
|
88
|
+
self._record_fs_start()
|
|
89
|
+
|
|
90
|
+
# STARTS STARTUP PHASE TRACKING
|
|
91
|
+
def start_startup(self):
|
|
92
|
+
self.startup_start = time.perf_counter()
|
|
93
|
+
if tracemalloc.is_tracing() and not self.tracemalloc_started:
|
|
94
|
+
self.tracemalloc_started = True
|
|
95
|
+
elif ENABLE_TRACEMALLOC and not self.tracemalloc_started:
|
|
96
|
+
tracemalloc.start(1)
|
|
97
|
+
self.tracemalloc_started = True
|
|
98
|
+
|
|
99
|
+
if self.tracemalloc_started and tracemalloc.is_tracing():
|
|
100
|
+
self.alloc_start = tracemalloc.take_snapshot()
|
|
101
|
+
self.gc_start_stats = self._get_gc_stats()
|
|
102
|
+
|
|
103
|
+
# ENDS STARTUP PHASE TRACKING
|
|
104
|
+
def end_startup(self):
|
|
105
|
+
self.startup_end = time.perf_counter()
|
|
106
|
+
|
|
107
|
+
# STARTS COMMAND EXECUTION TRACKING
|
|
108
|
+
def start_command(self):
|
|
109
|
+
self.command_start = time.perf_counter()
|
|
110
|
+
|
|
111
|
+
# ENDS COMMAND EXECUTION TRACKING
|
|
112
|
+
def end_command(self):
|
|
113
|
+
self.command_end = time.perf_counter()
|
|
114
|
+
|
|
115
|
+
# ENDS PROCESS-LEVEL METRICS TRACKING
|
|
116
|
+
def end_process(self):
|
|
117
|
+
self.process_end = time.perf_counter()
|
|
118
|
+
self._record_cpu_end()
|
|
119
|
+
self._record_rss_end()
|
|
120
|
+
self._record_fs_end()
|
|
121
|
+
if self.tracemalloc_started and tracemalloc.is_tracing():
|
|
122
|
+
self.alloc_end = tracemalloc.take_snapshot()
|
|
123
|
+
tracemalloc.stop()
|
|
124
|
+
self.gc_end_stats = self._get_gc_stats()
|
|
125
|
+
|
|
126
|
+
# STARTS TRACKING A NAMED STEP
|
|
127
|
+
def step_start(self, name: str):
|
|
128
|
+
if self._current_step:
|
|
129
|
+
self.step_end()
|
|
130
|
+
self._current_step = name
|
|
131
|
+
self._step_start = time.perf_counter()
|
|
132
|
+
|
|
133
|
+
# ENDS TRACKING THE CURRENT STEP
|
|
134
|
+
def step_end(self):
|
|
135
|
+
if self._current_step and self._step_start:
|
|
136
|
+
duration = time.perf_counter() - self._step_start
|
|
137
|
+
self.steps.append((self._current_step, duration))
|
|
138
|
+
self._current_step = None
|
|
139
|
+
self._step_start = None
|
|
140
|
+
|
|
141
|
+
# RECORDS CPU USAGE AT START
|
|
142
|
+
def _record_cpu_start(self):
|
|
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
|
|
155
|
+
|
|
156
|
+
# RECORDS CPU USAGE AT END
|
|
157
|
+
def _record_cpu_end(self):
|
|
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
|
|
170
|
+
|
|
171
|
+
# RECORDS MEMORY USAGE AT START
|
|
172
|
+
def _record_rss_start(self):
|
|
173
|
+
if PSUTIL_AVAILABLE:
|
|
174
|
+
process = psutil.Process()
|
|
175
|
+
self.rss_start = process.memory_info().rss / (1024 * 1024) # MB
|
|
176
|
+
elif RESOURCE_AVAILABLE:
|
|
177
|
+
usage = resource.getrusage(resource.RUSAGE_SELF)
|
|
178
|
+
self.rss_start = usage.ru_maxrss / 1024 # MB (LINUX) OR KB (MACOS)
|
|
179
|
+
if sys.platform == 'darwin':
|
|
180
|
+
self.rss_start = self.rss_start / 1024 # CONVERT KB TO MB ON MACOS
|
|
181
|
+
else:
|
|
182
|
+
self.rss_start = 0.0
|
|
183
|
+
|
|
184
|
+
# RECORDS MEMORY USAGE AT END
|
|
185
|
+
def _record_rss_end(self):
|
|
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
|
|
189
|
+
|
|
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
|
|
209
|
+
|
|
210
|
+
# RECORDS FILESYSTEM I/O AT START
|
|
211
|
+
def _record_fs_start(self):
|
|
212
|
+
if PSUTIL_AVAILABLE:
|
|
213
|
+
try:
|
|
214
|
+
process = psutil.Process()
|
|
215
|
+
io_counters = process.io_counters()
|
|
216
|
+
self.fs_read_start = io_counters.read_bytes
|
|
217
|
+
self.fs_write_start = io_counters.write_bytes
|
|
218
|
+
self.fs_ops_start = getattr(io_counters, 'read_count', 0) + getattr(io_counters, 'write_count', 0)
|
|
219
|
+
except (AttributeError, psutil.AccessDenied):
|
|
220
|
+
self.fs_read_start = 0
|
|
221
|
+
self.fs_write_start = 0
|
|
222
|
+
self.fs_ops_start = 0
|
|
223
|
+
else:
|
|
224
|
+
self.fs_read_start = 0
|
|
225
|
+
self.fs_write_start = 0
|
|
226
|
+
self.fs_ops_start = 0
|
|
227
|
+
|
|
228
|
+
# RECORDS FILESYSTEM I/O AT END
|
|
229
|
+
def _record_fs_end(self):
|
|
230
|
+
if PSUTIL_AVAILABLE:
|
|
231
|
+
try:
|
|
232
|
+
process = psutil.Process()
|
|
233
|
+
io_counters = process.io_counters()
|
|
234
|
+
self.fs_read_end = io_counters.read_bytes
|
|
235
|
+
self.fs_write_end = io_counters.write_bytes
|
|
236
|
+
self.fs_ops_end = getattr(io_counters, 'read_count', 0) + getattr(io_counters, 'write_count', 0)
|
|
237
|
+
except (AttributeError, psutil.AccessDenied):
|
|
238
|
+
self.fs_read_end = self.fs_read_start
|
|
239
|
+
self.fs_write_end = self.fs_write_start
|
|
240
|
+
self.fs_ops_end = self.fs_ops_start
|
|
241
|
+
else:
|
|
242
|
+
self.fs_read_end = self.fs_read_start
|
|
243
|
+
self.fs_write_end = self.fs_write_start
|
|
244
|
+
self.fs_ops_end = self.fs_ops_start
|
|
245
|
+
|
|
246
|
+
# GETS CURRENT GARBAGE COLLECTION STATISTICS
|
|
247
|
+
def _get_gc_stats(self) -> List[Dict]:
|
|
248
|
+
return gc.get_stats()
|
|
249
|
+
|
|
250
|
+
# CALCULATES DURATION METRICS IN MILLISECONDS
|
|
251
|
+
def _calculate_durations(self) -> Tuple[float, float, float]:
|
|
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
|
|
255
|
+
return total_duration_ms, startup_duration_ms, command_duration_ms
|
|
256
|
+
|
|
257
|
+
# CALCULATES CPU TIME METRICS IN MILLISECONDS
|
|
258
|
+
def _calculate_cpu_time(self) -> Tuple[float, float, float]:
|
|
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
|
|
261
|
+
cpu_time_total_ms = cpu_user_ms + cpu_sys_ms
|
|
262
|
+
return cpu_time_total_ms, cpu_user_ms, cpu_sys_ms
|
|
263
|
+
|
|
264
|
+
# CALCULATES TOTAL MEMORY ALLOCATIONS IN MB
|
|
265
|
+
def _calculate_allocations(self) -> float:
|
|
266
|
+
alloc_mb_total = 0
|
|
267
|
+
if self.alloc_start and self.alloc_end:
|
|
268
|
+
diff = self.alloc_end.compare_to(self.alloc_start, 'lineno')
|
|
269
|
+
alloc_bytes = sum(stat.size_diff for stat in diff if stat.size_diff > 0)
|
|
270
|
+
alloc_mb_total = alloc_bytes / (1024 * 1024)
|
|
271
|
+
return alloc_mb_total
|
|
272
|
+
|
|
273
|
+
# CALCULATES GARBAGE COLLECTION METRICS
|
|
274
|
+
def _calculate_gc_stats(self) -> Tuple[float, int]:
|
|
275
|
+
gc_pause_total_ms = 0
|
|
276
|
+
gc_collections_count = 0
|
|
277
|
+
if self.gc_start_stats and self.gc_end_stats:
|
|
278
|
+
end_stats = gc.get_stats()
|
|
279
|
+
start_stats = self.gc_start_stats
|
|
280
|
+
|
|
281
|
+
if len(start_stats) == len(end_stats):
|
|
282
|
+
for start_stat, end_stat in zip(start_stats, end_stats):
|
|
283
|
+
start_collections = start_stat.get('collections', 0)
|
|
284
|
+
end_collections = end_stat.get('collections', 0)
|
|
285
|
+
gc_collections_count += max(0, end_collections - start_collections)
|
|
286
|
+
|
|
287
|
+
start_time = start_stat.get('total_time', 0)
|
|
288
|
+
end_time = end_stat.get('total_time', 0)
|
|
289
|
+
gc_pause_total_ms += max(0, (end_time - start_time) * 1000) # CONVERT TO MS
|
|
290
|
+
|
|
291
|
+
return gc_pause_total_ms, gc_collections_count
|
|
292
|
+
|
|
293
|
+
# CALCULATES FILESYSTEM I/O METRICS
|
|
294
|
+
def _calculate_fs_io(self) -> Tuple[int, int, int]:
|
|
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
|
|
298
|
+
return fs_bytes_read_total, fs_bytes_written_total, fs_ops_count
|
|
299
|
+
|
|
300
|
+
# CALCULATES AND RETURNS ALL PERFORMANCE METRICS AS A DICTIONARY
|
|
301
|
+
def get_metrics(self) -> Dict:
|
|
302
|
+
if self._current_step: self.step_end()
|
|
303
|
+
|
|
304
|
+
# CALCULATE ALL METRICS
|
|
305
|
+
total_duration_ms, startup_duration_ms, command_duration_ms = self._calculate_durations()
|
|
306
|
+
cpu_time_total_ms, cpu_user_ms, cpu_sys_ms = self._calculate_cpu_time()
|
|
307
|
+
alloc_mb_total = self._calculate_allocations()
|
|
308
|
+
gc_pause_total_ms, gc_collections_count = self._calculate_gc_stats()
|
|
309
|
+
fs_bytes_read_total, fs_bytes_written_total, fs_ops_count = self._calculate_fs_io()
|
|
310
|
+
|
|
311
|
+
# MEMORY
|
|
312
|
+
rss_mb_peak = self.rss_peak if self.rss_peak else 0
|
|
313
|
+
|
|
314
|
+
# TOP SLOWEST STEPS
|
|
315
|
+
top_slowest_steps = sorted(self.steps, key=lambda x: x[1], reverse=True)[:5]
|
|
316
|
+
top_slowest_steps_formatted = [
|
|
317
|
+
{'step': name, 'duration_ms': duration * 1000}
|
|
318
|
+
for name, duration in top_slowest_steps
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
'total_duration_ms': round(total_duration_ms, 2),
|
|
323
|
+
'startup_duration_ms': round(startup_duration_ms, 2),
|
|
324
|
+
'command_duration_ms': round(command_duration_ms, 2),
|
|
325
|
+
'top_slowest_steps': top_slowest_steps_formatted,
|
|
326
|
+
'cpu_time_total_ms': round(cpu_time_total_ms, 2),
|
|
327
|
+
'cpu_user_ms': round(cpu_user_ms, 2),
|
|
328
|
+
'cpu_sys_ms': round(cpu_sys_ms, 2),
|
|
329
|
+
'rss_mb_peak': round(rss_mb_peak, 2),
|
|
330
|
+
'alloc_mb_total': round(alloc_mb_total, 2),
|
|
331
|
+
'gc_pause_total_ms': round(gc_pause_total_ms, 2),
|
|
332
|
+
'gc_collections_count': gc_collections_count,
|
|
333
|
+
'fs_bytes_read_total': fs_bytes_read_total,
|
|
334
|
+
'fs_bytes_written_total': fs_bytes_written_total,
|
|
335
|
+
'fs_ops_count': fs_ops_count
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# GLOBAL METRICS INSTANCE
|
|
339
|
+
_metrics = PerformanceMetrics()
|
|
340
|
+
|
|
341
|
+
# CONTEXT MANAGER FOR TRACKING NAMED STEPS
|
|
342
|
+
@contextmanager
|
|
343
|
+
def track_step(name: str):
|
|
344
|
+
_metrics.step_start(name)
|
|
345
|
+
try:
|
|
346
|
+
yield
|
|
347
|
+
finally:
|
|
348
|
+
_metrics.step_end()
|
|
349
|
+
|
|
350
|
+
# CHECKS IF PERFORMANCE METRICS SHOULD BE ENABLED
|
|
351
|
+
def should_enable_metrics(ctx) -> bool:
|
|
352
|
+
current = ctx
|
|
353
|
+
while current:
|
|
354
|
+
if current.params.get('perf', False): return True
|
|
355
|
+
current = getattr(current, 'parent', None)
|
|
356
|
+
|
|
357
|
+
if not ENABLE_PERFORMANCE_METRICS: return False
|
|
358
|
+
|
|
359
|
+
module = sys.modules.get('autotools')
|
|
360
|
+
module_file = getattr(module, '__file__', '') or ''
|
|
361
|
+
is_dev = module and 'site-packages' not in module_file.lower()
|
|
362
|
+
|
|
363
|
+
return is_dev
|
|
364
|
+
|
|
365
|
+
# DISPLAYS PERFORMANCE METRICS IN A FORMATTED WAY
|
|
366
|
+
def display_metrics(metrics: Dict):
|
|
367
|
+
click.echo(click.style("\n" + "="*60, fg='cyan'))
|
|
368
|
+
click.echo(click.style("PERFORMANCE METRICS", fg='cyan', bold=True))
|
|
369
|
+
click.echo(click.style("="*60, fg='cyan'))
|
|
370
|
+
|
|
371
|
+
# DURATION METRICS
|
|
372
|
+
click.echo(click.style("\nDURATION METRICS:", fg='yellow', bold=True))
|
|
373
|
+
click.echo(f" Total Duration: {metrics['total_duration_ms']:.2f} ms")
|
|
374
|
+
click.echo(f" Startup Duration: {metrics['startup_duration_ms']:.2f} ms")
|
|
375
|
+
click.echo(f" Command Duration: {metrics['command_duration_ms']:.2f} ms")
|
|
376
|
+
|
|
377
|
+
# CPU METRICS
|
|
378
|
+
click.echo(click.style("\nCPU METRICS:", fg='yellow', bold=True))
|
|
379
|
+
click.echo(f" CPU Time Total: {metrics['cpu_time_total_ms']:.2f} ms")
|
|
380
|
+
click.echo(f" CPU User Time: {metrics['cpu_user_ms']:.2f} ms")
|
|
381
|
+
click.echo(f" CPU System Time: {metrics['cpu_sys_ms']:.2f} ms")
|
|
382
|
+
cpu_ratio = (metrics['cpu_time_total_ms'] / metrics['total_duration_ms'] * 100) if metrics['total_duration_ms'] > 0 else 0
|
|
383
|
+
click.echo(f" CPU Usage Ratio: {cpu_ratio:.1f}%")
|
|
384
|
+
|
|
385
|
+
# MEMORY METRICS
|
|
386
|
+
click.echo(click.style("\nMEMORY METRICS:", fg='yellow', bold=True))
|
|
387
|
+
click.echo(f" RSS Peak: {metrics['rss_mb_peak']:.2f} MB")
|
|
388
|
+
click.echo(f" Allocations Total: {metrics['alloc_mb_total']:.2f} MB")
|
|
389
|
+
|
|
390
|
+
# GC METRICS
|
|
391
|
+
if metrics['gc_collections_count'] > 0 or metrics['gc_pause_total_ms'] > 0:
|
|
392
|
+
click.echo(click.style("\nGARBAGE COLLECTION METRICS:", fg='yellow', bold=True))
|
|
393
|
+
click.echo(f" GC Pause Total: {metrics['gc_pause_total_ms']:.2f} ms")
|
|
394
|
+
click.echo(f" GC Collections: {metrics['gc_collections_count']}")
|
|
395
|
+
|
|
396
|
+
# FS I/O METRICS
|
|
397
|
+
if metrics['fs_bytes_read_total'] > 0 or metrics['fs_bytes_written_total'] > 0:
|
|
398
|
+
click.echo(click.style("\nFILESYSTEM I/O METRICS:", fg='yellow', bold=True))
|
|
399
|
+
click.echo(f" Bytes Read: {metrics['fs_bytes_read_total']:,} bytes ({metrics['fs_bytes_read_total'] / 1024 / 1024:.2f} MB)")
|
|
400
|
+
click.echo(f" Bytes Written: {metrics['fs_bytes_written_total']:,} bytes ({metrics['fs_bytes_written_total'] / 1024 / 1024:.2f} MB)")
|
|
401
|
+
click.echo(f" FS Operations: {metrics['fs_ops_count']:,}")
|
|
402
|
+
|
|
403
|
+
# TOP SLOWEST STEPS
|
|
404
|
+
if metrics['top_slowest_steps']:
|
|
405
|
+
click.echo(click.style("\nTOP SLOWEST STEPS:", fg='yellow', bold=True))
|
|
406
|
+
for i, step in enumerate(metrics['top_slowest_steps'], 1): click.echo(f" {i}. {step['step']}: {step['duration_ms']:.2f} ms")
|
|
407
|
+
|
|
408
|
+
click.echo(click.style("\n" + "="*60 + "\n", fg='cyan'))
|
|
409
|
+
|
|
410
|
+
# INITIALIZES METRICS TRACKING
|
|
411
|
+
def init_metrics():
|
|
412
|
+
_metrics.reset()
|
|
413
|
+
_metrics.start_process()
|
|
414
|
+
_metrics.start_startup()
|
|
415
|
+
|
|
416
|
+
# FINALIZES AND DISPLAYS METRICS IF ENABLED
|
|
417
|
+
def finalize_metrics(ctx):
|
|
418
|
+
if should_enable_metrics(ctx):
|
|
419
|
+
_metrics.end_process()
|
|
420
|
+
metrics = _metrics.get_metrics()
|
|
421
|
+
display_metrics(metrics)
|
|
422
|
+
|
|
423
|
+
# GETS THE GLOBAL METRICS INSTANCE
|
|
424
|
+
def get_metrics() -> PerformanceMetrics: return _metrics
|
|
@@ -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/text.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
# ENSURE TEXT IS SAFE TO WRITE TO THE CURRENT STDOUT ENCODING
|
|
5
|
+
# SOME WINDOWS TERMINALS USE LEGACY ENCODINGS THAT CANNOT ENCODE CERTAIN CHARACTERS
|
|
6
|
+
def safe_text(text: Any) -> Any:
|
|
7
|
+
if not isinstance(text, str): return text
|
|
8
|
+
|
|
9
|
+
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
text.encode(encoding)
|
|
13
|
+
return text
|
|
14
|
+
except Exception:
|
|
15
|
+
try: return text.encode(encoding, errors="replace").decode(encoding)
|
|
16
|
+
except Exception: return text.encode("ascii", errors="replace").decode("ascii")
|
autotools/utils/updates.py
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
1
3
|
import click
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
from importlib.metadata import version as get_version, distribution, PackageNotFoundError
|
|
4
7
|
from packaging.version import parse as parse_version
|
|
5
|
-
import pkg_resources
|
|
6
8
|
|
|
9
|
+
# CHECKS PYPI FOR AVAILABLE UPDATES TO THE PACKAGE
|
|
7
10
|
def check_for_updates():
|
|
8
|
-
|
|
11
|
+
# SKIP UPDATE CHECK IN TEST ENVIRONMENT
|
|
12
|
+
if os.getenv('PYTEST_CURRENT_TEST') or os.getenv('CI'): return None
|
|
13
|
+
|
|
9
14
|
try:
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
try:
|
|
16
|
+
dist = distribution("Open-AutoTools")
|
|
17
|
+
current_version = parse_version(dist.version)
|
|
18
|
+
except PackageNotFoundError:
|
|
19
|
+
return None
|
|
12
20
|
|
|
13
21
|
pypi_url = "https://pypi.org/pypi/Open-AutoTools/json"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
except
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
req = urllib.request.Request(pypi_url)
|
|
23
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
24
|
+
if response.status == 200:
|
|
25
|
+
data = json.loads(response.read().decode())
|
|
26
|
+
latest_version = data["info"]["version"]
|
|
27
|
+
latest_parsed = parse_version(latest_version)
|
|
28
|
+
|
|
29
|
+
if latest_parsed > current_version:
|
|
30
|
+
update_cmd = "pip install --upgrade Open-AutoTools"
|
|
31
|
+
return (
|
|
32
|
+
click.style(f"\nUpdate available: v{latest_version}", fg='red', bold=True) + "\n" +
|
|
33
|
+
click.style(f"Run '{update_cmd}' to update", fg='red')
|
|
34
|
+
)
|
|
35
|
+
except urllib.error.URLError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
30
38
|
return None
|
autotools/utils/version.py
CHANGED
|
@@ -1,74 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
1
4
|
import click
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import requests
|
|
5
|
-
from packaging.version import parse as parse_version
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.error
|
|
6
7
|
from datetime import datetime
|
|
8
|
+
from packaging.version import parse as parse_version
|
|
9
|
+
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
10
|
+
|
|
11
|
+
# FORMATS AND DISPLAYS RELEASE DATE
|
|
12
|
+
def _format_release_date(upload_time, pkg_version):
|
|
13
|
+
date_formats = ["%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%d %H:%M:%S"]
|
|
14
|
+
for date_format in date_formats:
|
|
15
|
+
try:
|
|
16
|
+
published_date = datetime.strptime(upload_time, date_format)
|
|
17
|
+
formatted_date = published_date.strftime("%d %B %Y at %H:%M:%S")
|
|
18
|
+
prefix = "Pre-Released" if "rc" in pkg_version.lower() else "Released"
|
|
19
|
+
click.echo(f"{prefix}: {formatted_date}")
|
|
20
|
+
return True
|
|
21
|
+
except ValueError:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
# DISPLAYS RELEASE INFORMATION FROM PYPI DATA
|
|
27
|
+
def _display_release_info(data, pkg_version):
|
|
28
|
+
releases = data["releases"]
|
|
29
|
+
if pkg_version in releases and releases[pkg_version]:
|
|
30
|
+
try:
|
|
31
|
+
upload_time = releases[pkg_version][0]["upload_time"]
|
|
32
|
+
_format_release_date(upload_time, pkg_version)
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# CHECKS AND DISPLAYS UPDATE INFORMATION
|
|
37
|
+
def _check_for_updates(current_version, latest_version):
|
|
38
|
+
latest_parsed = parse_version(latest_version)
|
|
39
|
+
if latest_parsed > current_version:
|
|
40
|
+
update_cmd = "pip install --upgrade Open-AutoTools"
|
|
41
|
+
click.echo(click.style(f"\nUpdate available: v{latest_version}", fg='red', bold=True))
|
|
42
|
+
click.echo(click.style(f"Run '{update_cmd}' to update", fg='red'))
|
|
43
|
+
|
|
44
|
+
# FETCHES AND PROCESSES PYPI VERSION INFORMATION
|
|
45
|
+
def _fetch_pypi_version_info(pkg_version):
|
|
46
|
+
pypi_url = "https://pypi.org/pypi/Open-AutoTools/json"
|
|
47
|
+
try:
|
|
48
|
+
req = urllib.request.Request(pypi_url)
|
|
49
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
50
|
+
if response.status != 200:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
data = json.loads(response.read().decode())
|
|
54
|
+
latest_version = data["info"]["version"]
|
|
55
|
+
current_version = parse_version(pkg_version)
|
|
56
|
+
|
|
57
|
+
_display_release_info(data, pkg_version)
|
|
58
|
+
_check_for_updates(current_version, latest_version)
|
|
59
|
+
except urllib.error.URLError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# PRINTS VERSION INFORMATION AND CHECKS FOR UPDATES
|
|
63
|
+
def print_version(ctx, value):
|
|
64
|
+
if not value or ctx.resilient_parsing: return
|
|
7
65
|
|
|
8
|
-
# VERSION CALLBACK
|
|
9
|
-
def print_version(ctx, param, value):
|
|
10
|
-
"""PRINT VERSION AND CHECK FOR UPDATES"""
|
|
11
|
-
if not value or ctx.resilient_parsing:
|
|
12
|
-
return
|
|
13
|
-
|
|
14
66
|
try:
|
|
15
|
-
# GET CURRENT VERSION
|
|
16
67
|
pkg_version = get_version('Open-AutoTools')
|
|
17
68
|
click.echo(f"Open-AutoTools version {pkg_version}")
|
|
18
69
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
70
|
+
module = sys.modules.get('autotools')
|
|
71
|
+
module_file = getattr(module, '__file__', '') or ''
|
|
72
|
+
if module and 'site-packages' not in module_file.lower():
|
|
73
|
+
click.echo(click.style("Development mode: enabled", fg='yellow', bold=True))
|
|
22
74
|
|
|
23
|
-
|
|
24
|
-
pypi_url = "https://pypi.org/pypi/Open-AutoTools/json"
|
|
25
|
-
response = requests.get(pypi_url)
|
|
26
|
-
|
|
27
|
-
# CHECK IF RESPONSE IS SUCCESSFUL
|
|
28
|
-
if response.status_code == 200:
|
|
29
|
-
data = response.json()
|
|
30
|
-
latest_version = data["info"]["version"]
|
|
31
|
-
releases = data["releases"]
|
|
32
|
-
|
|
33
|
-
# GET RELEASE DATE
|
|
34
|
-
if pkg_version in releases and releases[pkg_version]:
|
|
35
|
-
try:
|
|
36
|
-
upload_time = releases[pkg_version][0]["upload_time"]
|
|
37
|
-
for date_format in [
|
|
38
|
-
"%Y-%m-%dT%H:%M:%S",
|
|
39
|
-
"%Y-%m-%dT%H:%M:%S.%fZ",
|
|
40
|
-
"%Y-%m-%d %H:%M:%S"
|
|
41
|
-
]:
|
|
42
|
-
# TRY TO PARSE DATE
|
|
43
|
-
try:
|
|
44
|
-
published_date = datetime.strptime(upload_time, date_format)
|
|
45
|
-
formatted_date = published_date.strftime("%d %B %Y at %H:%M:%S")
|
|
46
|
-
# CHECK IF VERSION IS RC
|
|
47
|
-
if "rc" in pkg_version.lower():
|
|
48
|
-
click.echo(f"Pre-Released: {formatted_date}")
|
|
49
|
-
else:
|
|
50
|
-
click.echo(f"Released: {formatted_date}")
|
|
51
|
-
break
|
|
52
|
-
except ValueError:
|
|
53
|
-
continue
|
|
54
|
-
except Exception:
|
|
55
|
-
pass # SKIP DATE IF PARSING FAILS
|
|
56
|
-
|
|
57
|
-
# CHECK FOR UPDATES
|
|
58
|
-
latest_parsed = parse_version(latest_version)
|
|
59
|
-
|
|
60
|
-
# COMPARE VERSIONS AND PRINT UPDATE MESSAGE IF NEEDED
|
|
61
|
-
if latest_parsed > current_version:
|
|
62
|
-
update_cmd = "pip install --upgrade Open-AutoTools"
|
|
63
|
-
click.echo(click.style(f"\nUpdate available: v{latest_version}", fg='red', bold=True))
|
|
64
|
-
click.echo(click.style(f"Run '{update_cmd}' to update", fg='red'))
|
|
75
|
+
_fetch_pypi_version_info(pkg_version)
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
except
|
|
68
|
-
click.echo("Package distribution not found")
|
|
69
|
-
except PackageNotFoundError:
|
|
70
|
-
click.echo("Open-AutoTools version information not available")
|
|
71
|
-
except Exception as e:
|
|
72
|
-
click.echo(f"Error checking updates: {str(e)}")
|
|
77
|
+
except PackageNotFoundError: click.echo("Open-AutoTools version information not available")
|
|
78
|
+
except Exception as e: click.echo(f"Error checking updates: {str(e)}")
|
|
73
79
|
|
|
74
80
|
ctx.exit()
|