Open-AutoTools 0.0.4rc1__py3-none-any.whl → 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. autotools/autocaps/commands.py +21 -0
  2. autotools/autocolor/__init__.py +0 -0
  3. autotools/autocolor/commands.py +60 -0
  4. autotools/autocolor/core.py +99 -0
  5. autotools/autoconvert/__init__.py +0 -0
  6. autotools/autoconvert/commands.py +79 -0
  7. autotools/autoconvert/conversion/__init__.py +0 -0
  8. autotools/autoconvert/conversion/convert_audio.py +24 -0
  9. autotools/autoconvert/conversion/convert_image.py +29 -0
  10. autotools/autoconvert/conversion/convert_text.py +101 -0
  11. autotools/autoconvert/conversion/convert_video.py +25 -0
  12. autotools/autoconvert/core.py +54 -0
  13. autotools/autoip/commands.py +39 -1
  14. autotools/autoip/core.py +100 -43
  15. autotools/autolower/commands.py +21 -0
  16. autotools/autonote/__init__.py +0 -0
  17. autotools/autonote/commands.py +70 -0
  18. autotools/autonote/core.py +106 -0
  19. autotools/autopassword/commands.py +39 -1
  20. autotools/autotest/commands.py +43 -12
  21. autotools/autotodo/__init__.py +87 -0
  22. autotools/autotodo/commands.py +115 -0
  23. autotools/autotodo/core.py +567 -0
  24. autotools/autounit/__init__.py +0 -0
  25. autotools/autounit/commands.py +55 -0
  26. autotools/autounit/core.py +36 -0
  27. autotools/autozip/__init__.py +0 -0
  28. autotools/autozip/commands.py +88 -0
  29. autotools/autozip/core.py +107 -0
  30. autotools/cli.py +66 -62
  31. autotools/utils/commands.py +141 -10
  32. autotools/utils/performance.py +67 -35
  33. autotools/utils/requirements.py +21 -0
  34. autotools/utils/smoke.py +246 -0
  35. autotools/utils/text.py +73 -0
  36. open_autotools-0.0.5.dist-info/METADATA +100 -0
  37. open_autotools-0.0.5.dist-info/RECORD +54 -0
  38. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/WHEEL +1 -1
  39. open_autotools-0.0.5.dist-info/entry_points.txt +12 -0
  40. open_autotools-0.0.4rc1.dist-info/METADATA +0 -103
  41. open_autotools-0.0.4rc1.dist-info/RECORD +0 -28
  42. open_autotools-0.0.4rc1.dist-info/entry_points.txt +0 -6
  43. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/licenses/LICENSE +0 -0
  44. {open_autotools-0.0.4rc1.dist-info → open_autotools-0.0.5.dist-info}/top_level.txt +0 -0
autotools/autoip/core.py CHANGED
@@ -2,16 +2,27 @@ import socket
2
2
  import requests
3
3
  import json
4
4
  import ipaddress
5
- import netifaces
6
5
  import time
7
6
  import speedtest
8
7
  import psutil
8
+ from ..utils.text import is_ci_environment, mask_ipv4, mask_ipv6, mask_sensitive_info
9
+
10
+ # NORMALIZES psutil.net_if_addrs() OUTPUT TO A netifaces-LIKE STRUCTURE:
11
+ def _psutil_addrs_to_family_map(addrs):
12
+ family_map = {}
13
+ for snic in addrs or []:
14
+ family = getattr(snic, 'family', None)
15
+ addr = getattr(snic, 'address', None)
16
+ if not addr: continue
17
+ if family not in (socket.AF_INET, socket.AF_INET6): continue
18
+ family_map.setdefault(family, []).append({'addr': addr})
19
+ return family_map
9
20
 
10
21
  # EXTRACTS IPV4 ADDRESSES FROM INTERFACE ADDRESSES
11
22
  def _extract_ipv4_addresses(addrs):
12
23
  ipv4_list = []
13
- if netifaces.AF_INET in addrs:
14
- for addr in addrs[netifaces.AF_INET]:
24
+ if socket.AF_INET in addrs:
25
+ for addr in addrs[socket.AF_INET]:
15
26
  if 'addr' in addr and not addr['addr'].startswith('127.'):
16
27
  ipv4_list.append(addr['addr'])
17
28
  return ipv4_list
@@ -19,9 +30,9 @@ def _extract_ipv4_addresses(addrs):
19
30
  # EXTRACTS IPV6 ADDRESSES FROM INTERFACE ADDRESSES
20
31
  def _extract_ipv6_addresses(addrs):
21
32
  ipv6_list = []
22
- if netifaces.AF_INET6 in addrs:
23
- for addr in addrs[netifaces.AF_INET6]:
24
- if 'addr' in addr and not addr['addr'].startswith('fe80:'):
33
+ if socket.AF_INET6 in addrs:
34
+ for addr in addrs[socket.AF_INET6]:
35
+ if 'addr' in addr and not addr['addr'].lower().startswith('fe80:'):
25
36
  clean_addr = addr['addr'].split('%')[0]
26
37
  ipv6_list.append(clean_addr)
27
38
  return ipv6_list
@@ -30,10 +41,13 @@ def _extract_ipv6_addresses(addrs):
30
41
  def get_local_ips():
31
42
  ips = {'ipv4': [], 'ipv6': []}
32
43
 
33
- for interface in netifaces.interfaces():
34
- addrs = netifaces.ifaddresses(interface)
35
- ips['ipv4'].extend(_extract_ipv4_addresses(addrs))
36
- ips['ipv6'].extend(_extract_ipv6_addresses(addrs))
44
+ try: if_addrs = psutil.net_if_addrs()
45
+ except Exception: return ips
46
+
47
+ for _, addrs in (if_addrs or {}).items():
48
+ family_map = _psutil_addrs_to_family_map(addrs)
49
+ ips['ipv4'].extend(_extract_ipv4_addresses(family_map))
50
+ ips['ipv6'].extend(_extract_ipv6_addresses(family_map))
37
51
 
38
52
  return ips
39
53
 
@@ -122,20 +136,26 @@ def get_public_ip():
122
136
 
123
137
  # GETS LOCAL IP ADDRESS OF DEFAULT NETWORK INTERFACE
124
138
  def get_local_ip():
139
+ # TRY psutil FIRST (NO NETWORK NEEDED)
125
140
  try:
126
- gateways = netifaces.gateways()
127
- default_interface = gateways['default'][netifaces.AF_INET][1]
128
- addrs = netifaces.ifaddresses(default_interface)
129
- return addrs[netifaces.AF_INET][0]['addr']
130
- except (KeyError, IndexError, OSError):
131
- try:
132
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
133
- s.connect(("8.8.8.8", 80))
134
- ip = s.getsockname()[0]
135
- s.close()
136
- return ip
137
- except OSError:
138
- return None
141
+ if_addrs = psutil.net_if_addrs()
142
+ for _, addrs in (if_addrs or {}).items():
143
+ for snic in addrs or []:
144
+ if getattr(snic, 'family', None) != socket.AF_INET: continue
145
+ addr = getattr(snic, 'address', None)
146
+ if addr and not addr.startswith('127.'): return addr
147
+ except Exception:
148
+ pass
149
+
150
+ # FALLBACK: UDP SOCKET TRICK
151
+ try:
152
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
153
+ s.connect(("8.8.8.8", 80))
154
+ ip = s.getsockname()[0]
155
+ s.close()
156
+ return ip
157
+ except OSError:
158
+ return None
139
159
 
140
160
  # RETRIEVES DETAILED INFORMATION ABOUT AN IP ADDRESS
141
161
  def get_ip_info(ip=None):
@@ -152,59 +172,93 @@ def get_ip_info(ip=None):
152
172
  response = requests.get(url)
153
173
  data = response.json()
154
174
 
155
- if 'error' in data: raise ValueError(f"Error getting IP info: {data['error']}")
175
+ if 'error' in data:
176
+ raise ValueError(f"Error getting IP info: {data['error']}")
156
177
 
157
178
  return data
158
179
  except requests.RequestException as e:
159
180
  raise ValueError(f"Error connecting to IP info service: {str(e)}")
160
181
 
161
- # DISPLAYS LOCAL AND PUBLIC IP ADDRESSES
162
- def _display_ip_addresses(output):
163
- local_ips = get_local_ips()
164
- public_ips = get_public_ips()
165
- output.append("\nLocal IPs:")
182
+ # FORMATS IP ADDRESS FOR DISPLAY (MASKS IF IN CI)
183
+ def _format_ip_for_display(ip, mask_func, in_ci):
184
+ if not ip: return 'Not available'
185
+ return mask_func(ip) if in_ci else ip
166
186
 
187
+ # DISPLAYS LOCAL IP ADDRESSES
188
+ def _display_local_ips(output, local_ips, in_ci):
189
+ output.append("\nLocal IPs:")
167
190
  if local_ips['ipv4']:
168
- for ip in local_ips['ipv4']: output.append(f"IPv4: {ip}")
191
+ for ip in local_ips['ipv4']:
192
+ display_ip = _format_ip_for_display(ip, mask_ipv4, in_ci)
193
+ output.append(f"IPv4: {display_ip}")
169
194
  else:
170
195
  output.append("IPv4: Not available")
171
-
172
196
  if local_ips['ipv6']:
173
- for ip in local_ips['ipv6']: output.append(f"IPv6: {ip}")
197
+ for ip in local_ips['ipv6']:
198
+ display_ip = _format_ip_for_display(ip, mask_ipv6, in_ci)
199
+ output.append(f"IPv6: {display_ip}")
174
200
  else:
175
201
  output.append("IPv6: Not available")
176
-
202
+
203
+ # DISPLAYS PUBLIC IP ADDRESSES
204
+ def _display_public_ips(output, public_ips, in_ci):
177
205
  output.append("\nPublic IPs:")
178
- output.append(f"IPv4: {public_ips['ipv4'] or 'Not available'}")
179
- output.append(f"IPv6: {public_ips['ipv6'] or 'Not available'}")
206
+ public_ipv4 = _format_ip_for_display(public_ips['ipv4'], mask_ipv4, in_ci)
207
+ public_ipv6 = _format_ip_for_display(public_ips['ipv6'], mask_ipv6, in_ci)
208
+ output.append(f"IPv4: {public_ipv4}")
209
+ output.append(f"IPv6: {public_ipv6}")
210
+
211
+ # DISPLAYS LOCAL AND PUBLIC IP ADDRESSES
212
+ def _display_ip_addresses(output):
213
+ local_ips = get_local_ips()
214
+ public_ips = get_public_ips()
215
+ in_ci = is_ci_environment()
216
+ _display_local_ips(output, local_ips, in_ci)
217
+ _display_public_ips(output, public_ips, in_ci)
180
218
 
181
219
  # DISPLAYS CONNECTIVITY TEST RESULTS
182
220
  def _display_connectivity_tests(output):
183
221
  output.append("\nConnectivity Tests:")
184
222
  results = test_connectivity()
185
223
  for name, success, latency in results:
186
- status = f" {latency}ms" if success else " Failed"
224
+ status = f"OK {latency}ms" if success else "X Failed"
187
225
  output.append(f"{name:<15} {status}")
188
226
 
189
227
  # DISPLAYS LOCATION INFORMATION
190
228
  def _display_location_info(output):
191
229
  try:
192
230
  loc = get_ip_info()
231
+ in_ci = is_ci_environment()
193
232
  output.append("\nLocation Info:")
194
- output.append(f"City: {loc.get('city', 'Unknown')}")
195
- output.append(f"Region: {loc.get('region', 'Unknown')}")
196
- output.append(f"Country: {loc.get('country', 'Unknown')}")
197
- output.append(f"ISP: {loc.get('org', 'Unknown')}")
233
+
234
+ if in_ci:
235
+ output.append("City: [REDACTED]")
236
+ output.append("Region: [REDACTED]")
237
+ output.append("Country: [REDACTED]")
238
+ output.append("ISP: [REDACTED]")
239
+ else:
240
+ output.append(f"City: {loc.get('city', 'Unknown')}")
241
+ output.append(f"Region: {loc.get('region', 'Unknown')}")
242
+ output.append(f"Country: {loc.get('country', 'Unknown')}")
243
+ output.append(f"ISP: {loc.get('org', 'Unknown')}")
244
+
198
245
  except Exception as e:
199
- output.append(f"\nLocation lookup failed: {str(e)}")
246
+ error_msg = str(e)
247
+ if is_ci_environment(): error_msg = mask_sensitive_info(error_msg)
248
+ output.append(f"\nLocation lookup failed: {error_msg}")
200
249
 
201
250
  # DISPLAYS DNS SERVER INFORMATION
202
251
  def _display_dns_servers(output):
203
252
  output.append("\nDNS Servers:")
253
+ in_ci = is_ci_environment()
204
254
  try:
205
255
  with open('/etc/resolv.conf', 'r') as f:
206
256
  for line in f:
207
- if 'nameserver' in line: output.append(f"DNS: {line.split()[1]}")
257
+ if 'nameserver' in line:
258
+ dns_ip = line.split()[1]
259
+ display_dns = mask_ipv4(dns_ip) if in_ci else dns_ip
260
+ output.append(f"DNS: {display_dns}")
261
+
208
262
  except OSError:
209
263
  output.append("Could not read DNS configuration")
210
264
 
@@ -246,6 +300,7 @@ def _monitor_network_traffic(output, interval):
246
300
  # MAIN FUNCTION TO RUN NETWORK DIAGNOSTICS AND DISPLAY RESULTS
247
301
  def run(test=False, speed=False, monitor=False, interval=1, ports=False, dns=False, location=False, no_ip=False):
248
302
  output = []
303
+ in_ci = is_ci_environment()
249
304
 
250
305
  if not no_ip: _display_ip_addresses(output)
251
306
  if test: _display_connectivity_tests(output)
@@ -259,4 +314,6 @@ def run(test=False, speed=False, monitor=False, interval=1, ports=False, dns=Fal
259
314
  if run_speedtest(): output.append("Speed test completed successfully")
260
315
  else: output.append("Speed test failed")
261
316
 
262
- return "\n".join(output)
317
+ result = "\n".join(output)
318
+ if in_ci: result = mask_sensitive_info(result, mask_ips=True)
319
+ return result
@@ -3,10 +3,31 @@ from .core import autolower_transform
3
3
  from ..utils.loading import LoadingAnimation
4
4
  from ..utils.updates import check_for_updates
5
5
 
6
+ # TOOL CATEGORY (USED BY 'autotools smoke')
7
+ TOOL_CATEGORY = 'Text'
8
+
9
+ # SMOKE TEST CASES (USED BY 'autotools smoke')
10
+ SMOKE_TESTS = [
11
+ {'name': 'basic', 'args': ['TEST', 'WITH', 'MULTIPLE', 'WORDS']},
12
+ {'name': 'special', 'args': ['SPECIAL', 'CHARS:', '!@#$%^&*()']},
13
+ {'name': 'mixed', 'args': ['123', 'MIXED', 'with', 'UPPERCASE', '456']},
14
+ {'name': 'unicode', 'args': ['ÁCCÊNTS', 'ÀND', 'ÉMOJIS', '🚀', '⭐']},
15
+ ]
16
+
6
17
  # CLI COMMAND TO TRANSFORM TEXT TO LOWERCASE
7
18
  @click.command()
8
19
  @click.argument('text', nargs=-1)
9
20
  def autolower(text):
21
+ """
22
+ TRANSFORMS TEXT TO LOWERCASE.
23
+
24
+ \b
25
+ EXAMPLES:
26
+ autolower HELLO WORLD
27
+ autolower "THIS IS A TEST"
28
+ echo "TEXT" | autolower
29
+ """
30
+
10
31
  with LoadingAnimation(): result = autolower_transform(" ".join(text))
11
32
  click.echo(result)
12
33
  update_msg = check_for_updates()
File without changes
@@ -0,0 +1,70 @@
1
+ import click
2
+ from .core import autonote_add, autonote_list, DEFAULT_NOTES_FILE
3
+ from ..utils.loading import LoadingAnimation
4
+ from ..utils.updates import check_for_updates
5
+
6
+ # TOOL CATEGORY (USED BY 'autotools smoke')
7
+ TOOL_CATEGORY = 'Text'
8
+
9
+ # SMOKE TEST CASES (USED BY 'autotools smoke')
10
+ SMOKE_TESTS = [
11
+ {'name': 'add-note', 'args': ['--add', 'Test note']},
12
+ {'name': 'list-notes', 'args': ['--list']},
13
+ ]
14
+
15
+ # CLI COMMAND TO MANAGE NOTES
16
+ @click.command()
17
+ @click.option('--file', '-f', 'notes_path', default=DEFAULT_NOTES_FILE, help='PATH TO NOTES FILE (DEFAULT: NOTES.md)')
18
+ @click.option('--add', 'add_note', metavar='NOTE', help='ADD A NOTE')
19
+ @click.option('--no-timestamp', 'no_timestamp', is_flag=True, help='ADD NOTE WITHOUT TIMESTAMP')
20
+ @click.option('--list', 'list_notes', is_flag=True, help='LIST ALL NOTES')
21
+ @click.option('--limit', type=int, metavar='N', help='LIMIT NUMBER OF NOTES WHEN LISTING (SHOWS LAST N NOTES)')
22
+ def autonote(notes_path, add_note, no_timestamp, list_notes, limit):
23
+ """
24
+ TAKES QUICK NOTES AND SAVES THEM TO A MARKDOWN FILE.
25
+
26
+ \b
27
+ OPERATIONS:
28
+ - ADD NOTE: --add "your note here" [--no-timestamp]
29
+ - LIST NOTES: --list [--limit N]
30
+
31
+ \b
32
+ EXAMPLES:
33
+ autonote --add "Meeting with team at 3pm"
34
+ autonote --add "Remember to update docs" --no-timestamp
35
+ autonote --list
36
+ autonote --list --limit 5
37
+ """
38
+
39
+ operations = sum([bool(add_note), bool(list_notes)])
40
+
41
+ if operations == 0:
42
+ click.echo(click.style("ERROR: NO OPERATION SPECIFIED", fg='red'), err=True)
43
+ click.echo(click.get_current_context().get_help())
44
+ raise click.Abort()
45
+
46
+ if operations > 1:
47
+ click.echo(click.style("ERROR: ONLY ONE OPERATION CAN BE PERFORMED AT A TIME", fg='red'), err=True)
48
+ raise click.Abort()
49
+
50
+ try:
51
+ with LoadingAnimation():
52
+ if add_note:
53
+ result = autonote_add(notes_path, add_note, timestamp=not no_timestamp)
54
+ click.echo(click.style(f"SUCCESS: ADDED NOTE TO {result}", fg='green'))
55
+ elif list_notes: # pragma: no branch
56
+ notes = autonote_list(notes_path, limit, format_for_terminal=True)
57
+ if not notes:
58
+ click.echo(click.style("NO NOTES FOUND", fg='yellow'))
59
+ else:
60
+ click.echo(click.style(f"\nNOTES ({len(notes)}):", fg='blue', bold=True))
61
+ for note in notes:
62
+ click.echo(f" {note}")
63
+
64
+ update_msg = check_for_updates()
65
+ if update_msg:
66
+ click.echo(update_msg)
67
+
68
+ except Exception as e:
69
+ click.echo(click.style(f"UNEXPECTED ERROR: {str(e)}", fg='red'), err=True)
70
+ raise click.Abort()
@@ -0,0 +1,106 @@
1
+ from pathlib import Path
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ DEFAULT_NOTES_FILE = "NOTES.md"
6
+
7
+ NOTES_TEMPLATE = """# NOTES
8
+
9
+ """
10
+
11
+ # READS NOTES FILE CONTENT
12
+ def _read_notes_file(notes_path: Path) -> str:
13
+ if not notes_path.exists():
14
+ return NOTES_TEMPLATE
15
+ return notes_path.read_text(encoding='utf-8')
16
+
17
+ # WRITES NOTES FILE CONTENT
18
+ def _write_notes_file(notes_path: Path, content: str):
19
+ notes_path.parent.mkdir(parents=True, exist_ok=True)
20
+ notes_path.write_text(content, encoding='utf-8')
21
+
22
+ # ADDS A NOTE TO THE NOTES FILE
23
+ def autonote_add(notes_path: str, note: str, timestamp: Optional[bool] = True):
24
+ notes_file = Path(notes_path)
25
+ content = _read_notes_file(notes_file)
26
+
27
+ lines = content.split('\n')
28
+
29
+ # REMOVE TRAILING EMPTY LINES
30
+ while lines and lines[-1].strip() == '':
31
+ lines.pop()
32
+
33
+ # ENSURE FILE HAS HEADER IF EMPTY OR MISSING
34
+ if not lines or (len(lines) == 1 and lines[0].strip() == ''):
35
+ lines = ['# NOTES', '']
36
+ elif not any(line.strip().startswith('#') for line in lines):
37
+ lines.insert(0, '# NOTES')
38
+ lines.insert(1, '')
39
+
40
+ # ADD NEW NOTE WITH TIMESTAMP
41
+ if timestamp:
42
+ timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
43
+ new_note = f"- **[{timestamp_str}]** {note}"
44
+ else:
45
+ new_note = f"- {note}"
46
+
47
+ lines.append(new_note)
48
+ lines.append('') # ADD EMPTY LINE AFTER NOTE
49
+
50
+ content = '\n'.join(lines)
51
+ _write_notes_file(notes_file, content)
52
+ return str(notes_file)
53
+
54
+ # FORMATS NOTE FOR TERMINAL DISPLAY (REMOVES MARKDOWN)
55
+ def _format_note_for_terminal(note_line: str) -> str:
56
+ # REMOVE LEADING DASH AND SPACES
57
+ note = note_line.lstrip('-').strip()
58
+
59
+ # EXTRACT TIMESTAMP IF PRESENT (SUPPORTS MULTIPLE FORMATS FOR COMPATIBILITY)
60
+ import re
61
+ # NEW FORMAT: **[YYYY-MM-DD HH:MM:SS]** note
62
+ timestamp_match = re.match(r'\*\*\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\*\*\s*(.+)', note)
63
+ if timestamp_match:
64
+ timestamp = timestamp_match.group(1)
65
+ note_text = timestamp_match.group(2)
66
+ return f"[{timestamp}] {note_text}"
67
+
68
+ # FORMAT: [YYYY-MM-DD HH:MM:SS] note (WITHOUT BOLD)
69
+ timestamp_match = re.match(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s*(.+)', note)
70
+ if timestamp_match:
71
+ timestamp = timestamp_match.group(1)
72
+ note_text = timestamp_match.group(2)
73
+ return f"[{timestamp}] {note_text}"
74
+
75
+ # OLD FORMAT: **YYYY-MM-DD HH:MM:SS**: note (FOR BACKWARD COMPATIBILITY)
76
+ timestamp_match = re.match(r'\*\*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\*\*:\s*(.+)', note)
77
+ if timestamp_match:
78
+ timestamp = timestamp_match.group(1)
79
+ note_text = timestamp_match.group(2)
80
+ return f"[{timestamp}] {note_text}"
81
+
82
+ # NO TIMESTAMP, RETURN AS IS
83
+ return note
84
+
85
+ # LISTS NOTES FROM THE FILE
86
+ def autonote_list(notes_path: str, limit: Optional[int] = None, format_for_terminal: bool = False):
87
+ notes_file = Path(notes_path)
88
+ if not notes_file.exists():
89
+ return []
90
+
91
+ content = _read_notes_file(notes_file)
92
+ lines = content.split('\n')
93
+
94
+ notes = []
95
+ for line in lines:
96
+ stripped = line.strip()
97
+ if stripped and stripped.startswith('-'):
98
+ if format_for_terminal:
99
+ notes.append(_format_note_for_terminal(stripped))
100
+ else:
101
+ notes.append(stripped)
102
+
103
+ if limit and limit > 0:
104
+ notes = notes[-limit:] # GET LAST N NOTES
105
+
106
+ return notes
@@ -4,6 +4,22 @@ from ..utils.loading import LoadingAnimation
4
4
  from ..utils.updates import check_for_updates
5
5
  from .core import generate_password, generate_encryption_key, analyze_password_strength
6
6
 
7
+ # TOOL CATEGORY (USED BY 'autotools smoke')
8
+ TOOL_CATEGORY = 'Security'
9
+
10
+ # SMOKE TEST CASES (USED BY 'autotools smoke')
11
+ SMOKE_TESTS = [
12
+ {'name': 'basic', 'args': []},
13
+ {'name': 'length', 'args': ['--length', '32']},
14
+ {'name': 'no-special', 'args': ['--no-special']},
15
+ {'name': 'no-numbers', 'args': ['--no-numbers']},
16
+ {'name': 'no-uppercase', 'args': ['--no-uppercase']},
17
+ {'name': 'min-special', 'args': ['--min-special', '3']},
18
+ {'name': 'min-numbers', 'args': ['--min-numbers', '3']},
19
+ {'name': 'analysis', 'args': ['--analyze']},
20
+ {'name': 'encryption', 'args': ['--gen-key']},
21
+ ]
22
+
7
23
  # CLI COMMAND TO GENERATE PASSWORDS OR ENCRYPTION KEYS
8
24
  @click.command()
9
25
  @click.option('--length', '-l', default=12, help='Password length (default: 12)')
@@ -15,7 +31,29 @@ from .core import generate_password, generate_encryption_key, analyze_password_s
15
31
  @click.option('--analyze', '-a', is_flag=True, help='Analyze password strength')
16
32
  @click.option('--gen-key', '-g', is_flag=True, help='Generate encryption key')
17
33
  @click.option('--password-key', '-p', help='Generate key from password')
18
- def autopassword(length, no_uppercase, no_numbers, no_special, min_special, min_numbers, analyze, gen_key, password_key):
34
+ def autopassword(length, no_uppercase, no_numbers, no_special, min_special, min_numbers, analyze, gen_key, password_key):
35
+ """
36
+ GENERATES SECURE PASSWORDS OR ENCRYPTION KEYS.
37
+
38
+ \b
39
+ FEATURES:
40
+ - Generate random passwords with customizable length
41
+ - Control character sets (uppercase, numbers, special)
42
+ - Set minimum requirements for special characters and numbers
43
+ - Analyze password strength
44
+ - Generate encryption keys
45
+ - Derive keys from passwords
46
+
47
+ \b
48
+ EXAMPLES:
49
+ autopassword
50
+ autopassword --length 16
51
+ autopassword --length 20 --analyze
52
+ autopassword --no-special --min-numbers 3
53
+ autopassword --gen-key
54
+ autopassword --password-key "mypassword" --analyze
55
+ """
56
+
19
57
  # DISPLAYS PASSWORD STRENGTH ANALYSIS RESULTS
20
58
  def show_analysis(text, prefix=""):
21
59
  if not analyze: return
@@ -4,6 +4,16 @@ import sys
4
4
  import os
5
5
  import re
6
6
  from ..utils.updates import check_for_updates
7
+ from ..utils.text import safe_text
8
+
9
+ # TOOL CATEGORY (USED BY 'autotools smoke')
10
+ TOOL_CATEGORY = 'Testing'
11
+
12
+ # SMOKE TEST CASES (USED BY 'autotools smoke')
13
+ # NOTE: THIS TOOL IS SKIPPED BY DEFAULT IN SMOKE RUNS (SEE docs/docker.md)
14
+ SMOKE_TESTS = [
15
+ {'name': 'help', 'args': ['--help']},
16
+ ]
7
17
 
8
18
  # CLI COMMAND TO RUN TEST SUITE WITH PYTEST
9
19
  @click.command()
@@ -13,6 +23,29 @@ from ..utils.updates import check_for_updates
13
23
  @click.option('--html', is_flag=True, help='Generate HTML coverage report')
14
24
  @click.option('--module', '-m', help='Test specific module (e.g., autocaps, autolower)')
15
25
  def autotest(unit, integration, no_cov, html, module):
26
+ """
27
+ RUNS THE TEST SUITE WITH PYTEST.
28
+
29
+ \b
30
+ FEATURES:
31
+ - Run unit tests only
32
+ - Run integration tests only
33
+ - Generate coverage reports
34
+ - Generate HTML coverage reports
35
+ - Test specific modules
36
+ - Auto-install test dependencies
37
+
38
+ \b
39
+ EXAMPLES:
40
+ autotest
41
+ autotest --unit
42
+ autotest --integration
43
+ autotest --module autocaps
44
+ autotest --html
45
+ autotest --no-cov
46
+ autotest --unit --module autopassword
47
+ """
48
+
16
49
  _install_test_dependencies()
17
50
 
18
51
  cmd = _build_test_command(unit, integration, no_cov, html, module)
@@ -32,12 +65,12 @@ def _install_test_dependencies():
32
65
  import pytest
33
66
  import pytest_cov
34
67
  except ImportError:
35
- click.echo(click.style("\n pytest and/or pytest-cov not found. Installing...", fg='yellow', bold=True))
68
+ click.echo(safe_text(click.style("\n[X] pytest and/or pytest-cov not found. Installing...", fg='yellow', bold=True)))
36
69
  try:
37
- subprocess.run(['pip', 'install', 'pytest', 'pytest-cov'], check=True)
38
- click.echo(click.style(" Successfully installed pytest and pytest-cov", fg='green', bold=True))
70
+ subprocess.run([sys.executable, '-m', 'pip', 'install', 'pytest', 'pytest-cov'], check=True)
71
+ click.echo(safe_text(click.style("[OK] Successfully installed pytest and pytest-cov", fg='green', bold=True)))
39
72
  except subprocess.CalledProcessError as e:
40
- click.echo(click.style(f"\n Failed to install dependencies: {str(e)}", fg='red', bold=True))
73
+ click.echo(safe_text(click.style(f"\n[X] Failed to install dependencies: {str(e)}", fg='red', bold=True)))
41
74
  sys.exit(1)
42
75
 
43
76
  # BUILDS THE TEST COMMAND ARGUMENTS BY ADDING THE CORRECT TEST PATH AND OPTIONS
@@ -149,10 +182,10 @@ def _run_test_process(cmd):
149
182
  coverage_data = _process_test_output(process)
150
183
  _handle_test_result(process, coverage_data)
151
184
  except subprocess.CalledProcessError as e:
152
- click.echo(click.style(f"\n TESTS FAILED WITH RETURN CODE {e.returncode}", fg='red', bold=True))
185
+ click.echo(safe_text(click.style(f"\n[X] TESTS FAILED WITH RETURN CODE {e.returncode}", fg='red', bold=True)))
153
186
  sys.exit(1)
154
187
  except Exception as e:
155
- click.echo(click.style(f"\n ERROR RUNNING TESTS: {str(e)}", fg='red', bold=True))
188
+ click.echo(safe_text(click.style(f"\n[X] ERROR RUNNING TESTS: {str(e)}", fg='red', bold=True)))
156
189
  sys.exit(1)
157
190
 
158
191
  # PREPARES ENVIRONMENT FOR TEST PROCESS
@@ -197,9 +230,7 @@ def _process_test_output(process):
197
230
 
198
231
  # HANDLES TEST RESULT AND DISPLAYS COVERAGE
199
232
  def _handle_test_result(process, coverage_data):
200
- if process.returncode == 0:
201
- click.echo(click.style("\n ALL TESTS PASSED !", fg='green', bold=True))
202
- _display_coverage_metrics(coverage_data)
203
- else:
204
- click.echo(click.style("\n❌ SOME TESTS FAILED!", fg='red', bold=True))
205
- sys.exit(1)
233
+ if process.returncode == 0: click.echo(safe_text(click.style("\n[OK] ALL TESTS PASSED !", fg='green', bold=True)))
234
+ else: click.echo(safe_text(click.style("\n[X] SOME TESTS FAILED!", fg='red', bold=True)))
235
+ _display_coverage_metrics(coverage_data)
236
+ if process.returncode != 0: sys.exit(1)
@@ -0,0 +1,87 @@
1
+ from autotools.autotodo.core import (
2
+ DEFAULT_TODO_FILE,
3
+ PRIORITY_BADGES,
4
+ TODO_TEMPLATE,
5
+ _read_todo_file,
6
+ _write_todo_file,
7
+ _clean_empty_lines_after_insert,
8
+ _extract_task_prefix_and_text,
9
+ _prefix_to_ing,
10
+ _extract_text_from_tasks_line,
11
+ _extract_text_from_in_progress_line,
12
+ _extract_task_text_from_line,
13
+ _insert_task_into_section,
14
+ _find_section_boundaries,
15
+ _calculate_insert_idx,
16
+ _ensure_tasks_section,
17
+ _move_in_progress_section,
18
+ _remove_empty_placeholders,
19
+ _find_in_progress_position,
20
+ _handle_in_progress_section,
21
+ _check_done_sections,
22
+ _remove_empty_simple_done,
23
+ _handle_done_section,
24
+ _ensure_sections,
25
+ _find_done_section,
26
+ _find_section_start,
27
+ _find_section,
28
+ _create_task_line,
29
+ _find_last_task_in_section,
30
+ _insert_task_into_empty_section,
31
+ _add_task_to_section,
32
+ _get_task_line_by_index,
33
+ _move_to_in_progress,
34
+ _move_to_done,
35
+ _find_task_lines_in_section,
36
+ _remove_task,
37
+ _extract_task_lines_from_section,
38
+ autotodo_add_task,
39
+ autotodo_start,
40
+ autotodo_done,
41
+ autotodo_remove,
42
+ autotodo_list
43
+ )
44
+
45
+ __all__ = [
46
+ 'DEFAULT_TODO_FILE',
47
+ 'PRIORITY_BADGES',
48
+ 'TODO_TEMPLATE',
49
+ '_read_todo_file',
50
+ '_write_todo_file',
51
+ '_clean_empty_lines_after_insert',
52
+ '_extract_task_prefix_and_text',
53
+ '_prefix_to_ing',
54
+ '_extract_text_from_tasks_line',
55
+ '_extract_text_from_in_progress_line',
56
+ '_extract_task_text_from_line',
57
+ '_insert_task_into_section',
58
+ '_find_section_boundaries',
59
+ '_calculate_insert_idx',
60
+ '_ensure_tasks_section',
61
+ '_move_in_progress_section',
62
+ '_remove_empty_placeholders',
63
+ '_find_in_progress_position',
64
+ '_handle_in_progress_section',
65
+ '_check_done_sections',
66
+ '_remove_empty_simple_done',
67
+ '_handle_done_section',
68
+ '_ensure_sections',
69
+ '_find_done_section',
70
+ '_find_section_start',
71
+ '_find_section',
72
+ '_create_task_line',
73
+ '_find_last_task_in_section',
74
+ '_insert_task_into_empty_section',
75
+ '_add_task_to_section',
76
+ '_get_task_line_by_index',
77
+ '_move_to_in_progress',
78
+ '_move_to_done',
79
+ '_find_task_lines_in_section',
80
+ '_remove_task',
81
+ '_extract_task_lines_from_section',
82
+ 'autotodo_add_task',
83
+ 'autotodo_start',
84
+ 'autotodo_done',
85
+ 'autotodo_remove',
86
+ 'autotodo_list'
87
+ ]