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/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
|
|
14
|
-
for addr in addrs[
|
|
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
|
|
23
|
-
for addr in addrs[
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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:
|
|
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
|
-
#
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
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']:
|
|
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']:
|
|
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
|
-
|
|
179
|
-
|
|
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"
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
317
|
+
result = "\n".join(output)
|
|
318
|
+
if in_ci: result = mask_sensitive_info(result, mask_ips=True)
|
|
319
|
+
return result
|
autotools/autolower/commands.py
CHANGED
|
@@ -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
|
autotools/autotest/commands.py
CHANGED
|
@@ -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
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
]
|