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.
@@ -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()