Coverage for src/ecnl/adapters/inbound/formatters.py: 96%
78 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-28 08:19 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-28 08:19 +0000
1"""Formatters — domain models -> human/LLM-readable strings.
3Each ``_fmt_*`` takes a domain result and returns a compact text block. Keeping
4formatting here (out of the tool functions) means the tool bodies stay one line
5and the wire format is consistent across tools.
6"""
8import json
9from typing import Any
11from ...domain.models import (
12 Club,
13 Event,
14 EventOverview,
15 Match,
16 MatchResult,
17 Standings,
18 Team,
19 TeamRPI,
20)
23def _fmt_events(events: list[Event]) -> str:
24 """Format a list of discovered events."""
25 if not events:
26 return "No matching events found."
27 lines = [f"{len(events)} event(s):"]
28 for e in events:
29 loc = f" — {e.location}" if e.location else ""
30 lines.append(f" [{e.event_id}] {e.league} {e.gender} · {e.conference} · {e.season}{loc}")
31 return "\n".join(lines)
34def _fmt_event_overview(overview: EventOverview) -> str:
35 """Format an event's division/flight tree."""
36 e = overview.event
37 lines = [
38 f"{e.name} [event {e.event_id}]",
39 f" league={e.league} gender={e.gender} conference={e.conference} season={e.season}",
40 ]
41 if not overview.divisions:
42 lines.append(" (no divisions published)")
43 for d in overview.divisions:
44 lines.append(f" Division {d.name} [{d.division_id}] ({d.gender}):")
45 for f in d.flights:
46 sched = "schedule active" if f.has_active_schedule else "no schedule yet"
47 lines.append(f" flight [{f.flight_id}] {f.name} — {f.teams_count} teams, {sched}")
48 return "\n".join(lines)
51def _fmt_standings(standings: Standings) -> str:
52 """Format a flight standings table."""
53 if not standings.rows:
54 return f"No standings available for flight {standings.flight_id}."
55 lines = [
56 f"Standings — event {standings.event_id}, division {standings.division_id}, flight {standings.flight_id}:",
57 f" {'#':>2} {'Team':<34} {'W-L-D':>8} {'Pts':>4} {'PPG':>5}",
58 ]
59 for i, r in enumerate(standings.rows, start=1):
60 rank = r.rank if r.rank is not None else i
61 record = f"{r.wins}-{r.losses}-{r.draws}"
62 lines.append(f" {rank:>2} {r.team_name[:34]:<34} {record:>8} {r.points:>4} {r.points_per_game:>5.2f}")
63 return "\n".join(lines)
66def _fmt_match_line(m: Match) -> str:
67 """Format a single match as one line."""
68 when = " ".join(x for x in (m.date, m.time) if x) or "TBD"
69 score = f"{m.home_score}-{m.away_score}" if m.is_played else "vs"
70 venue = f" @ {m.venue}" if m.venue else ""
71 return f" {when}: {m.home_team} {score} {m.away_team}{venue}"
74def _fmt_schedule(matches: list[Match]) -> str:
75 """Format a flight or team schedule."""
76 if not matches:
77 return "No matches scheduled."
78 return "\n".join([f"{len(matches)} match(es):", *(_fmt_match_line(m) for m in matches)])
81def _fmt_teams(teams: list[Team]) -> str:
82 """Format a team list."""
83 if not teams:
84 return "No teams found."
85 lines = [f"{len(teams)} team(s):"]
86 for t in teams:
87 coach = f" — coach {t.head_coach}" if t.head_coach else ""
88 lines.append(f" [{t.team_id}] {t.name}{coach}")
89 return "\n".join(lines)
92def _fmt_clubs(clubs: list[Club]) -> str:
93 """Format a club list."""
94 if not clubs:
95 return "No clubs found."
96 lines = [f"{len(clubs)} club(s):"]
97 for c in clubs:
98 where = ", ".join(x for x in (c.city, c.state_code) if x)
99 where = f" ({where})" if where else ""
100 lines.append(f" [{c.club_id}] {c.name}{where}")
101 return "\n".join(lines)
104def _fmt_results(results: list[MatchResult]) -> str:
105 """Format extracted completed results."""
106 if not results:
107 return "No completed matches yet."
108 lines = [f"{len(results)} completed match(es):"]
109 for r in results:
110 lines.append(f" {r.home_team} {r.home_score}-{r.away_score} {r.away_team}")
111 return "\n".join(lines)
114def _fmt_rpi(table: list[TeamRPI]) -> str:
115 """Format a full RPI table."""
116 if not table:
117 return "No completed matches — RPI cannot be computed yet."
118 lines = [
119 "RPI = 0.25·WP + 0.50·OWP + 0.25·OOWP",
120 f" {'#':>2} {'Team':<34} {'W-L-D':>8} {'WP':>6} {'OWP':>6} {'OOWP':>6} {'RPI':>6}",
121 ]
122 for r in table:
123 record = f"{r.wins}-{r.losses}-{r.draws}"
124 lines.append(
125 f" {r.rank:>2} {r.team[:34]:<34} {record:>8} {r.wp:>6.3f} {r.owp:>6.3f} {r.oowp:>6.3f} {r.rpi:>6.3f}"
126 )
127 return "\n".join(lines)
130def _fmt_team_rpi(r: TeamRPI) -> str:
131 """Format a single team's RPI breakdown."""
132 return (
133 f"{r.team} — RPI {r.rpi:.4f} (rank {r.rank})\n"
134 f" record: {r.wins}-{r.losses}-{r.draws}\n"
135 f" WP (0.25): {r.wp:.4f}\n"
136 f" OWP (0.50): {r.owp:.4f}\n"
137 f" OOWP (0.25): {r.oowp:.4f}"
138 )
141def _fmt_raw(data: Any) -> str:
142 """Format an unstructured payload (match detail, brackets) as pretty JSON."""
143 if not data:
144 return "No data available."
145 return json.dumps(data, indent=2, default=str)