Implement CLI functionality and enhance README instructions

This commit is contained in:
zvspany
2026-04-29 17:31:27 +02:00
parent d17eed7314
commit 4dd83a8cda
7 changed files with 223 additions and 9 deletions

View File

@@ -1,3 +1,11 @@
from .app import CheckAddyApp
from __future__ import annotations
__all__ = ["CheckAddyApp"]
def __getattr__(name: str) -> object:
if name == "CheckAddyApp":
from .app import CheckAddyApp
return CheckAddyApp
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

156
checkaddy_app/cli.py Normal file
View File

@@ -0,0 +1,156 @@
from __future__ import annotations
import argparse
import json
import sys
from collections.abc import Sequence
from .api import ApiClient
from .constants import COIN_DISPLAY_SYMBOL, COIN_OPTIONS, POLYGON
from .formatters import format_amount_display, format_validation_badge
from .lookup import build_lookup_result
from .models import LookupResult
from .validators import validate_address
COIN_ALIASES = {
"MATIC": POLYGON,
"POLYGON": POLYGON,
"BNB": "BSC",
}
SUPPORTED_COINS = tuple(coin for coin, _ in COIN_OPTIONS)
EXTRA_COIN_ALIASES = tuple(sorted(alias for alias in COIN_ALIASES if alias not in SUPPORTED_COINS))
def launch_tui() -> None:
from .app import CheckAddyApp
CheckAddyApp().run()
def normalize_coin(value: str) -> str:
coin = value.strip().upper()
normalized = COIN_ALIASES.get(coin, coin)
if normalized not in SUPPORTED_COINS:
supported = ", ".join(SUPPORTED_COINS + EXTRA_COIN_ALIASES)
raise argparse.ArgumentTypeError(f"unsupported coin '{value}'. Supported: {supported}")
return normalized
def detect_matching_coins(address: str) -> list[tuple[str, str]]:
matches: list[tuple[str, str]] = []
for coin in SUPPORTED_COINS:
is_valid, reason = validate_address(coin, address)
if is_valid:
matches.append((coin, reason))
return matches
def resolve_coin(address: str, requested_coin: str | None) -> tuple[str | None, str | None]:
if requested_coin is not None:
return requested_coin, None
matches = detect_matching_coins(address)
if not matches:
return None, "Address does not match any supported network format."
if len(matches) == 1:
return matches[0][0], None
choices = ", ".join(coin for coin, _ in matches)
return None, f"Address is ambiguous across networks: {choices}. Re-run with --coin."
def render_text_result(result: LookupResult) -> str:
tx_count = str(result.tx_count) if result.tx_count is not None else "N/A"
status = "api skipped" if result.api_skipped else "api error" if result.api_error else "ok"
lines = [
"checkaddy",
f"Status: {status}",
f"Coin: {result.coin} ({COIN_DISPLAY_SYMBOL[result.coin]})",
f"Address: {result.address}",
f"Validation: {format_validation_badge(result.is_valid_format, result.validation_reason)}",
f"Confirmed balance: {format_amount_display(result.coin, result.confirmed_balance)}",
f"Unconfirmed balance: {format_amount_display(result.coin, result.unconfirmed_balance)}",
f"Total received: {format_amount_display(result.coin, result.total_received)}",
f"Total sent: {format_amount_display(result.coin, result.total_sent)}",
f"Transaction count: {tx_count}",
f"Data source: {result.data_source}",
f"Explorer: {result.explorer_url}",
f"Fetched at UTC: {result.fetched_at_utc}",
]
if result.api_error:
lines.append(f"API error: {result.api_error}")
if result.api_skipped:
lines.append("Remote lookup skipped because the address format is invalid.")
return "\n".join(lines)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="checkaddy",
description=(
"Check a public cryptocurrency address from the terminal. "
"Run without arguments to launch the Textual TUI."
),
)
parser.add_argument("address", nargs="?", help="public wallet address to validate and inspect")
parser.add_argument(
"-c",
"--coin",
type=normalize_coin,
help="network to check: BTC, LTC, DOGE, DASH, BCH, ETH, BSC, POLYGON/MATIC",
)
parser.add_argument("--json", action="store_true", help="print normalized JSON output")
parser.add_argument("--tui", action="store_true", help="launch the Textual TUI")
return parser
def run_lookup(address: str, coin: str, *, as_json: bool) -> int:
client = ApiClient()
try:
result = build_lookup_result(client, coin, address)
finally:
client.close()
if as_json:
print(json.dumps(result.as_dict(), indent=2, ensure_ascii=False))
else:
print(render_text_result(result))
if result.api_skipped:
return 2
if result.api_error:
return 1
return 0
def main(argv: Sequence[str] | None = None) -> int:
args_list = list(sys.argv[1:] if argv is None else argv)
if not args_list:
launch_tui()
return 0
parser = build_parser()
args = parser.parse_args(args_list)
if args.tui:
launch_tui()
return 0
if not args.address:
parser.error("address is required unless running without arguments or using --tui")
coin, error = resolve_coin(args.address.strip(), args.coin)
if error is not None:
if args.json:
print(json.dumps({"error": error}, indent=2), file=sys.stderr)
else:
print(f"checkaddy: {error}", file=sys.stderr)
return 2
assert coin is not None
return run_lookup(args.address.strip(), coin, as_json=args.json)
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,11 +1,7 @@
from __future__ import annotations
from .app import CheckAddyApp
def main() -> None:
CheckAddyApp().run()
from .cli import main
if __name__ == "__main__":
main()
raise SystemExit(main())