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.
Files changed (46) hide show
  1. autotools/autocaps/commands.py +3 -7
  2. autotools/autocaps/core.py +5 -4
  3. autotools/autoip/commands.py +8 -12
  4. autotools/autoip/core.py +151 -200
  5. autotools/autolower/commands.py +3 -7
  6. autotools/autolower/core.py +4 -3
  7. autotools/autopassword/commands.py +27 -33
  8. autotools/autopassword/core.py +32 -73
  9. autotools/autotest/__init__.py +2 -0
  10. autotools/autotest/commands.py +206 -0
  11. autotools/cli.py +123 -62
  12. autotools/utils/commands.py +13 -0
  13. autotools/utils/loading.py +14 -6
  14. autotools/utils/performance.py +424 -0
  15. autotools/utils/requirements.py +21 -0
  16. autotools/utils/text.py +16 -0
  17. autotools/utils/updates.py +30 -22
  18. autotools/utils/version.py +69 -63
  19. open_autotools-0.0.4.dist-info/METADATA +84 -0
  20. open_autotools-0.0.4.dist-info/RECORD +30 -0
  21. {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info}/WHEEL +1 -1
  22. {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info}/entry_points.txt +0 -3
  23. Open_AutoTools-0.0.3rc4.dist-info/METADATA +0 -308
  24. Open_AutoTools-0.0.3rc4.dist-info/RECORD +0 -44
  25. autotools/autocaps/tests/__init__.py +0 -1
  26. autotools/autocaps/tests/test_autocaps_core.py +0 -45
  27. autotools/autocaps/tests/test_autocaps_integration.py +0 -46
  28. autotools/autodownload/__init__.py +0 -0
  29. autotools/autodownload/commands.py +0 -38
  30. autotools/autodownload/core.py +0 -373
  31. autotools/autoip/tests/__init__.py +0 -1
  32. autotools/autoip/tests/test_autoip_core.py +0 -72
  33. autotools/autoip/tests/test_autoip_integration.py +0 -92
  34. autotools/autolower/tests/__init__.py +0 -1
  35. autotools/autolower/tests/test_autolower_core.py +0 -45
  36. autotools/autolower/tests/test_autolower_integration.py +0 -46
  37. autotools/autospell/__init__.py +0 -3
  38. autotools/autospell/commands.py +0 -123
  39. autotools/autospell/core.py +0 -222
  40. autotools/autotranslate/__init__.py +0 -3
  41. autotools/autotranslate/commands.py +0 -42
  42. autotools/autotranslate/core.py +0 -52
  43. autotools/test/__init__.py +0 -3
  44. autotools/test/commands.py +0 -120
  45. {Open_AutoTools-0.0.3rc4.dist-info → open_autotools-0.0.4.dist-info/licenses}/LICENSE +0 -0
  46. {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 []
@@ -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")
@@ -1,30 +1,38 @@
1
+ import os
2
+ import json
1
3
  import click
2
- import requests
3
- from importlib.metadata import version as get_version
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
- """CHECK IF AN UPDATE IS AVAILABLE AND RETURN UPDATE MESSAGE IF NEEDED"""
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
- dist = pkg_resources.get_distribution("Open-AutoTools")
11
- current_version = parse_version(dist.version)
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
- response = requests.get(pypi_url)
15
-
16
- if response.status_code == 200:
17
- data = response.json()
18
- latest_version = data["info"]["version"]
19
- latest_parsed = parse_version(latest_version)
20
-
21
- if latest_parsed > current_version:
22
- update_cmd = "pip install --upgrade Open-AutoTools"
23
- return (
24
- click.style(f"\nUpdate available: v{latest_version}", fg='red', bold=True) + "\n" +
25
- click.style(f"Run '{update_cmd}' to update", fg='red')
26
- )
27
- except Exception as e:
28
- print(f"Error checking updates: {str(e)}")
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
@@ -1,74 +1,80 @@
1
+ import os
2
+ import sys
3
+ import json
1
4
  import click
2
- from importlib.metadata import version as get_version, PackageNotFoundError
3
- import pkg_resources
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
- # GET DISTRIBUTION INFO
20
- dist = pkg_resources.get_distribution("Open-AutoTools")
21
- current_version = parse_version(dist.version)
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
- # GET LATEST VERSION FROM PYPI
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
- # EXCEPTIONS FOR ERRORS
67
- except pkg_resources.DistributionNotFound:
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()