AutoPayer Tools — 6 free Python CLI tools, zero dependencies

AutoPayer Tools — 6 free Python CLI tools, zero dependencies


Free single-file Python CLI tools — standard library only, no pip, no dependencies. Copy any of them and they just work on Python 3.8+. MIT license, public donations welcome.

webpify

Batch-convert images (JPG/PNG/GIF/BMP) to WebP. Perfect to shrink folders of photos before uploading.

#!/usr/bin/env python3
"""webpify - batch convert/compress images to WebP (CLI).
ETH donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse, sys
from pathlib import Path
try:
from PIL import Image
except ImportError:
sys.exit("Pillow is required: pip install Pillow")

def main():
p = argparse.ArgumentParser(description="Convert images to optimized WebP")
p.add_argument("paths", nargs="+")
p.add_argument("-q", type=int, default=80)
p.add_argument("-o", "--outdir", default=".")
p.add_argument("--resize", type=int, default=None, help="max width in px")
p.add_argument("--strip", action="store_true", help="remove EXIF metadata")
a = p.parse_args()
out = Path(a.outdir); out.mkdir(parents=True, exist_ok=True)
exts = {".jpg",".jpeg",".png",".bmp",".gif",".tif",".tiff",".webp"}
files = [f for pth in a.paths for f in (Path(pth).rglob("*") if Path(pth).is_dir() else [Path(pth)]) if f.suffix.lower() in exts]
saved = 0
for f in files:
dest = out / (f.stem + ".webp")
try:
im = Image.open(f)
if im.mode not in ("RGB","RGBA"): im = im.convert("RGB")
if a.resize and a.resize < im.width:
im = im.resize((a.resize, round(im.height * a.resize / im.width)))
kwargs = {"quality": a.q}
if a.strip:
clean = Image.new(im.mode, im.size); clean.putdata(list(im.getdata())); im = clean
elif "exif" in im.info:
kwargs["exif"] = im.info["exif"]
im.save(dest, "WEBP", **kwargs)
d = f.stat().st_size - dest.stat().st_size; saved += d
print(f"{f.name}: {f.stat().st_size//1024}KB -> {dest.stat().st_size//1024}KB")
except Exception as e:
print(f"{f.name}: ERROR {e}")
print(f"Total saved: {max(saved,0)//1024} KB across {len(files)} files")

if __name__ == "__main__":
main()

csvstat

Instant statistics for any CSV: per-column types, missing values, min/max/mean/std, top values. No pandas needed.

#!/usr/bin/env python3
"""csvstat: quick CSV stats from the terminal. Pure stdlib.
Usage:
python3 csvstat.py data.csv
python3 csvstat.py --cols age,salary data.csv
curl -s https://.../data.csv | python3 csvstat.py -
Per column: type, missing, unique; numeric ones: min/max/mean/stdev.
Exit 0. MIT license.
Donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse, csv, sys, statistics

def fmt(x):
if isinstance(x, float):
return f"{x:.4g}"
return str(x)

def main():
ap = argparse.ArgumentParser(prog="csvstat", description="Quick CSV statistics")
ap.add_argument("source", help="CSV file, or '-' for stdin")
ap.add_argument("--cols", default="", help="comma-separated columns (default: all)")
ap.add_argument("--delim", default=",")
a = ap.parse_args()
fh = sys.stdin if a.source == "-" else open(a.source, newline="", encoding="utf-8-sig")
rdr = csv.DictReader(fh, delimiter=a.delim)
rows = list(rdr)
fh.close()
if not rows:
print("no rows", file=sys.stderr); sys.exit(2)
cols = [c for c in (a.cols.split(",") if a.cols else rdr.fieldnames) if c in rdr.fieldnames]
print(f"rows: {len(rows)} columns: {len(rdr.fieldnames)}")
for c in cols:
vals = [r[c] for r in rows]
miss = sum(1 for v in vals if v == "")
uniq = len({v for v in vals if v != ""})
nums = []
for v in vals:
if v == "": continue
try: nums.append(float(v))
except ValueError: pass
if len(nums) >= len(vals) - miss and nums and miss < len(vals):
mean = statistics.fmean(nums)
sd = statistics.pstdev(nums) if len(nums) > 1 else 0.0
info = (f"numeric miss={miss} uniq={uniq} min={fmt(min(nums))} "
f"max={fmt(max(nums))} mean={fmt(mean)} stdev={fmt(sd)}")
else:
info = f"text miss={miss} uniq={uniq}"
print(f" {c}: {info}")

if __name__ == "__main__":
main()

md2page

Convert Markdown to a standalone styled HTML page (no JavaScript, no dependencies).

#!/usr/bin/env python3
"""md2page: Markdown -> standalone HTML page (inline CSS), pure stdlib.
Usage: python3 md2page.py page.md -o index.html --title "My page"
Supports: #..### headings, paragraphs, **bold**, *italic*, `code`,
[triple-backtick code blocks], - lists, [t](u) links, --- rules.
Output: a .html with no external deps, ready for Neocities/Pages/attachment.
Exit 0. MIT. Donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse, html, re, sys

CSS = ("body{font:16px/1.6 system-ui,sans-serif;max-width:720px;margin:2rem auto;"
"padding:0 1rem;color:#1a1a1a}h1,h2,h3{line-height:1.2}code,pre{background:#f4f4f4;"
"border-radius:6px}code{padding:.15em .35em}pre{padding:1em;overflow:auto}"
"a{color:#0b62d6}hr{border:0;border-top:1px solid #ddd}footer{margin-top:3rem;"
"font-size:.85em;color:#888}")

def inline(t):
t = html.escape(t, quote=False)
t = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", t)
t = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", t)
t = re.sub(r"`([^`]+)`", r"<code>\1</code>", t)
t = re.sub(r"\[([^\]]+)\]\((https?://[^)\s]+)\)", r'<a href="\2">\1</a>', t)
return t

def render(md, footer=""):
out, i, lines = [], 0, md.splitlines()
while i < len(lines):
ln = lines[i]
if ln.startswith("```"):
buf = []
i += 1
while i < len(lines) and not lines[i].startswith("```"):
buf.append(lines[i]); i += 1
i += 1
out.append("<pre><code>" + html.escape("\n".join(buf)) + "</code></pre>")
elif ln.startswith("### "): out.append("<h3>" + inline(ln[4:]) + "</h3>"); i += 1
elif ln.startswith("## "): out.append("<h2>" + inline(ln[3:]) + "</h2>"); i += 1
elif ln.startswith("# "): out.append("<h1>" + inline(ln[2:]) + "</h1>"); i += 1
elif ln.strip() in ("---", "***"): out.append("<hr>"); i += 1
elif ln.startswith("- "):
buf = []
while i < len(lines) and lines[i].startswith("- "):
buf.append("<li>" + inline(lines[i][2:]) + "</li>"); i += 1
out.append("<ul>" + "".join(buf) + "</ul>")
elif ln.strip() == "": i += 1
else:
buf = [ln]
while i + 1 < len(lines) and lines[i+1].strip() and not re.match(r"^(#|```|- |\-\-\-)", lines[i+1]):
i += 1; buf.append(lines[i])
out.append("<p>" + inline(" ".join(buf)) + "</p>"); i += 1
body = "\n".join(out) + (f'<footer>{html.escape(footer)}</footer>' if footer else "")
return f"<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'><style>{CSS}</style>{body}"

def main():
ap = argparse.ArgumentParser(prog="md2page")
ap.add_argument("md"); ap.add_argument("-o", default="")
ap.add_argument("--title", default=""); ap.add_argument("--footer", default="")
a = ap.parse_args()
md = sys.stdin.read() if a.md == "-" else open(a.md, encoding="utf-8").read()
page = render(md, a.footer)
if a.o: open(a.o, "w", encoding="utf-8").write(page)
else: sys.stdout.write(page)

if __name__ == "__main__":
main()

uptimer

Monitor one or many URLs from cron: exit code 2 on downtime, so any cron+mail setup becomes an uptime alert.

#!/usr/bin/env python3
"""uptimer: watch URLs from the terminal. Handy in cron.
Usage:
python3 uptimer.py https://example.com https://api.example.com/health
python3 uptimer.py --timeout 5 --retries 2 https://example.com
echo "https://example.com" | python3 uptimer.py -
Exit code 0 = all OK; 1 = at least one down (perfect for cron + alerts).
Stdlib only. MIT license.
Donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse, sys, time, urllib.request, urllib.error

UA = {"User-Agent": "uptimer/1.0"}

def check(url, timeout):
req = urllib.request.Request(url, headers=UA)
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status in range(200, 400)
except urllib.error.HTTPError as e:
return e.code in range(200, 400)
except Exception:
return False

def main():
ap = argparse.ArgumentParser(prog="uptimer", description="URL health-checks for cron")
ap.add_argument("urls", nargs="+", help="URLs, or '-' to read from stdin (one per line)")
ap.add_argument("--timeout", type=float, default=10)
ap.add_argument("--retries", type=int, default=1)
ap.add_argument("--sleep", type=float, default=2, help="seconds between retries")
a = ap.parse_args()
urls = []
for u in a.urls:
if u == "-":
urls += [l.strip() for l in sys.stdin if l.strip() and not l.startswith("#")]
else:
urls.append(u)
down = []
for u in urls:
ok = False
for attempt in range(1, a.retries + 1):
ok = check(u, a.timeout)
if ok:
break
if attempt < a.retries:
time.sleep(a.sleep)
mark = "UP " if ok else "DOWN"
print(f"[{mark}] {u}" + ("" if ok else f" (after {a.retries} attempt(s))"))
if not ok:
down.append(u)
sys.exit(1 if down else 0)

if __name__ == "__main__":
main()

qrpay

Generate QR codes as PNG/SVG from any text or EIP-681 crypto payment URI. Pure Python.

#!/usr/bin/env python3
"""qrpay: generates payment QR codes (ERC-681) or free text, as PNG and ASCII.
Usage: python3 qrpay.py --to 0x... [--amount 0.01] [-o pay.png]
python3 qrpay.py --text "hello world" -o note.png
No external services: everything is local. Requires: pip install qrcode pillow
MIT license. Donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse
from decimal import Decimal
import qrcode

def main():
ap = argparse.ArgumentParser(prog="qrpay", description="ETH payment QR (ERC-681) or free text")
ap.add_argument("--to", help="destination Ethereum address")
ap.add_argument("--amount", type=Decimal, help="amount in ETH")
ap.add_argument("--text", help="free content (if not using --to)")
ap.add_argument("-o", "--out", default="qrpay.png", help="output PNG file (def: qrpay.png)")
ap.add_argument("--size", type=int, default=10, help="box size in px (def 10)")
a = ap.parse_args()

if a.to:
addr = a.to.strip()
if not (addr.startswith("0x") and len(addr) == 42):
ap.error("address must start with 0x and be 42 chars long")
if a.amount is not None and a.amount < 0:
ap.error("--amount cannot be negative")
uri = "ethereum:" + addr
if a.amount is not None:
wei = int((a.amount * Decimal(10) ** 18).to_integral_value())
uri += "?value=" + str(wei)
data = uri
elif a.text:
data = a.text
else:
ap.error("provide --to (ETH payment) or --text (free QR)")
return

img = qrcode.make(data, box_size=a.size, border=2)
img.save(a.out)
print(f"PNG: {a.out}")
print("Content:", data)
qr = qrcode.QRCode(border=1)
qr.add_data(data)
qr.make()
qr.print_ascii(invert=True)

if __name__ == "__main__":
main()

ethbal

Check Ethereum balances (ETH + tokens) from the terminal via public RPC, no API key needed.

#!/usr/bin/env python3
"""ethbal: ETH balance of any address, with USD value.
Public RPC and public CoinGecko only (no signups, no keys).
Usage:
python3 ethbal.py 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
python3 ethbal.py 0xABC... 0xDEF... # several at once
python3 ethbal.py 0xABC... --rpc https://cloudflare-eth.com
Deps: stdlib only. MIT license.
Donations: 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c
"""
import argparse, json, sys, urllib.request

RPCS = ["https://ethereum-rpc.publicnode.com", "https://eth.drpc.org", "https://rpc.ankr.com/eth"]

def rpc_call(method, params, rpc):
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
req = urllib.request.Request(rpc, data=body, headers={"Content-Type": "application/json", "User-Agent": "ethbal/1.0"})
return json.load(urllib.request.urlopen(req, timeout=15)).get("result")

def balance(eth_addr, rpc):
hexbal = rpc_call("eth_getBalance", [eth_addr, "latest"], rpc)
if hexbal is None:
return None
return int(hexbal, 16) / 1e18

def eth_price():
try:
with urllib.request.urlopen("https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd", timeout=10) as r:
return json.load(r)["ethereum"]["usd"]
except Exception:
return None

def main():
ap = argparse.ArgumentParser(prog="ethbal", description="ETH balance (+USD) via public RPC")
ap.add_argument("addresses", nargs="+")
ap.add_argument("--rpc", default=None, help="RPC endpoint (default: list of public ones)")
ap.add_argument("--no-usd", action="store_true")
a = ap.parse_args()
price = None if a.no_usd else eth_price()
for addr in a.addresses:
bal, used = None, a.rpc or RPCS[0]
endpoints = [a.rpc] if a.rpc else RPCS
for rpc in endpoints:
try:
bal = balance(addr, rpc)
if bal is not None:
used = rpc
break
except Exception:
continue
if bal is None:
print(f"{addr} ERROR: no RPC response")
continue
usd = f" (~${bal * price:,.2f} USD)" if price else ""
print(f"{addr} {bal:.6f} ETH{usd}")

if __name__ == "__main__":
main()

Install: save the file, chmod +x, run with python3. Suggestions and feedback are very welcome.Tips (ETH): 0xD16bb23f47f1F6C315daC29F9ce25A3BFFf7CE0c

Report Page