Initial upload: Devtoolbelt v1.0.0 - unified CLI toolkit for developers
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-01 21:45:50 +00:00
parent f44408e663
commit 378f417135

View File

@@ -0,0 +1,413 @@
"""Utility commands for Devtoolbelt."""
import hashlib
import ipaddress
import json
import random
import string
import time
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import click
from rich import print as rprint
from rich.table import Table
from ..utils import console, format_duration
@click.group()
def utils():
"""Common utility functions."""
pass
@utils.command("timestamp")
@click.option(
"--to-iso", "-i",
is_flag=True,
help="Convert to ISO format."
)
@click.option(
"--to-unix", "-u",
is_flag=True,
help="Convert to Unix timestamp."
)
@click.option(
"--timezone", "-z",
type=click.Choice(["local", "utc"]),
default="local",
help="Timezone for display."
)
def timestamp(to_iso: bool, to_unix: bool, tz: str):
"""Get current timestamp in various formats."""
now = datetime.now(timezone.utc)
table = Table(title="Current Timestamp")
table.add_column("Format", style="cyan")
table.add_column("Value", style="green")
if tz == "utc":
now_local = now
else:
now_local = datetime.now()
table.add_row("ISO 8601", now_local.isoformat())
table.add_row("Unix", str(int(now_local.timestamp())))
table.add_row("RFC 2822", now_local.strftime("%a, %d %b %Y %H:%M:%S %z"))
table.add_row("Human Readable", now_local.strftime("%Y-%m-%d %H:%M:%S"))
console.print(table)
@utils.command("hash")
@click.argument("input_string")
@click.option(
"--algorithm", "-a",
type=click.Choice(["md5", "sha1", "sha256", "sha512"]),
default="sha256",
help="Hash algorithm to use."
)
def hash_string(input_string: str, algorithm: str):
"""Generate hash of a string."""
hasher = hashlib.new(algorithm)
hasher.update(input_string.encode('utf-8'))
result = hasher.hexdigest()
rprint(f"[bold cyan]{algorithm.upper()}:[/bold cyan] [green]{result}[/green]")
@utils.command("hash-file")
@click.argument("file_path", type=click.Path(exists=True))
@click.option(
"--algorithm", "-a",
type=click.Choice(["md5", "sha1", "sha256", "sha512"]),
default="sha256",
help="Hash algorithm to use."
)
def hash_file(file_path: str, algorithm: str):
"""Generate hash of a file."""
hasher = hashlib.new(algorithm)
with open(file_path, 'rb') as f:
hasher.update(f.read())
result = hasher.hexdigest()
rprint(f"[bold cyan]{algorithm.upper()}:[/bold cyan] [green]{result}[/green]")
@utils.command("uuid")
@click.option(
"--count", "-n",
type=int,
default=1,
help="Number of UUIDs to generate."
)
def generate_uuid(count: int):
"""Generate UUID(s)."""
import uuid
for _ in range(min(count, 100)):
rprint(str(uuid.uuid4()))
@utils.command("random")
@click.option(
"--length", "-l",
type=int,
default=32,
help="Length of random string."
)
@click.option(
"--chars", "-c",
type=click.Choice(["all", "letters", "digits", "hex"]),
default="all",
help="Character set to use."
)
def random_string(length: int, chars: str):
"""Generate random string."""
if chars == "all":
characters = string.ascii_letters + string.digits + string.punctuation
elif chars == "letters":
characters = string.ascii_letters
elif chars == "digits":
characters = string.digits
elif chars == "hex":
characters = string.hexdigits.lower()
result = ''.join(random.choice(characters) for _ in range(length))
rprint(result)
@utils.command("json")
@click.argument("input_string")
@click.option(
"--validate", "-v",
is_flag=True,
help="Only validate JSON format."
)
@click.option(
"--minify", "-m",
is_flag=True,
help="Minify JSON output."
)
def json_tool(input_string: str, validate: bool, minify: bool):
"""Format or validate JSON."""
try:
data = json.loads(input_string)
if validate:
rprint("[green]Valid JSON[/green]")
return
if minify:
result = json.dumps(data)
else:
result = json.dumps(data, indent=2)
rprint(result)
except json.JSONDecodeError as e:
rprint(f"[red]Invalid JSON: {e}[/red]")
@utils.command("base64")
@click.argument("input_string")
@click.option(
"--decode", "-d",
is_flag=True,
help="Decode instead of encode."
)
def base64_tool(input_string: str, decode: bool):
"""Encode or decode Base64."""
import base64
try:
if decode:
result = base64.b64decode(input_string).decode('utf-8')
else:
result = base64.b64encode(input_string.encode('utf-8')).decode('utf-8')
rprint(result)
except Exception as e:
rprint(f"[red]Error: {e}[/red]")
@utils.command("url")
@click.argument("input_string")
@click.option(
"--decode", "-d",
is_flag=True,
help="Decode instead of encode."
)
def url_tool(input_string: str, decode: bool):
"""Encode or decode URL."""
from urllib.parse import quote, unquote
try:
if decode:
result = unquote(input_string)
else:
result = quote(input_string, safe='')
rprint(result)
except Exception as e:
rprint(f"[red]Error: {e}[/red]")
@utils.command("ip")
@click.argument("ip_address")
@click.option(
"--details", "-d",
is_flag=True,
help="Show detailed IP information."
)
def ip_info(ip_address: str, details: bool):
"""Get information about an IP address."""
try:
ip = ipaddress.ip_address(ip_address)
table = Table(title=f"IP Information: {ip_address}")
table.add_column("Property", style="cyan")
table.add_column("Value", style="green")
table.add_row("Address", str(ip))
table.add_row("Version", f"IPv{ip.version}")
table.add_row("Is Private", str(ip.is_private))
table.add_row("Is Global", str(ip.is_global))
table.add_row("Is Loopback", str(ip.is_loopback))
table.add_row("Is Multicast", str(ip.is_multicast))
if details:
table.add_row("Packed", ip.packed.hex())
table.add_row("Reverse DNS", str(ip.reverse_pointer) if ip.is_global else "N/A")
console.print(table)
except ValueError as e:
rprint(f"[red]Invalid IP address: {e}[/red]")
@utils.command("jwt")
@click.argument("token")
@click.option(
"--decode", "-d",
is_flag=True,
help="Decode JWT without verification."
)
@click.option(
"--show-header", "-H",
is_flag=True,
help="Show JWT header."
)
def jwt_tool(token: str, decode: bool, show_header: bool):
"""Decode JWT token."""
import base64
parts = token.split('.')
if len(parts) != 3:
rprint("[red]Invalid JWT format[/red]")
return
try:
def decode_part(part: str) -> Dict[str, Any]:
padding = 4 - len(part) % 4
if padding != 4:
part += '=' * padding
decoded = base64.urlsafe_b64decode(part)
return json.loads(decoded.decode('utf-8'))
if show_header or decode:
header = decode_part(parts[0])
rprint("[bold cyan]Header:[/bold cyan]")
rprint(json.dumps(header, indent=2))
payload = decode_part(parts[1])
rprint("[bold cyan]Payload:[/bold cyan]")
rprint(json.dumps(payload, indent=2))
except Exception as e:
rprint(f"[red]Error decoding JWT: {e}[/red]")
@utils.command("cron")
@click.argument("cron_expression")
def cron_info(cron_expression: str):
"""Parse and describe cron expression."""
parts = cron_expression.split()
if len(parts) != 5:
rprint("[red]Invalid cron expression (expected 5 fields)[/red]")
return
minute, hour, day, month, dow = parts
table = Table(title=f"Cron Expression: {cron_expression}")
table.add_column("Field", style="cyan")
table.add_column("Value", style="green")
table.add_row("Minute", _describe_cron_field(minute, 0, 59, "minutes"))
table.add_row("Hour", _describe_cron_field(hour, 0, 23, "hours"))
table.add_row("Day of Month", _describe_cron_field(day, 1, 31, "days"))
table.add_row("Month", _describe_cron_field(month, 1, 12, "months"))
table.add_row("Day of Week", _describe_cron_field(dow, 0, 6, "weekdays", ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]))
console.print(table)
def _describe_cron_field(value: str, min_val: int, max_val: int, unit: str, names: Optional[List[str]] = None) -> str:
"""Describe a cron field."""
if value == "*":
return f"Every {unit}"
result = []
parts = value.split(',')
for part in parts:
if "/" in part:
base, step = part.split("/")
if base == "*":
result.append(f"Every {step} {unit}")
else:
result.append(f"Every {step} {unit} starting from {base}")
elif "-" in part:
start, end = part.split("-")
result.append(f"{start} to {end}")
else:
if names and part.isdigit():
idx = int(part)
if 0 <= idx < len(names):
result.append(names[idx])
continue
result.append(part)
return ", ".join(result)
@utils.command("password")
@click.option(
"--length", "-l",
type=int,
default=16,
help="Password length."
)
@click.option(
"--no-special", "-n",
is_flag=True,
help="Exclude special characters."
)
@click.option(
"--count", "-c",
type=int,
default=1,
help="Number of passwords to generate."
)
def generate_password(length: int, no_special: bool, count: int):
"""Generate secure password(s)."""
alphabet = string.ascii_letters + string.digits
if not no_special:
alphabet += string.punctuation
for _ in range(min(count, 20)):
password = ''.join(random.choice(alphabet) for _ in range(length))
rprint(password)
@utils.command("timer")
@click.option(
"--seconds", "-s",
type=int,
default=0,
help="Timer duration in seconds."
)
@click.option(
"--minutes", "-m",
type=int,
default=0,
help="Timer duration in minutes."
)
def timer(seconds: int, minutes: int):
"""Simple countdown timer."""
total_seconds = seconds + minutes * 60
if total_seconds <= 0:
rprint("[yellow]Please specify a duration.[/yellow]")
return
rprint(f"[bold]Starting timer for {total_seconds} seconds...[/bold]")
for remaining in range(total_seconds, 0, -1):
mins, secs = divmod(remaining, 60)
rprint(f"\r[bold]{mins:02d}:{secs:02d}[/bold] ", end="", flush=True)
time.sleep(1)
rprint("\n[bold green]⏰ Time's up![/bold green]")
@utils.command("stopwatch")
def stopwatch():
"""Simple stopwatch."""
rprint("[bold]Press Enter to start, Enter to stop...[/bold]")
input()
start_time = time.time()
rprint("[bold]Stopwatch running... Press Enter to stop.[/bold]")
input()
elapsed = time.time() - start_time
rprint(f"[bold green]Elapsed: {format_duration(elapsed)}[/bold green]")