Make bets pretty print
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import re as _re
|
import re as _re
|
||||||
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@@ -28,6 +29,8 @@ def _verdict_str(verdict: TicketVerdict) -> str:
|
|||||||
|
|
||||||
_FC = 14 # field column visual width
|
_FC = 14 # field column visual width
|
||||||
_VC = 24 # value 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] = {
|
_FIELD_LABELS: dict[str, str] = {
|
||||||
"team1Name": "team1",
|
"team1Name": "team1",
|
||||||
@@ -35,15 +38,14 @@ _FIELD_LABELS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"]
|
_FIELD_ORDER = ["type", "team1Name", "team2Name", "date", "league"]
|
||||||
_SKIP_FIELDS = {"ticketType"}
|
_SKIP_FIELDS = {"ticketType"}
|
||||||
|
_BLANK_ROW = f"│{' ' * (_FC + 2)}│{' ' * (_VC + 2)}│{' ' * (_VC + 2)}│"
|
||||||
|
|
||||||
|
|
||||||
def _vlen(text: str) -> int:
|
def _vlen(text: str) -> int:
|
||||||
"""Visual length of a string — strips ANSI escape codes."""
|
|
||||||
return len(_re.sub(r"\033\[[^m]*m", "", text))
|
return len(_re.sub(r"\033\[[^m]*m", "", text))
|
||||||
|
|
||||||
|
|
||||||
def _vpad(text: str, width: int) -> str:
|
def _vpad(text: str, width: int) -> str:
|
||||||
"""Pad text to visual width, accounting for ANSI codes."""
|
|
||||||
return text + " " * max(0, width - _vlen(text))
|
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():
|
for k, v in vars(bet).items():
|
||||||
if k in _SKIP_FIELDS:
|
if k in _SKIP_FIELDS:
|
||||||
continue
|
continue
|
||||||
if k == "date" and isinstance(v, datetime):
|
fields[k] = v.strftime("%Y-%m-%d %H:%M") if k == "date" and isinstance(v, datetime) else str(v)
|
||||||
fields[k] = v.strftime("%Y-%m-%d %H:%M")
|
|
||||||
else:
|
|
||||||
fields[k] = str(v)
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def _tbl_row(field: str, lval: str, ival: str) -> str:
|
def _tbl_row(field: str, lval: str, ival: str) -> str:
|
||||||
label = _FIELD_LABELS.get(field, field)
|
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:
|
def _tbl_sep(left: str, mid: str, right: str) -> str:
|
||||||
return f" {left}{fill * (_FC + 2)}{mid}{fill * (_VC + 2)}{mid}{fill * (_VC + 2)}{right}"
|
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:
|
def _bet_to_lines(idx: int, link_bet: Bet | None, img_bet: Bet | None) -> list[str]:
|
||||||
print(f"\n {_ansi.bold(_ansi.cyan(f' Bet {idx} '))}")
|
|
||||||
|
|
||||||
link_fields = _bet_fields(link_bet) if link_bet is not None else {}
|
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 {}
|
img_fields = _bet_fields(img_bet) if img_bet is not None else {}
|
||||||
all_keys = link_fields.keys() | img_fields.keys()
|
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]
|
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("┌", "┬", "┐"))
|
data_rows = []
|
||||||
print(_tbl_row("field", _ansi.bold("link classifier"), _ansi.bold("image classifier")))
|
|
||||||
print(_tbl_divider("├", "┼", "┤"))
|
|
||||||
|
|
||||||
for key in keys:
|
for key in keys:
|
||||||
lval_raw = link_fields.get(key, "")
|
lval_raw = link_fields.get(key, "")
|
||||||
ival_raw = img_fields.get(key, "")
|
ival_raw = img_fields.get(key, "")
|
||||||
match = lval_raw == ival_raw
|
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))
|
header = _vpad(_ansi.bold(_ansi.cyan(f" Bet {idx} ")), _BET_W)
|
||||||
ival = _ansi.gray("—") if ival_raw == "" else (ival_raw if match else _ansi.yellow(ival_raw))
|
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))
|
def _pad_to(lines: list[str], target: int) -> list[str]:
|
||||||
|
result = list(lines)
|
||||||
print(_tbl_divider("└", "┴", "┘"))
|
while len(result) < target:
|
||||||
|
result.insert(-1, _BLANK_ROW)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _print_compare(link_ticket: Ticket, img_ticket: Ticket) -> None:
|
def _print_compare(link_ticket: Ticket, img_ticket: Ticket) -> None:
|
||||||
n_link = len(link_ticket.bets)
|
n_link = len(link_ticket.bets)
|
||||||
n_img = len(img_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 ''}"
|
n_max = max(n_link, n_img)
|
||||||
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):
|
term_w = shutil.get_terminal_size((120, 24)).columns
|
||||||
_print_bet_compare(i, lb, ib)
|
n_cols = max(1, term_w // (_BET_W + len(_GAP)))
|
||||||
for i, lb in enumerate(link_ticket.bets[n_img:], start=n_img + 1):
|
row_w = min(term_w, n_cols * (_BET_W + len(_GAP)) - len(_GAP) + 2)
|
||||||
_print_bet_compare(i, lb, None)
|
|
||||||
for i, ib in enumerate(img_ticket.bets[n_link:], start=n_link + 1):
|
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_bet_compare(i, None, ib)
|
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:
|
def load_config(path: str) -> Config | None:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from beaky.datamodels.ticket import (
|
|||||||
BetOutcome,
|
BetOutcome,
|
||||||
MatchInfo,
|
MatchInfo,
|
||||||
Ticket,
|
Ticket,
|
||||||
UnknownTicket,
|
UnknownBet,
|
||||||
)
|
)
|
||||||
from beaky.resolvers.config import ResolverConfig
|
from beaky.resolvers.config import ResolverConfig
|
||||||
|
|
||||||
@@ -53,8 +53,8 @@ class ResolvedTicket:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def verdict(self) -> TicketVerdict:
|
def verdict(self) -> TicketVerdict:
|
||||||
resolvable = [b for b in self.bets if not 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, UnknownTicket)]
|
unresolvable = [b for b in self.bets if isinstance(b.bet, UnknownBet)]
|
||||||
if not resolvable:
|
if not resolvable:
|
||||||
return TicketVerdict.UNKNOWN
|
return TicketVerdict.UNKNOWN
|
||||||
if any(b.outcome == BetOutcome.LOSE for b in resolvable):
|
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}')}"
|
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}')}")
|
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(f" │ skipping — not implemented: {bet.raw_text!r}"))
|
||||||
print(_ansi.gray(" └─ UNKNOWN"))
|
print(_ansi.gray(" └─ UNKNOWN"))
|
||||||
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN)
|
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN)
|
||||||
|
|||||||
Reference in New Issue
Block a user