Odysseus v1.0
This commit is contained in:
135
scripts/odysseus-signature
Executable file
135
scripts/odysseus-signature
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""odysseus-signature — shell wrapper for stored email signature images.
|
||||
|
||||
The web UI lets you draw a signature; the result lives as a PNG (and
|
||||
optional SVG) in the `signatures` table, keyed by id + owner.
|
||||
|
||||
odysseus-signature list # all signatures (metadata only)
|
||||
odysseus-signature show SIG_ID # full record incl. data_png
|
||||
odysseus-signature export SIG_ID --png OUT # write PNG bytes to a file
|
||||
odysseus-signature delete SIG_ID
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
||||
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
||||
quiet_logs()
|
||||
|
||||
import argparse, base64, json, logging, os, sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from sqlalchemy import text
|
||||
from core.database import engine
|
||||
quiet_logs()
|
||||
except ModuleNotFoundError as e:
|
||||
sys.stderr.write(f"error: {e}\nhint: run from repo root with venv active.\n")
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
"""No `Signature` SQLAlchemy model is registered for the
|
||||
`signatures` table — query via raw SQL so we don't depend on it."""
|
||||
with engine.connect() as conn:
|
||||
rows = conn.execute(text(
|
||||
"SELECT id, owner, name, width, height, "
|
||||
" length(data_png) AS png_len, "
|
||||
" svg IS NOT NULL AS has_svg, "
|
||||
" created_at "
|
||||
"FROM signatures ORDER BY created_at DESC"
|
||||
)).mappings().all()
|
||||
out = [
|
||||
{
|
||||
"id": r["id"],
|
||||
"owner": r["owner"] or "",
|
||||
"name": r["name"],
|
||||
"width": r["width"],
|
||||
"height": r["height"],
|
||||
"png_bytes_approx": r["png_len"],
|
||||
"has_svg": bool(r["has_svg"]),
|
||||
"created_at": str(r["created_at"] or ""),
|
||||
} for r in rows
|
||||
]
|
||||
emit(out, args)
|
||||
|
||||
|
||||
def cmd_show(args):
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(text(
|
||||
"SELECT id, owner, name, width, height, data_png, svg, created_at "
|
||||
"FROM signatures WHERE id = :id"
|
||||
), {"id": args.id}).mappings().first()
|
||||
if not row:
|
||||
fail(f"no signature with id {args.id!r}")
|
||||
emit({
|
||||
"id": row["id"],
|
||||
"owner": row["owner"] or "",
|
||||
"name": row["name"],
|
||||
"width": row["width"],
|
||||
"height": row["height"],
|
||||
"data_png": row["data_png"],
|
||||
"has_svg": bool(row["svg"]),
|
||||
"created_at": str(row["created_at"] or ""),
|
||||
}, args)
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
"""Decode the data_png column to bytes and write to `--png OUT`.
|
||||
`data_png` is stored as a data URL (`data:image/png;base64,...`).
|
||||
We strip the prefix, base64-decode, and write the bytes."""
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(text(
|
||||
"SELECT data_png FROM signatures WHERE id = :id"
|
||||
), {"id": args.id}).mappings().first()
|
||||
if not row:
|
||||
fail(f"no signature with id {args.id!r}")
|
||||
raw = row["data_png"] or ""
|
||||
if "," in raw:
|
||||
raw = raw.split(",", 1)[1]
|
||||
try:
|
||||
png_bytes = base64.b64decode(raw)
|
||||
except Exception as e:
|
||||
fail(f"data_png is not valid base64: {e}")
|
||||
out = Path(args.png)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(png_bytes)
|
||||
emit({"ok": True, "id": args.id, "path": str(out), "bytes": len(png_bytes)}, args)
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
with engine.begin() as conn:
|
||||
res = conn.execute(text("DELETE FROM signatures WHERE id = :id"), {"id": args.id})
|
||||
if res.rowcount == 0:
|
||||
fail(f"no signature with id {args.id!r}")
|
||||
emit({"ok": True, "id": args.id, "deleted": True}, args)
|
||||
|
||||
|
||||
def _build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--pretty", action="store_true")
|
||||
p = argparse.ArgumentParser(prog="odysseus-signature", parents=[common])
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pl = sub.add_parser("list", parents=[common])
|
||||
pl.set_defaults(func=cmd_list)
|
||||
|
||||
psh = sub.add_parser("show", parents=[common])
|
||||
psh.add_argument("id")
|
||||
psh.set_defaults(func=cmd_show)
|
||||
|
||||
pe = sub.add_parser("export", parents=[common])
|
||||
pe.add_argument("id")
|
||||
pe.add_argument("--png", required=True, help="output PNG path")
|
||||
pe.set_defaults(func=cmd_export)
|
||||
|
||||
pd = sub.add_parser("delete", parents=[common])
|
||||
pd.add_argument("id")
|
||||
pd.set_defaults(func=cmd_delete)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run(_build_parser()))
|
||||
Reference in New Issue
Block a user