devdash-mac 0.1.0__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.
- devdash/__init__.py +4 -0
- devdash/__main__.py +6 -0
- devdash/app.py +145 -0
- devdash/clipboard.py +98 -0
- devdash/config.py +51 -0
- devdash/plugin_loader.py +43 -0
- devdash/storage.py +244 -0
- devdash/tools/__init__.py +1 -0
- devdash/tools/base.py +38 -0
- devdash/tools/base64_tool.py +79 -0
- devdash/tools/color_tool.py +108 -0
- devdash/tools/cron_tool.py +137 -0
- devdash/tools/hash_tool.py +65 -0
- devdash/tools/json_tool.py +61 -0
- devdash/tools/jwt_tool.py +97 -0
- devdash/tools/lorem_tool.py +102 -0
- devdash/tools/password_tool.py +366 -0
- devdash/tools/regex_tool.py +88 -0
- devdash/tools/timestamp_tool.py +142 -0
- devdash/tools/url_tool.py +75 -0
- devdash/tools/uuid_tool.py +119 -0
- devdash/ui/__init__.py +1 -0
- devdash/ui/notifications.py +22 -0
- devdash/ui/windows.py +84 -0
- devdash_mac-0.1.0.dist-info/METADATA +194 -0
- devdash_mac-0.1.0.dist-info/RECORD +30 -0
- devdash_mac-0.1.0.dist-info/WHEEL +5 -0
- devdash_mac-0.1.0.dist-info/entry_points.txt +2 -0
- devdash_mac-0.1.0.dist-info/licenses/LICENSE +21 -0
- devdash_mac-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Base64 encode/decode tool."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from devdash.tools.base import DevTool
|
|
7
|
+
|
|
8
|
+
_BASE64_RE = re.compile(r"^[A-Za-z0-9+/\n\r]+=*$")
|
|
9
|
+
_BASE64_URLSAFE_RE = re.compile(r"^[A-Za-z0-9_\-\n\r]+=*$")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_base64(text: str) -> bool:
|
|
13
|
+
"""Check if text looks like valid Base64."""
|
|
14
|
+
cleaned = text.strip().replace("\n", "").replace("\r", "")
|
|
15
|
+
if len(cleaned) < 4 or len(cleaned) % 4 != 0:
|
|
16
|
+
return False
|
|
17
|
+
return bool(_BASE64_RE.match(cleaned) or _BASE64_URLSAFE_RE.match(cleaned))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Base64Tool(DevTool):
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "Base64 Encode / Decode"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def keyword(self) -> str:
|
|
27
|
+
return "base64"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def category(self) -> str:
|
|
31
|
+
return "Encoders / Decoders"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def description(self) -> str:
|
|
35
|
+
return "Encode or decode Base64 (standard and URL-safe)"
|
|
36
|
+
|
|
37
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
38
|
+
if not input_text.strip():
|
|
39
|
+
return "Error: Empty input."
|
|
40
|
+
|
|
41
|
+
mode = str(kwargs.get("mode", "auto"))
|
|
42
|
+
url_safe = bool(kwargs.get("url_safe", False))
|
|
43
|
+
|
|
44
|
+
if mode == "encode":
|
|
45
|
+
return self._encode(input_text, url_safe)
|
|
46
|
+
elif mode == "decode":
|
|
47
|
+
return self._decode(input_text, url_safe)
|
|
48
|
+
else:
|
|
49
|
+
# Auto-detect: try decode if it looks like Base64
|
|
50
|
+
if _is_base64(input_text.strip()):
|
|
51
|
+
decoded = self._decode(input_text, url_safe)
|
|
52
|
+
if not decoded.startswith("Error:"):
|
|
53
|
+
byte_len = len(input_text.strip().encode())
|
|
54
|
+
return f"[Auto-detected Base64 → Decoded]\n{decoded}\n\nBytes: {byte_len}"
|
|
55
|
+
return self._encode(input_text, url_safe)
|
|
56
|
+
|
|
57
|
+
def _encode(self, text: str, url_safe: bool) -> str:
|
|
58
|
+
data = text.encode("utf-8")
|
|
59
|
+
if url_safe:
|
|
60
|
+
encoded = base64.urlsafe_b64encode(data).decode("ascii")
|
|
61
|
+
else:
|
|
62
|
+
encoded = base64.b64encode(data).decode("ascii")
|
|
63
|
+
return f"{encoded}\n\nOriginal bytes: {len(data)}, Encoded bytes: {len(encoded)}"
|
|
64
|
+
|
|
65
|
+
def _decode(self, text: str, url_safe: bool) -> str:
|
|
66
|
+
cleaned = text.strip()
|
|
67
|
+
try:
|
|
68
|
+
if url_safe:
|
|
69
|
+
decoded = base64.urlsafe_b64decode(cleaned)
|
|
70
|
+
else:
|
|
71
|
+
decoded = base64.b64decode(cleaned)
|
|
72
|
+
result = decoded.decode("utf-8")
|
|
73
|
+
return f"{result}\n\nEncoded bytes: {len(cleaned)}, Decoded bytes: {len(decoded)}"
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return f"Error: Could not decode Base64: {e}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def register() -> DevTool:
|
|
79
|
+
return Base64Tool()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Color converter - HEX, RGB, HSL, HSV."""
|
|
2
|
+
|
|
3
|
+
import colorsys
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from devdash.tools.base import DevTool
|
|
7
|
+
|
|
8
|
+
_HEX_RE = re.compile(r"^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")
|
|
9
|
+
_RGB_RE = re.compile(r"^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$", re.IGNORECASE)
|
|
10
|
+
_HSL_RE = re.compile(r"^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%?\s*,\s*(\d{1,3})%?\s*\)$", re.IGNORECASE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ColorTool(DevTool):
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return "Color Converter"
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def keyword(self) -> str:
|
|
20
|
+
return "color"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def category(self) -> str:
|
|
24
|
+
return "Converters"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return "Convert between HEX, RGB, HSL, and HSV color formats"
|
|
29
|
+
|
|
30
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
31
|
+
if not input_text.strip():
|
|
32
|
+
return (
|
|
33
|
+
"Error: Empty input. Provide a color (e.g., #FF0000, rgb(255,0,0), hsl(0,100,50))"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
text = input_text.strip()
|
|
37
|
+
|
|
38
|
+
# Try HEX
|
|
39
|
+
hex_match = _HEX_RE.match(text)
|
|
40
|
+
if hex_match:
|
|
41
|
+
hex_val = hex_match.group(1)
|
|
42
|
+
if len(hex_val) == 3:
|
|
43
|
+
hex_val = "".join(c * 2 for c in hex_val)
|
|
44
|
+
r, g, b = int(hex_val[0:2], 16), int(hex_val[2:4], 16), int(hex_val[4:6], 16)
|
|
45
|
+
return self._format_all(r, g, b)
|
|
46
|
+
|
|
47
|
+
# Try RGB
|
|
48
|
+
rgb_match = _RGB_RE.match(text)
|
|
49
|
+
if rgb_match:
|
|
50
|
+
r, g, b = int(rgb_match.group(1)), int(rgb_match.group(2)), int(rgb_match.group(3))
|
|
51
|
+
if not all(0 <= v <= 255 for v in (r, g, b)):
|
|
52
|
+
return "Error: RGB values must be between 0 and 255."
|
|
53
|
+
return self._format_all(r, g, b)
|
|
54
|
+
|
|
55
|
+
# Try HSL
|
|
56
|
+
hsl_match = _HSL_RE.match(text)
|
|
57
|
+
if hsl_match:
|
|
58
|
+
h = int(hsl_match.group(1))
|
|
59
|
+
s = int(hsl_match.group(2))
|
|
60
|
+
l_val = int(hsl_match.group(3))
|
|
61
|
+
if not (0 <= h <= 360 and 0 <= s <= 100 and 0 <= l_val <= 100):
|
|
62
|
+
return "Error: HSL values out of range (H: 0-360, S: 0-100, L: 0-100)."
|
|
63
|
+
r, g, b = self._hsl_to_rgb(h, s, l_val)
|
|
64
|
+
return self._format_all(r, g, b)
|
|
65
|
+
|
|
66
|
+
# Try plain RGB numbers: "255, 0, 0" or "255 0 0"
|
|
67
|
+
parts = re.split(r"[,\s]+", text)
|
|
68
|
+
if len(parts) == 3 and all(p.isdigit() for p in parts):
|
|
69
|
+
r, g, b = int(parts[0]), int(parts[1]), int(parts[2])
|
|
70
|
+
if all(0 <= v <= 255 for v in (r, g, b)):
|
|
71
|
+
return self._format_all(r, g, b)
|
|
72
|
+
|
|
73
|
+
return "Error: Unrecognized color format. Use #HEX, rgb(r,g,b), hsl(h,s,l), or r,g,b"
|
|
74
|
+
|
|
75
|
+
def _format_all(self, r: int, g: int, b: int) -> str:
|
|
76
|
+
hex_val = f"#{r:02X}{g:02X}{b:02X}"
|
|
77
|
+
|
|
78
|
+
# HSL
|
|
79
|
+
h_norm, l_norm, s_norm = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
|
80
|
+
h_deg = round(h_norm * 360)
|
|
81
|
+
s_pct = round(s_norm * 100)
|
|
82
|
+
l_pct = round(l_norm * 100)
|
|
83
|
+
|
|
84
|
+
# HSV
|
|
85
|
+
h_norm2, s_norm2, v_norm2 = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
|
|
86
|
+
h_deg2 = round(h_norm2 * 360)
|
|
87
|
+
s_pct2 = round(s_norm2 * 100)
|
|
88
|
+
v_pct2 = round(v_norm2 * 100)
|
|
89
|
+
|
|
90
|
+
# Complementary color
|
|
91
|
+
comp_r, comp_g, comp_b = 255 - r, 255 - g, 255 - b
|
|
92
|
+
comp_hex = f"#{comp_r:02X}{comp_g:02X}{comp_b:02X}"
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
f"HEX: {hex_val}\n"
|
|
96
|
+
f"RGB: rgb({r}, {g}, {b})\n"
|
|
97
|
+
f"HSL: hsl({h_deg}, {s_pct}%, {l_pct}%)\n"
|
|
98
|
+
f"HSV: hsv({h_deg2}, {s_pct2}%, {v_pct2}%)\n"
|
|
99
|
+
f"Complementary: {comp_hex}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _hsl_to_rgb(self, h: int, s: int, lightness: int) -> tuple[int, int, int]:
|
|
103
|
+
r, g, b = colorsys.hls_to_rgb(h / 360, lightness / 100, s / 100)
|
|
104
|
+
return round(r * 255), round(g * 255), round(b * 255)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def register() -> DevTool:
|
|
108
|
+
return ColorTool()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Cron expression parser and scheduler."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
from croniter import croniter
|
|
6
|
+
|
|
7
|
+
from devdash.tools.base import DevTool
|
|
8
|
+
|
|
9
|
+
PRESETS: dict[str, str] = {
|
|
10
|
+
"every minute": "* * * * *",
|
|
11
|
+
"hourly": "0 * * * *",
|
|
12
|
+
"daily": "0 0 * * *",
|
|
13
|
+
"weekly": "0 0 * * 1",
|
|
14
|
+
"monthly": "0 0 1 * *",
|
|
15
|
+
"yearly": "0 0 1 1 *",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_FIELD_NAMES = ["minute", "hour", "day of month", "month", "day of week"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CronTool(DevTool):
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return "Cron Expression Parser"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def keyword(self) -> str:
|
|
28
|
+
return "cron"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def category(self) -> str:
|
|
32
|
+
return "Converters"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def description(self) -> str:
|
|
36
|
+
return "Parse cron expressions to human-readable descriptions"
|
|
37
|
+
|
|
38
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
39
|
+
if not input_text.strip():
|
|
40
|
+
lines = ["Common cron presets:"]
|
|
41
|
+
for name, expr in PRESETS.items():
|
|
42
|
+
lines.append(f" {expr:15s} {name}")
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
text = input_text.strip()
|
|
46
|
+
|
|
47
|
+
# Check if it's a preset name
|
|
48
|
+
if text.lower() in PRESETS:
|
|
49
|
+
text = PRESETS[text.lower()]
|
|
50
|
+
|
|
51
|
+
# Validate cron expression
|
|
52
|
+
if not croniter.is_valid(text):
|
|
53
|
+
return (
|
|
54
|
+
f"Error: Invalid cron expression '{text}'.\n\n"
|
|
55
|
+
"Format: minute hour day-of-month month day-of-week\n"
|
|
56
|
+
"Example: 0 9 * * 1-5 (9 AM on weekdays)"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Human-readable description
|
|
60
|
+
desc = self._describe(text)
|
|
61
|
+
|
|
62
|
+
# Next 5 run times
|
|
63
|
+
now = datetime.now(timezone.utc)
|
|
64
|
+
cron = croniter(text, now)
|
|
65
|
+
next_runs: list[str] = []
|
|
66
|
+
for _ in range(5):
|
|
67
|
+
next_dt = cron.get_next(datetime)
|
|
68
|
+
next_runs.append(f" {next_dt.strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
69
|
+
|
|
70
|
+
lines = [
|
|
71
|
+
f"Expression: {text}",
|
|
72
|
+
f"Description: {desc}",
|
|
73
|
+
"",
|
|
74
|
+
"Next 5 runs:",
|
|
75
|
+
*next_runs,
|
|
76
|
+
]
|
|
77
|
+
return "\n".join(lines)
|
|
78
|
+
|
|
79
|
+
def _describe(self, expr: str) -> str:
|
|
80
|
+
"""Generate a human-readable description of a cron expression."""
|
|
81
|
+
parts = expr.split()
|
|
82
|
+
if len(parts) < 5:
|
|
83
|
+
return expr
|
|
84
|
+
|
|
85
|
+
minute, hour, dom, month, dow = parts[0], parts[1], parts[2], parts[3], parts[4]
|
|
86
|
+
|
|
87
|
+
# Common patterns
|
|
88
|
+
if expr == "* * * * *":
|
|
89
|
+
return "Every minute"
|
|
90
|
+
if minute != "*" and hour == "*" and dom == "*" and month == "*" and dow == "*":
|
|
91
|
+
return f"Every hour at minute {minute}"
|
|
92
|
+
if minute != "*" and hour != "*" and dom == "*" and month == "*" and dow == "*":
|
|
93
|
+
return f"At {hour.zfill(2)}:{minute.zfill(2)} every day"
|
|
94
|
+
if minute != "*" and hour != "*" and dom == "*" and month == "*" and dow != "*":
|
|
95
|
+
dow_name = self._dow_name(dow)
|
|
96
|
+
return f"At {hour.zfill(2)}:{minute.zfill(2)} on {dow_name}"
|
|
97
|
+
if minute != "*" and hour != "*" and dom != "*" and month == "*" and dow == "*":
|
|
98
|
+
return f"At {hour.zfill(2)}:{minute.zfill(2)} on day {dom} of every month"
|
|
99
|
+
|
|
100
|
+
# Generic fallback
|
|
101
|
+
desc_parts: list[str] = []
|
|
102
|
+
if minute == "*":
|
|
103
|
+
desc_parts.append("every minute")
|
|
104
|
+
else:
|
|
105
|
+
desc_parts.append(f"at minute {minute}")
|
|
106
|
+
if hour != "*":
|
|
107
|
+
desc_parts.append(f"past hour {hour}")
|
|
108
|
+
if dom != "*":
|
|
109
|
+
desc_parts.append(f"on day {dom}")
|
|
110
|
+
if month != "*":
|
|
111
|
+
desc_parts.append(f"in month {month}")
|
|
112
|
+
if dow != "*":
|
|
113
|
+
desc_parts.append(f"on {self._dow_name(dow)}")
|
|
114
|
+
|
|
115
|
+
return ", ".join(desc_parts).capitalize()
|
|
116
|
+
|
|
117
|
+
def _dow_name(self, dow: str) -> str:
|
|
118
|
+
names = {
|
|
119
|
+
"0": "Sunday",
|
|
120
|
+
"1": "Monday",
|
|
121
|
+
"2": "Tuesday",
|
|
122
|
+
"3": "Wednesday",
|
|
123
|
+
"4": "Thursday",
|
|
124
|
+
"5": "Friday",
|
|
125
|
+
"6": "Saturday",
|
|
126
|
+
"7": "Sunday",
|
|
127
|
+
}
|
|
128
|
+
if dow in names:
|
|
129
|
+
return names[dow]
|
|
130
|
+
if "-" in dow:
|
|
131
|
+
start, end = dow.split("-", 1)
|
|
132
|
+
return f"{names.get(start, start)} to {names.get(end, end)}"
|
|
133
|
+
return dow
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def register() -> DevTool:
|
|
137
|
+
return CronTool()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Hash generator - MD5, SHA-1, SHA-256, SHA-512, BLAKE2b, HMAC."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
|
|
6
|
+
from devdash.tools.base import DevTool
|
|
7
|
+
|
|
8
|
+
ALGORITHMS = ["md5", "sha1", "sha256", "sha512", "blake2b"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HashTool(DevTool):
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "Hash Generator"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def keyword(self) -> str:
|
|
18
|
+
return "hash"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def category(self) -> str:
|
|
22
|
+
return "Encoders / Decoders"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
return "Generate MD5, SHA-1, SHA-256, SHA-512, BLAKE2b hashes"
|
|
27
|
+
|
|
28
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
29
|
+
if not input_text.strip():
|
|
30
|
+
return "Error: Empty input. Please provide text to hash."
|
|
31
|
+
|
|
32
|
+
algorithm = str(kwargs.get("algorithm", "all"))
|
|
33
|
+
hmac_key = kwargs.get("key")
|
|
34
|
+
|
|
35
|
+
data = input_text.encode("utf-8")
|
|
36
|
+
|
|
37
|
+
if hmac_key:
|
|
38
|
+
return self._hmac_hash(data, str(hmac_key), algorithm)
|
|
39
|
+
|
|
40
|
+
if algorithm != "all" and algorithm in ALGORITHMS:
|
|
41
|
+
h = hashlib.new(algorithm, data)
|
|
42
|
+
return f"{algorithm.upper()}: {h.hexdigest()}"
|
|
43
|
+
|
|
44
|
+
lines: list[str] = []
|
|
45
|
+
for algo in ALGORITHMS:
|
|
46
|
+
h = hashlib.new(algo, data)
|
|
47
|
+
lines.append(f"{algo.upper():>8}: {h.hexdigest()}")
|
|
48
|
+
return "\n".join(lines)
|
|
49
|
+
|
|
50
|
+
def _hmac_hash(self, data: bytes, key: str, algorithm: str) -> str:
|
|
51
|
+
key_bytes = key.encode("utf-8")
|
|
52
|
+
if algorithm == "all":
|
|
53
|
+
algorithm = "sha256"
|
|
54
|
+
if algorithm not in ALGORITHMS:
|
|
55
|
+
return f"Error: Unknown algorithm '{algorithm}'. Use: {', '.join(ALGORITHMS)}"
|
|
56
|
+
if algorithm == "blake2b":
|
|
57
|
+
# HMAC doesn't support blake2b directly, use hashlib
|
|
58
|
+
h = hmac.new(key_bytes, data, hashlib.sha256)
|
|
59
|
+
return f"HMAC-SHA256 (blake2b not supported for HMAC): {h.hexdigest()}"
|
|
60
|
+
h = hmac.new(key_bytes, data, algorithm)
|
|
61
|
+
return f"HMAC-{algorithm.upper()}: {h.hexdigest()}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register() -> DevTool:
|
|
65
|
+
return HashTool()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""JSON formatter, validator, and minifier."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from devdash.tools.base import DevTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JsonTool(DevTool):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "JSON Formatter"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def keyword(self) -> str:
|
|
15
|
+
return "json"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def category(self) -> str:
|
|
19
|
+
return "Formatters"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Format, validate, or minify JSON"
|
|
24
|
+
|
|
25
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
26
|
+
if not input_text.strip():
|
|
27
|
+
return "Error: Empty input. Please provide a JSON string."
|
|
28
|
+
|
|
29
|
+
mode = str(kwargs.get("mode", "format"))
|
|
30
|
+
|
|
31
|
+
if mode == "validate":
|
|
32
|
+
return self._validate_json(input_text)
|
|
33
|
+
elif mode == "minify":
|
|
34
|
+
return self._minify(input_text)
|
|
35
|
+
else:
|
|
36
|
+
return self._format(input_text)
|
|
37
|
+
|
|
38
|
+
def _format(self, text: str) -> str:
|
|
39
|
+
try:
|
|
40
|
+
parsed = json.loads(text)
|
|
41
|
+
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
42
|
+
except json.JSONDecodeError as e:
|
|
43
|
+
return f"Invalid JSON: {e.msg} (line {e.lineno}, column {e.colno})"
|
|
44
|
+
|
|
45
|
+
def _minify(self, text: str) -> str:
|
|
46
|
+
try:
|
|
47
|
+
parsed = json.loads(text)
|
|
48
|
+
return json.dumps(parsed, separators=(",", ":"), ensure_ascii=False)
|
|
49
|
+
except json.JSONDecodeError as e:
|
|
50
|
+
return f"Invalid JSON: {e.msg} (line {e.lineno}, column {e.colno})"
|
|
51
|
+
|
|
52
|
+
def _validate_json(self, text: str) -> str:
|
|
53
|
+
try:
|
|
54
|
+
json.loads(text)
|
|
55
|
+
return "Valid JSON"
|
|
56
|
+
except json.JSONDecodeError as e:
|
|
57
|
+
return f"Invalid JSON: {e.msg} (line {e.lineno}, column {e.colno})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def register() -> DevTool:
|
|
61
|
+
return JsonTool()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""JWT decoder - decode header and payload without verification."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
import jwt
|
|
7
|
+
|
|
8
|
+
from devdash.tools.base import DevTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JwtTool(DevTool):
|
|
12
|
+
@property
|
|
13
|
+
def name(self) -> str:
|
|
14
|
+
return "JWT Decoder"
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def keyword(self) -> str:
|
|
18
|
+
return "jwt"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def category(self) -> str:
|
|
22
|
+
return "Encoders / Decoders"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
return "Decode JWT tokens (header + payload + expiry check)"
|
|
27
|
+
|
|
28
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
29
|
+
if not input_text.strip():
|
|
30
|
+
return "Error: Empty input. Please provide a JWT token."
|
|
31
|
+
|
|
32
|
+
token = input_text.strip()
|
|
33
|
+
|
|
34
|
+
parts = token.split(".")
|
|
35
|
+
if len(parts) != 3:
|
|
36
|
+
return (
|
|
37
|
+
"Error: Invalid JWT format. "
|
|
38
|
+
f"Expected 3 parts (header.payload.signature), got {len(parts)}."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
header = jwt.get_unverified_header(token)
|
|
43
|
+
except jwt.exceptions.DecodeError as e:
|
|
44
|
+
return f"Error: Could not decode JWT header: {e}"
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
algorithms = [
|
|
48
|
+
"HS256",
|
|
49
|
+
"HS384",
|
|
50
|
+
"HS512",
|
|
51
|
+
"RS256",
|
|
52
|
+
"RS384",
|
|
53
|
+
"RS512",
|
|
54
|
+
"ES256",
|
|
55
|
+
"ES384",
|
|
56
|
+
"ES512",
|
|
57
|
+
]
|
|
58
|
+
payload = jwt.decode(token, options={"verify_signature": False}, algorithms=algorithms)
|
|
59
|
+
except jwt.exceptions.DecodeError as e:
|
|
60
|
+
return f"Error: Could not decode JWT payload: {e}"
|
|
61
|
+
|
|
62
|
+
lines: list[str] = []
|
|
63
|
+
lines.append("=== HEADER ===")
|
|
64
|
+
lines.append(json.dumps(header, indent=2))
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append("=== PAYLOAD ===")
|
|
67
|
+
lines.append(json.dumps(payload, indent=2, default=str))
|
|
68
|
+
|
|
69
|
+
# Expiration check
|
|
70
|
+
exp = payload.get("exp")
|
|
71
|
+
iat = payload.get("iat")
|
|
72
|
+
now = datetime.now(timezone.utc)
|
|
73
|
+
|
|
74
|
+
lines.append("")
|
|
75
|
+
lines.append("=== TOKEN INFO ===")
|
|
76
|
+
|
|
77
|
+
if iat:
|
|
78
|
+
issued = datetime.fromtimestamp(iat, tz=timezone.utc)
|
|
79
|
+
lines.append(f"Issued At: {issued.isoformat()}")
|
|
80
|
+
|
|
81
|
+
if exp:
|
|
82
|
+
expiry = datetime.fromtimestamp(exp, tz=timezone.utc)
|
|
83
|
+
is_expired = now > expiry
|
|
84
|
+
lines.append(f"Expires At: {expiry.isoformat()}")
|
|
85
|
+
status = "*** EXPIRED ***" if is_expired else "Valid (not expired)"
|
|
86
|
+
lines.append(f"Status: {status}")
|
|
87
|
+
else:
|
|
88
|
+
lines.append("Expires At: No expiration set")
|
|
89
|
+
|
|
90
|
+
lines.append("")
|
|
91
|
+
lines.append("WARNING: This tool only DECODES the token. It does NOT verify the signature.")
|
|
92
|
+
|
|
93
|
+
return "\n".join(lines)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def register() -> DevTool:
|
|
97
|
+
return JwtTool()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Lorem ipsum generator."""
|
|
2
|
+
|
|
3
|
+
from devdash.tools.base import DevTool
|
|
4
|
+
|
|
5
|
+
_LOREM_TEXT = (
|
|
6
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "
|
|
7
|
+
"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "
|
|
8
|
+
"exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure "
|
|
9
|
+
"dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. "
|
|
10
|
+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
|
|
11
|
+
"mollit anim id est laborum."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_SENTENCES = [
|
|
15
|
+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
|
16
|
+
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
17
|
+
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
|
|
18
|
+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.",
|
|
19
|
+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia.",
|
|
20
|
+
"Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit.",
|
|
21
|
+
"Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.",
|
|
22
|
+
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis.",
|
|
23
|
+
"Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse.",
|
|
24
|
+
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis.",
|
|
25
|
+
"Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit.",
|
|
26
|
+
"Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus.",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
_WORDS = _LOREM_TEXT.replace(",", "").replace(".", "").lower().split()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class LoremTool(DevTool):
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return "Lorem Ipsum Generator"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def keyword(self) -> str:
|
|
39
|
+
return "lorem"
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def category(self) -> str:
|
|
43
|
+
return "Generators"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def description(self) -> str:
|
|
47
|
+
return "Generate lorem ipsum text by words, sentences, or paragraphs"
|
|
48
|
+
|
|
49
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
50
|
+
text = input_text.strip().lower()
|
|
51
|
+
mode = str(kwargs.get("mode", ""))
|
|
52
|
+
|
|
53
|
+
if not text and not mode:
|
|
54
|
+
return self._paragraphs(3)
|
|
55
|
+
|
|
56
|
+
# Parse "5 words", "3 sentences", "2 paragraphs" or just a number
|
|
57
|
+
parts = text.split()
|
|
58
|
+
count = 1
|
|
59
|
+
unit = mode or "paragraphs"
|
|
60
|
+
|
|
61
|
+
if parts:
|
|
62
|
+
try:
|
|
63
|
+
count = int(parts[0])
|
|
64
|
+
except ValueError:
|
|
65
|
+
pass
|
|
66
|
+
if len(parts) > 1:
|
|
67
|
+
unit = parts[1].rstrip("s") + "s" if not parts[1].endswith("s") else parts[1]
|
|
68
|
+
|
|
69
|
+
count = max(1, min(count, 100))
|
|
70
|
+
|
|
71
|
+
if unit.startswith("word"):
|
|
72
|
+
return self._words(count)
|
|
73
|
+
elif unit.startswith("sentence"):
|
|
74
|
+
return self._sentences(count)
|
|
75
|
+
else:
|
|
76
|
+
return self._paragraphs(count)
|
|
77
|
+
|
|
78
|
+
def _words(self, count: int) -> str:
|
|
79
|
+
result: list[str] = []
|
|
80
|
+
while len(result) < count:
|
|
81
|
+
result.extend(_WORDS)
|
|
82
|
+
return " ".join(result[:count]).capitalize() + "."
|
|
83
|
+
|
|
84
|
+
def _sentences(self, count: int) -> str:
|
|
85
|
+
result: list[str] = []
|
|
86
|
+
idx = 0
|
|
87
|
+
while len(result) < count:
|
|
88
|
+
result.append(_SENTENCES[idx % len(_SENTENCES)])
|
|
89
|
+
idx += 1
|
|
90
|
+
return " ".join(result[:count])
|
|
91
|
+
|
|
92
|
+
def _paragraphs(self, count: int) -> str:
|
|
93
|
+
paragraphs: list[str] = []
|
|
94
|
+
for i in range(count):
|
|
95
|
+
start = (i * 3) % len(_SENTENCES)
|
|
96
|
+
sents = [_SENTENCES[(start + j) % len(_SENTENCES)] for j in range(4)]
|
|
97
|
+
paragraphs.append(" ".join(sents))
|
|
98
|
+
return "\n\n".join(paragraphs)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def register() -> DevTool:
|
|
102
|
+
return LoremTool()
|