Initial upload: Devtoolbelt v1.0.0 - unified CLI toolkit for developers
This commit is contained in:
413
devtoolbelt/commands/utils.py
Normal file
413
devtoolbelt/commands/utils.py
Normal 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]")
|
||||||
Reference in New Issue
Block a user