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,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()