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,366 @@
|
|
|
1
|
+
"""Secure password generator using secrets module."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import secrets
|
|
5
|
+
import string
|
|
6
|
+
|
|
7
|
+
from devdash.tools.base import DevTool
|
|
8
|
+
|
|
9
|
+
# Word list for passphrases (common English words)
|
|
10
|
+
_WORDLIST = [
|
|
11
|
+
"abandon",
|
|
12
|
+
"ability",
|
|
13
|
+
"able",
|
|
14
|
+
"about",
|
|
15
|
+
"above",
|
|
16
|
+
"absent",
|
|
17
|
+
"absorb",
|
|
18
|
+
"abstract",
|
|
19
|
+
"absurd",
|
|
20
|
+
"abuse",
|
|
21
|
+
"access",
|
|
22
|
+
"accident",
|
|
23
|
+
"account",
|
|
24
|
+
"accuse",
|
|
25
|
+
"achieve",
|
|
26
|
+
"acid",
|
|
27
|
+
"acquire",
|
|
28
|
+
"across",
|
|
29
|
+
"action",
|
|
30
|
+
"actor",
|
|
31
|
+
"actress",
|
|
32
|
+
"actual",
|
|
33
|
+
"adapt",
|
|
34
|
+
"address",
|
|
35
|
+
"adjust",
|
|
36
|
+
"admit",
|
|
37
|
+
"adult",
|
|
38
|
+
"advance",
|
|
39
|
+
"advice",
|
|
40
|
+
"aerobic",
|
|
41
|
+
"afford",
|
|
42
|
+
"afraid",
|
|
43
|
+
"again",
|
|
44
|
+
"agent",
|
|
45
|
+
"agree",
|
|
46
|
+
"ahead",
|
|
47
|
+
"alarm",
|
|
48
|
+
"album",
|
|
49
|
+
"alert",
|
|
50
|
+
"alien",
|
|
51
|
+
"allow",
|
|
52
|
+
"almost",
|
|
53
|
+
"alone",
|
|
54
|
+
"alpha",
|
|
55
|
+
"already",
|
|
56
|
+
"also",
|
|
57
|
+
"alter",
|
|
58
|
+
"always",
|
|
59
|
+
"amateur",
|
|
60
|
+
"amazing",
|
|
61
|
+
"among",
|
|
62
|
+
"amount",
|
|
63
|
+
"amused",
|
|
64
|
+
"anchor",
|
|
65
|
+
"ancient",
|
|
66
|
+
"anger",
|
|
67
|
+
"angle",
|
|
68
|
+
"animal",
|
|
69
|
+
"annual",
|
|
70
|
+
"answer",
|
|
71
|
+
"apart",
|
|
72
|
+
"apple",
|
|
73
|
+
"approve",
|
|
74
|
+
"arctic",
|
|
75
|
+
"arena",
|
|
76
|
+
"armor",
|
|
77
|
+
"army",
|
|
78
|
+
"arrow",
|
|
79
|
+
"artist",
|
|
80
|
+
"assign",
|
|
81
|
+
"assist",
|
|
82
|
+
"atom",
|
|
83
|
+
"attack",
|
|
84
|
+
"attend",
|
|
85
|
+
"august",
|
|
86
|
+
"aunt",
|
|
87
|
+
"author",
|
|
88
|
+
"auto",
|
|
89
|
+
"autumn",
|
|
90
|
+
"average",
|
|
91
|
+
"avocado",
|
|
92
|
+
"avoid",
|
|
93
|
+
"awake",
|
|
94
|
+
"aware",
|
|
95
|
+
"awesome",
|
|
96
|
+
"badge",
|
|
97
|
+
"balance",
|
|
98
|
+
"bamboo",
|
|
99
|
+
"banana",
|
|
100
|
+
"banner",
|
|
101
|
+
"barrel",
|
|
102
|
+
"basket",
|
|
103
|
+
"battle",
|
|
104
|
+
"beach",
|
|
105
|
+
"beauty",
|
|
106
|
+
"become",
|
|
107
|
+
"before",
|
|
108
|
+
"begin",
|
|
109
|
+
"behind",
|
|
110
|
+
"believe",
|
|
111
|
+
"bench",
|
|
112
|
+
"benefit",
|
|
113
|
+
"bicycle",
|
|
114
|
+
"blade",
|
|
115
|
+
"blanket",
|
|
116
|
+
"blast",
|
|
117
|
+
"bless",
|
|
118
|
+
"blind",
|
|
119
|
+
"blood",
|
|
120
|
+
"blossom",
|
|
121
|
+
"board",
|
|
122
|
+
"bonus",
|
|
123
|
+
"border",
|
|
124
|
+
"bottle",
|
|
125
|
+
"bounce",
|
|
126
|
+
"brave",
|
|
127
|
+
"breeze",
|
|
128
|
+
"bridge",
|
|
129
|
+
"bright",
|
|
130
|
+
"broken",
|
|
131
|
+
"brother",
|
|
132
|
+
"budget",
|
|
133
|
+
"bundle",
|
|
134
|
+
"burger",
|
|
135
|
+
"butter",
|
|
136
|
+
"cabin",
|
|
137
|
+
"cable",
|
|
138
|
+
"camera",
|
|
139
|
+
"campus",
|
|
140
|
+
"canal",
|
|
141
|
+
"cancel",
|
|
142
|
+
"candy",
|
|
143
|
+
"cannon",
|
|
144
|
+
"canvas",
|
|
145
|
+
"canyon",
|
|
146
|
+
"captain",
|
|
147
|
+
"carbon",
|
|
148
|
+
"carpet",
|
|
149
|
+
"castle",
|
|
150
|
+
"catalog",
|
|
151
|
+
"catch",
|
|
152
|
+
"cattle",
|
|
153
|
+
"caught",
|
|
154
|
+
"cause",
|
|
155
|
+
"ceiling",
|
|
156
|
+
"celery",
|
|
157
|
+
"cement",
|
|
158
|
+
"census",
|
|
159
|
+
"century",
|
|
160
|
+
"cereal",
|
|
161
|
+
"certain",
|
|
162
|
+
"chair",
|
|
163
|
+
"chalk",
|
|
164
|
+
"champion",
|
|
165
|
+
"change",
|
|
166
|
+
"chapter",
|
|
167
|
+
"charge",
|
|
168
|
+
"chase",
|
|
169
|
+
"cherry",
|
|
170
|
+
"chicken",
|
|
171
|
+
"choice",
|
|
172
|
+
"circle",
|
|
173
|
+
"citizen",
|
|
174
|
+
"civil",
|
|
175
|
+
"claim",
|
|
176
|
+
"clap",
|
|
177
|
+
"clarify",
|
|
178
|
+
"clean",
|
|
179
|
+
"clever",
|
|
180
|
+
"climate",
|
|
181
|
+
"clinic",
|
|
182
|
+
"clock",
|
|
183
|
+
"close",
|
|
184
|
+
"cloud",
|
|
185
|
+
"cluster",
|
|
186
|
+
"coach",
|
|
187
|
+
"coconut",
|
|
188
|
+
"coffee",
|
|
189
|
+
"collect",
|
|
190
|
+
"color",
|
|
191
|
+
"column",
|
|
192
|
+
"combine",
|
|
193
|
+
"comfort",
|
|
194
|
+
"comic",
|
|
195
|
+
"common",
|
|
196
|
+
"company",
|
|
197
|
+
"concert",
|
|
198
|
+
"conduct",
|
|
199
|
+
"confirm",
|
|
200
|
+
"congress",
|
|
201
|
+
"connect",
|
|
202
|
+
"consider",
|
|
203
|
+
"control",
|
|
204
|
+
"convince",
|
|
205
|
+
"cookie",
|
|
206
|
+
"copper",
|
|
207
|
+
"coral",
|
|
208
|
+
"correct",
|
|
209
|
+
"cotton",
|
|
210
|
+
"country",
|
|
211
|
+
"couple",
|
|
212
|
+
"course",
|
|
213
|
+
"cousin",
|
|
214
|
+
"cover",
|
|
215
|
+
"cradle",
|
|
216
|
+
"craft",
|
|
217
|
+
"cream",
|
|
218
|
+
"credit",
|
|
219
|
+
"creek",
|
|
220
|
+
"crew",
|
|
221
|
+
"crisis",
|
|
222
|
+
"cross",
|
|
223
|
+
"crowd",
|
|
224
|
+
"crucial",
|
|
225
|
+
"cruel",
|
|
226
|
+
"cruise",
|
|
227
|
+
"crystal",
|
|
228
|
+
"cube",
|
|
229
|
+
"culture",
|
|
230
|
+
"current",
|
|
231
|
+
"curtain",
|
|
232
|
+
"custom",
|
|
233
|
+
"cycle",
|
|
234
|
+
"damage",
|
|
235
|
+
"dance",
|
|
236
|
+
"danger",
|
|
237
|
+
"daughter",
|
|
238
|
+
"dawn",
|
|
239
|
+
"debate",
|
|
240
|
+
"decade",
|
|
241
|
+
"december",
|
|
242
|
+
"decide",
|
|
243
|
+
"decline",
|
|
244
|
+
"decorate",
|
|
245
|
+
"decrease",
|
|
246
|
+
"defense",
|
|
247
|
+
"define",
|
|
248
|
+
"degree",
|
|
249
|
+
"delay",
|
|
250
|
+
"deliver",
|
|
251
|
+
"demand",
|
|
252
|
+
"denial",
|
|
253
|
+
"dentist",
|
|
254
|
+
"deny",
|
|
255
|
+
"depart",
|
|
256
|
+
"depend",
|
|
257
|
+
"deposit",
|
|
258
|
+
"describe",
|
|
259
|
+
"desert",
|
|
260
|
+
"design",
|
|
261
|
+
"detail",
|
|
262
|
+
"detect",
|
|
263
|
+
"develop",
|
|
264
|
+
"device",
|
|
265
|
+
"devote",
|
|
266
|
+
"diagram",
|
|
267
|
+
"diamond",
|
|
268
|
+
"diary",
|
|
269
|
+
"diesel",
|
|
270
|
+
"differ",
|
|
271
|
+
"digital",
|
|
272
|
+
"dignity",
|
|
273
|
+
"dilemma",
|
|
274
|
+
"dinner",
|
|
275
|
+
"dinosaur",
|
|
276
|
+
"direct",
|
|
277
|
+
"discover",
|
|
278
|
+
"dismiss",
|
|
279
|
+
"display",
|
|
280
|
+
"distance",
|
|
281
|
+
"divide",
|
|
282
|
+
"doctor",
|
|
283
|
+
"document",
|
|
284
|
+
"dolphin",
|
|
285
|
+
"domain",
|
|
286
|
+
"donate",
|
|
287
|
+
"donkey",
|
|
288
|
+
"double",
|
|
289
|
+
"dragon",
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class PasswordTool(DevTool):
|
|
294
|
+
@property
|
|
295
|
+
def name(self) -> str:
|
|
296
|
+
return "Password Generator"
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def keyword(self) -> str:
|
|
300
|
+
return "password"
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def category(self) -> str:
|
|
304
|
+
return "Generators"
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def description(self) -> str:
|
|
308
|
+
return "Generate secure passwords or passphrases"
|
|
309
|
+
|
|
310
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
311
|
+
text = input_text.strip().lower()
|
|
312
|
+
mode = str(kwargs.get("mode", ""))
|
|
313
|
+
|
|
314
|
+
if text == "passphrase" or mode == "passphrase":
|
|
315
|
+
word_count = int(kwargs.get("words", 4))
|
|
316
|
+
return self._passphrase(max(2, min(word_count, 12)))
|
|
317
|
+
|
|
318
|
+
length = 16
|
|
319
|
+
if text.isdigit():
|
|
320
|
+
length = int(text)
|
|
321
|
+
elif kwargs.get("length"):
|
|
322
|
+
length = int(kwargs["length"]) # type: ignore[arg-type]
|
|
323
|
+
|
|
324
|
+
length = max(8, min(length, 128))
|
|
325
|
+
|
|
326
|
+
include_upper = bool(kwargs.get("uppercase", True))
|
|
327
|
+
include_lower = bool(kwargs.get("lowercase", True))
|
|
328
|
+
include_digits = bool(kwargs.get("digits", True))
|
|
329
|
+
include_symbols = bool(kwargs.get("symbols", True))
|
|
330
|
+
count = int(kwargs.get("count", 1))
|
|
331
|
+
count = max(1, min(count, 20))
|
|
332
|
+
|
|
333
|
+
charset = ""
|
|
334
|
+
if include_lower:
|
|
335
|
+
charset += string.ascii_lowercase
|
|
336
|
+
if include_upper:
|
|
337
|
+
charset += string.ascii_uppercase
|
|
338
|
+
if include_digits:
|
|
339
|
+
charset += string.digits
|
|
340
|
+
if include_symbols:
|
|
341
|
+
charset += string.punctuation
|
|
342
|
+
if not charset:
|
|
343
|
+
charset = string.ascii_letters + string.digits
|
|
344
|
+
|
|
345
|
+
results: list[str] = []
|
|
346
|
+
for _ in range(count):
|
|
347
|
+
password = "".join(secrets.choice(charset) for _ in range(length))
|
|
348
|
+
entropy = self._entropy(length, len(charset))
|
|
349
|
+
results.append(f"{password} (entropy: {entropy:.0f} bits)")
|
|
350
|
+
|
|
351
|
+
return "\n".join(results)
|
|
352
|
+
|
|
353
|
+
def _passphrase(self, word_count: int) -> str:
|
|
354
|
+
words = [secrets.choice(_WORDLIST) for _ in range(word_count)]
|
|
355
|
+
passphrase = "-".join(words)
|
|
356
|
+
entropy = math.log2(len(_WORDLIST)) * word_count
|
|
357
|
+
return f"{passphrase}\n\nWords: {word_count}, Entropy: {entropy:.0f} bits"
|
|
358
|
+
|
|
359
|
+
def _entropy(self, length: int, charset_size: int) -> float:
|
|
360
|
+
if charset_size <= 0:
|
|
361
|
+
return 0.0
|
|
362
|
+
return length * math.log2(charset_size)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def register() -> DevTool:
|
|
366
|
+
return PasswordTool()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Regex tester with match highlighting and common presets."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from devdash.tools.base import DevTool
|
|
6
|
+
|
|
7
|
+
PRESETS: dict[str, str] = {
|
|
8
|
+
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
|
9
|
+
"url": r"https?://[^\s<>\"]+",
|
|
10
|
+
"ipv4": r"\b(?:\d{1,3}\.){3}\d{1,3}\b",
|
|
11
|
+
"phone": r"\+?[\d\s\-().]{7,15}",
|
|
12
|
+
"date": r"\d{4}-\d{2}-\d{2}",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RegexTool(DevTool):
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "Regex Tester"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def keyword(self) -> str:
|
|
23
|
+
return "regex"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def category(self) -> str:
|
|
27
|
+
return "Testers"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def description(self) -> str:
|
|
31
|
+
return "Test regex patterns with match highlighting"
|
|
32
|
+
|
|
33
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
34
|
+
# Check kwargs first
|
|
35
|
+
pattern = str(kwargs.get("pattern", ""))
|
|
36
|
+
test_string = str(kwargs.get("test_string", ""))
|
|
37
|
+
|
|
38
|
+
if not input_text.strip() and not pattern:
|
|
39
|
+
return "Error: Empty input. Provide pattern and test string separated by \\n---\\n"
|
|
40
|
+
|
|
41
|
+
if not pattern:
|
|
42
|
+
parts = input_text.split("\n---\n", maxsplit=1)
|
|
43
|
+
if len(parts) == 2:
|
|
44
|
+
pattern = parts[0].strip()
|
|
45
|
+
test_string = parts[1]
|
|
46
|
+
else:
|
|
47
|
+
# Try first line as pattern, rest as test string
|
|
48
|
+
lines = input_text.split("\n", maxsplit=1)
|
|
49
|
+
pattern = lines[0].strip()
|
|
50
|
+
test_string = lines[1] if len(lines) > 1 else ""
|
|
51
|
+
|
|
52
|
+
if not pattern:
|
|
53
|
+
return "Error: No regex pattern provided."
|
|
54
|
+
if not test_string:
|
|
55
|
+
return "Error: No test string provided."
|
|
56
|
+
|
|
57
|
+
# Check for preset
|
|
58
|
+
if pattern.lower() in PRESETS:
|
|
59
|
+
pattern = PRESETS[pattern.lower()]
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
compiled = re.compile(pattern)
|
|
63
|
+
except re.error as e:
|
|
64
|
+
return f"Error: Invalid regex pattern: {e}"
|
|
65
|
+
|
|
66
|
+
matches = list(compiled.finditer(test_string))
|
|
67
|
+
|
|
68
|
+
if not matches:
|
|
69
|
+
return f"Pattern: {pattern}\nNo matches found."
|
|
70
|
+
|
|
71
|
+
lines = [f"Pattern: {pattern}", f"Matches: {len(matches)}", ""]
|
|
72
|
+
|
|
73
|
+
for i, match in enumerate(matches, 1):
|
|
74
|
+
lines.append(f"Match {i}: '{match.group()}' (position {match.start()}-{match.end()})")
|
|
75
|
+
groups = match.groups()
|
|
76
|
+
if groups:
|
|
77
|
+
for j, g in enumerate(groups, 1):
|
|
78
|
+
lines.append(f" Group {j}: '{g}'")
|
|
79
|
+
named = match.groupdict()
|
|
80
|
+
if named:
|
|
81
|
+
for name, val in named.items():
|
|
82
|
+
lines.append(f" Named '{name}': '{val}'")
|
|
83
|
+
|
|
84
|
+
return "\n".join(lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def register() -> DevTool:
|
|
88
|
+
return RegexTool()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Unix timestamp <-> human date converter."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from devdash.tools.base import DevTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _relative_time(dt: datetime) -> str:
|
|
10
|
+
"""Return human-friendly relative time string."""
|
|
11
|
+
now = datetime.now(timezone.utc)
|
|
12
|
+
diff = now - dt
|
|
13
|
+
seconds = int(diff.total_seconds())
|
|
14
|
+
|
|
15
|
+
if seconds < 0:
|
|
16
|
+
return _relative_future(-seconds)
|
|
17
|
+
|
|
18
|
+
if seconds < 60:
|
|
19
|
+
return f"{seconds} seconds ago"
|
|
20
|
+
minutes = seconds // 60
|
|
21
|
+
if minutes < 60:
|
|
22
|
+
return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
|
|
23
|
+
hours = minutes // 60
|
|
24
|
+
if hours < 24:
|
|
25
|
+
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
|
26
|
+
days = hours // 24
|
|
27
|
+
if days < 30:
|
|
28
|
+
return f"{days} day{'s' if days != 1 else ''} ago"
|
|
29
|
+
months = days // 30
|
|
30
|
+
if months < 12:
|
|
31
|
+
return f"{months} month{'s' if months != 1 else ''} ago"
|
|
32
|
+
years = days // 365
|
|
33
|
+
return f"{years} year{'s' if years != 1 else ''} ago"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _relative_future(seconds: int) -> str:
|
|
37
|
+
if seconds < 60:
|
|
38
|
+
return f"in {seconds} seconds"
|
|
39
|
+
minutes = seconds // 60
|
|
40
|
+
if minutes < 60:
|
|
41
|
+
return f"in {minutes} minute{'s' if minutes != 1 else ''}"
|
|
42
|
+
hours = minutes // 60
|
|
43
|
+
if hours < 24:
|
|
44
|
+
return f"in {hours} hour{'s' if hours != 1 else ''}"
|
|
45
|
+
days = hours // 24
|
|
46
|
+
return f"in {days} day{'s' if days != 1 else ''}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TimestampTool(DevTool):
|
|
50
|
+
@property
|
|
51
|
+
def name(self) -> str:
|
|
52
|
+
return "Timestamp Converter"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def keyword(self) -> str:
|
|
56
|
+
return "timestamp"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def category(self) -> str:
|
|
60
|
+
return "Converters"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def description(self) -> str:
|
|
64
|
+
return "Convert between Unix timestamps and human-readable dates"
|
|
65
|
+
|
|
66
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
67
|
+
if not input_text.strip():
|
|
68
|
+
# Show current timestamp
|
|
69
|
+
now = datetime.now(timezone.utc)
|
|
70
|
+
ts = int(now.timestamp())
|
|
71
|
+
return f"Current time:\nUnix: {ts}\nUTC: {now.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
72
|
+
|
|
73
|
+
text = input_text.strip()
|
|
74
|
+
|
|
75
|
+
# Auto-detect: if pure digits, treat as timestamp
|
|
76
|
+
if re.match(r"^\d{10,13}$", text):
|
|
77
|
+
return self._from_timestamp(text)
|
|
78
|
+
else:
|
|
79
|
+
return self._to_timestamp(text)
|
|
80
|
+
|
|
81
|
+
def _from_timestamp(self, text: str) -> str:
|
|
82
|
+
ts = int(text)
|
|
83
|
+
is_millis = len(text) == 13
|
|
84
|
+
if is_millis:
|
|
85
|
+
ts_seconds = ts / 1000
|
|
86
|
+
else:
|
|
87
|
+
ts_seconds = float(ts)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
dt = datetime.fromtimestamp(ts_seconds, tz=timezone.utc)
|
|
91
|
+
except (OSError, OverflowError, ValueError):
|
|
92
|
+
return f"Error: Timestamp {text} is out of valid range."
|
|
93
|
+
|
|
94
|
+
local_tz = datetime.now().astimezone().tzinfo
|
|
95
|
+
local_dt = dt.astimezone(local_tz)
|
|
96
|
+
|
|
97
|
+
lines = [
|
|
98
|
+
f"Input: {text} ({'milliseconds' if is_millis else 'seconds'})",
|
|
99
|
+
"",
|
|
100
|
+
f"UTC: {dt.strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
|
101
|
+
f"Local: {local_dt.strftime('%Y-%m-%d %H:%M:%S %Z')}",
|
|
102
|
+
"",
|
|
103
|
+
f"Relative: {_relative_time(dt)}",
|
|
104
|
+
f"ISO 8601: {dt.isoformat()}",
|
|
105
|
+
]
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
def _to_timestamp(self, text: str) -> str:
|
|
109
|
+
formats = [
|
|
110
|
+
"%Y-%m-%d %H:%M:%S",
|
|
111
|
+
"%Y-%m-%dT%H:%M:%S",
|
|
112
|
+
"%Y-%m-%dT%H:%M:%SZ",
|
|
113
|
+
"%Y-%m-%d",
|
|
114
|
+
"%d/%m/%Y %H:%M:%S",
|
|
115
|
+
"%d/%m/%Y",
|
|
116
|
+
"%m/%d/%Y",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
dt = None
|
|
120
|
+
for fmt in formats:
|
|
121
|
+
try:
|
|
122
|
+
dt = datetime.strptime(text, fmt).replace(tzinfo=timezone.utc)
|
|
123
|
+
break
|
|
124
|
+
except ValueError:
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
if dt is None:
|
|
128
|
+
return f"Error: Could not parse date '{text}'. Supported formats:\n" + "\n".join(
|
|
129
|
+
f" {f}" for f in formats
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
ts = int(dt.timestamp())
|
|
133
|
+
ts_ms = ts * 1000
|
|
134
|
+
return (
|
|
135
|
+
f"Unix (seconds): {ts}\n"
|
|
136
|
+
f"Unix (milliseconds): {ts_ms}\n"
|
|
137
|
+
f"ISO 8601: {dt.isoformat()}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def register() -> DevTool:
|
|
142
|
+
return TimestampTool()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""URL encode/decode and parser."""
|
|
2
|
+
|
|
3
|
+
from urllib.parse import parse_qs, quote, unquote, urlparse
|
|
4
|
+
|
|
5
|
+
from devdash.tools.base import DevTool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UrlTool(DevTool):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "URL Encode / Decode"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def keyword(self) -> str:
|
|
15
|
+
return "url"
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def category(self) -> str:
|
|
19
|
+
return "Encoders / Decoders"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "URL encode/decode and parse URL components"
|
|
24
|
+
|
|
25
|
+
def process(self, input_text: str, **kwargs: object) -> str:
|
|
26
|
+
if not input_text.strip():
|
|
27
|
+
return "Error: Empty input."
|
|
28
|
+
|
|
29
|
+
text = input_text.strip()
|
|
30
|
+
mode = str(kwargs.get("mode", "auto"))
|
|
31
|
+
|
|
32
|
+
if mode == "encode":
|
|
33
|
+
return self._encode(text)
|
|
34
|
+
elif mode == "decode":
|
|
35
|
+
return self._decode(text)
|
|
36
|
+
elif mode == "parse":
|
|
37
|
+
return self._parse(text)
|
|
38
|
+
else:
|
|
39
|
+
# Auto-detect
|
|
40
|
+
if text.startswith(("http://", "https://")):
|
|
41
|
+
return self._parse(text)
|
|
42
|
+
if "%" in text:
|
|
43
|
+
return f"[Decoded]\n{self._decode(text)}"
|
|
44
|
+
return f"[Encoded]\n{self._encode(text)}"
|
|
45
|
+
|
|
46
|
+
def _encode(self, text: str) -> str:
|
|
47
|
+
return quote(text, safe="")
|
|
48
|
+
|
|
49
|
+
def _decode(self, text: str) -> str:
|
|
50
|
+
return unquote(text)
|
|
51
|
+
|
|
52
|
+
def _parse(self, text: str) -> str:
|
|
53
|
+
parsed = urlparse(text)
|
|
54
|
+
params = parse_qs(parsed.query)
|
|
55
|
+
|
|
56
|
+
lines = [
|
|
57
|
+
f"Scheme: {parsed.scheme or '(none)'}",
|
|
58
|
+
f"Host: {parsed.hostname or '(none)'}",
|
|
59
|
+
f"Port: {parsed.port or '(default)'}",
|
|
60
|
+
f"Path: {parsed.path or '/'}",
|
|
61
|
+
f"Query: {parsed.query or '(none)'}",
|
|
62
|
+
f"Fragment: {parsed.fragment or '(none)'}",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
if params:
|
|
66
|
+
lines.append("\nQuery Parameters:")
|
|
67
|
+
for key, values in params.items():
|
|
68
|
+
for val in values:
|
|
69
|
+
lines.append(f" {key} = {val}")
|
|
70
|
+
|
|
71
|
+
return "\n".join(lines)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def register() -> DevTool:
|
|
75
|
+
return UrlTool()
|