Make bets pretty print

This commit is contained in:
2026-03-22 11:50:58 +01:00
parent d8e71d3483
commit 255a311b04
2 changed files with 57 additions and 44 deletions

View File

@@ -1,5 +1,6 @@
import argparse
import re as _re
import shutil
from datetime import datetime
import yaml
@@ -28,6 +29,8 @@ def _verdict_str(verdict: TicketVerdict) -> str:
_FC = 14 # field column visual width
_VC = 24 # value column visual width
_BET_W = 1 + (_FC + 2) + 1 + (_VC + 2) + 1 + (_VC + 2) + 1 # one bet table width
_GAP = " " # space between side-by-side tables
_FIELD_LABELS: dict[str, str] = {
"team1Name": "team1",
@@ -35,15 +38,14 @@ _FIELD_LABELS: dict[str, str] = {
}
_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"]
_SKIP_FIELDS = {"ticketType"}
_BLANK_ROW = f"{' ' * (_FC + 2)}{' ' * (_VC + 2)}{' ' * (_VC + 2)}"
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))
@@ -52,70 +54,81 @@ def _bet_fields(bet: Bet) -> dict[str, str]:
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)
fields[k] = v.strftime("%Y-%m-%d %H:%M") if k == "date" and isinstance(v, datetime) else 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)}"
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 _tbl_sep(left: str, mid: str, right: str) -> str:
return f"{left}{'' * (_FC + 2)}{mid}{'' * (_VC + 2)}{mid}{'' * (_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} '))}")
def _bet_to_lines(idx: int, link_bet: Bet | None, img_bet: Bet | None) -> list[str]:
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("", "", ""))
data_rows = []
for key in keys:
lval_raw = link_fields.get(key, "")
ival_raw = img_fields.get(key, "")
match = lval_raw == ival_raw
both = bool(lval_raw) and bool(ival_raw)
lval_raw = lval_raw[:_VC - 1] + "" if len(lval_raw) > _VC else lval_raw
ival_raw = ival_raw[:_VC - 1] + "" if len(ival_raw) > _VC else ival_raw
lval = _ansi.gray("") if not lval_raw else (lval_raw if (match or not both) else _ansi.yellow(lval_raw))
ival = _ansi.gray("") if not ival_raw else (ival_raw if (match or not both) else _ansi.yellow(ival_raw))
data_rows.append(_tbl_row(key, lval, ival))
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))
header = _vpad(_ansi.bold(_ansi.cyan(f" Bet {idx} ")), _BET_W)
return [
header,
_tbl_sep("", "", ""),
_tbl_row("", _ansi.bold("link classifier"), _ansi.bold("image classifier")),
_tbl_sep("", "", ""),
*data_rows,
_tbl_sep("", "", ""),
]
# 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 _pad_to(lines: list[str], target: int) -> list[str]:
result = list(lines)
while len(result) < target:
result.insert(-1, _BLANK_ROW)
return result
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}")
n_max = max(n_link, n_img)
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)
term_w = shutil.get_terminal_size((120, 24)).columns
n_cols = max(1, term_w // (_BET_W + len(_GAP)))
row_w = min(term_w, n_cols * (_BET_W + len(_GAP)) - len(_GAP) + 2)
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 ''}"
print(f"\n{'' * row_w}")
print(_ansi.bold(header))
print(f"{'' * row_w}")
all_lines = [
_bet_to_lines(i + 1, link_ticket.bets[i] if i < n_link else None, img_ticket.bets[i] if i < n_img else None)
for i in range(n_max)
]
for start in range(0, n_max, n_cols):
chunk = all_lines[start:start + n_cols]
max_h = max(len(b) for b in chunk)
padded = [_pad_to(b, max_h) for b in chunk]
print()
for row in zip(*padded):
print(" " + _GAP.join(row))
def load_config(path: str) -> Config | None:

View File

@@ -13,7 +13,7 @@ from beaky.datamodels.ticket import (
BetOutcome,
MatchInfo,
Ticket,
UnknownTicket,
UnknownBet,
)
from beaky.resolvers.config import ResolverConfig
@@ -53,8 +53,8 @@ class ResolvedTicket:
@property
def verdict(self) -> TicketVerdict:
resolvable = [b for b in self.bets if not isinstance(b.bet, UnknownTicket)]
unresolvable = [b for b in self.bets if isinstance(b.bet, UnknownTicket)]
resolvable = [b for b in self.bets if not isinstance(b.bet, UnknownBet)]
unresolvable = [b for b in self.bets if isinstance(b.bet, UnknownBet)]
if not resolvable:
return TicketVerdict.UNKNOWN
if any(b.outcome == BetOutcome.LOSE for b in resolvable):
@@ -99,7 +99,7 @@ class TicketResolver:
print(f"\n {_ansi.bold(_ansi.cyan(f'┌─ [{bet_type}]'))} {_ansi.bold(f'{bet.team1Name} vs {bet.team2Name}')}"
f" {_ansi.dim(f'{bet.date.strftime('%Y-%m-%d')} | {bet.league}')}")
if isinstance(bet, UnknownTicket):
if isinstance(bet, UnknownBet):
print(_ansi.gray(f" │ skipping — not implemented: {bet.raw_text!r}"))
print(_ansi.gray(" └─ UNKNOWN"))
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN)