From c77ccf1eb953cd8ea2a0b1ca4482cdae5c29e3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Hlavat=C3=BD?= Date: Sun, 22 Mar 2026 11:38:46 +0100 Subject: [PATCH] Pretty print --- src/beaky/cli.py | 103 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/src/beaky/cli.py b/src/beaky/cli.py index dee97be..0203021 100644 --- a/src/beaky/cli.py +++ b/src/beaky/cli.py @@ -1,10 +1,13 @@ import argparse +import re as _re +from datetime import datetime import yaml from pydantic import ValidationError from beaky import _ansi from beaky.config import Config +from beaky.datamodels.ticket import Bet, Ticket from beaky.image_classifier.classifier import img_classify from beaky.link_classifier.classifier import LinkClassifier from beaky.resolvers.resolver import TicketResolver, TicketVerdict @@ -23,6 +26,98 @@ def _verdict_str(verdict: TicketVerdict) -> str: return _ansi.gray(text) +_FC = 14 # field column visual width +_VC = 24 # value column visual width + +_FIELD_LABELS: dict[str, str] = { + "team1Name": "team1", + "team2Name": "team2", +} +_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"] +_SKIP_FIELDS = {"ticketType"} + + +def _vlen(text: str) -> int: + """Visual length of a string — strips ANSI escape codes.""" + return len(_re.sub(r"\033\[[^m]*m", "", text)) + + +def _vpad(text: str, width: int) -> str: + """Pad text to visual width, accounting for ANSI codes.""" + return text + " " * max(0, width - _vlen(text)) + + +def _bet_fields(bet: Bet) -> dict[str, str]: + fields: dict[str, str] = {"type": type(bet).__name__} + for k, v in vars(bet).items(): + if k in _SKIP_FIELDS: + continue + if k == "date" and isinstance(v, datetime): + fields[k] = v.strftime("%Y-%m-%d %H:%M") + else: + fields[k] = str(v) + return fields + + +def _tbl_row(field: str, lval: str, ival: str) -> str: + label = _FIELD_LABELS.get(field, field) + return f" │ {_vpad(label, _FC)} │ {_vpad(lval, _VC)} │ {_vpad(ival, _VC)} │" + + +def _tbl_divider(left: str, mid: str, right: str, fill: str = "─") -> str: + return f" {left}{fill * (_FC + 2)}{mid}{fill * (_VC + 2)}{mid}{fill * (_VC + 2)}{right}" + + +def _print_bet_compare(idx: int, link_bet: Bet | None, img_bet: Bet | None) -> None: + print(f"\n {_ansi.bold(_ansi.cyan(f' Bet {idx} '))}") + + link_fields = _bet_fields(link_bet) if link_bet is not None else {} + img_fields = _bet_fields(img_bet) if img_bet is not None else {} + all_keys = link_fields.keys() | img_fields.keys() + keys = [k for k in _FIELD_ORDER if k in all_keys] + [k for k in all_keys if k not in _FIELD_ORDER] + + print(_tbl_divider("┌", "┬", "┐")) + print(_tbl_row("field", _ansi.bold("link classifier"), _ansi.bold("image classifier"))) + print(_tbl_divider("├", "┼", "┤")) + + for key in keys: + lval_raw = link_fields.get(key, "") + ival_raw = img_fields.get(key, "") + match = lval_raw == ival_raw + + lval = _ansi.gray("—") if lval_raw == "" else (lval_raw if match else _ansi.yellow(lval_raw)) + ival = _ansi.gray("—") if ival_raw == "" else (ival_raw if match else _ansi.yellow(ival_raw)) + + # truncate raw value if too long before applying color + if len(lval_raw) > _VC: + lval_raw = lval_raw[:_VC - 1] + "…" + lval = lval_raw if match else _ansi.yellow(lval_raw) + if len(ival_raw) > _VC: + ival_raw = ival_raw[:_VC - 1] + "…" + ival = ival_raw if match else _ansi.yellow(ival_raw) + + print(_tbl_row(key, lval, ival)) + + print(_tbl_divider("└", "┴", "┘")) + + +def _print_compare(link_ticket: Ticket, img_ticket: Ticket) -> None: + n_link = len(link_ticket.bets) + n_img = len(img_ticket.bets) + header = f" Ticket {link_ticket.id} — link: {n_link} bet{'s' if n_link != 1 else ''} │ img: {n_img} bet{'s' if n_img != 1 else ''}" + total_w = _FC + _VC * 2 + 10 + print(f"\n{'═' * total_w}") + print(_ansi.bold(header)) + print(f"{'═' * total_w}") + + for i, (lb, ib) in enumerate(zip(link_ticket.bets, img_ticket.bets), start=1): + _print_bet_compare(i, lb, ib) + for i, lb in enumerate(link_ticket.bets[n_img:], start=n_img + 1): + _print_bet_compare(i, lb, None) + for i, ib in enumerate(img_ticket.bets[n_link:], start=n_link + 1): + _print_bet_compare(i, None, ib) + + def load_config(path: str) -> Config | None: with open(path) as f: config_dict = yaml.safe_load(f) @@ -80,11 +175,9 @@ def main() -> None: print(f"ERROR: ticket id {args.id} not found") return for link in links: - linkClass = linkclassifier.classify(link) - imgClass = img_classify(["./data/screenshots/{link.id}.png"], ticket_id=link.id) - - print(linkClass) - print(imgClass) + link_ticket = linkclassifier.classify(link) + img_ticket = img_classify([f"./data/screenshots/{link.id}.png"], ticket_id=link.id) + _print_compare(link_ticket, img_ticket) if args.mode == "resolve": classifier = LinkClassifier()