Coverage for src/mls/domain/models.py: 100%
106 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-05 10:11 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-05 10:11 +0000
1"""Domain models for the MLS MCP server.
3Pure Python dataclasses with zero framework dependencies. Adapters are
4responsible for translating to/from these types from the ESPN API wire format.
5"""
7from dataclasses import dataclass, field
10@dataclass
11class Team:
12 """An MLS franchise.
14 id and abbreviation are the stable identifiers used by the ESPN API.
15 """
17 id: str
18 name: str
19 abbreviation: str
20 location: str
21 display_name: str
22 logo_url: str | None = None
25@dataclass
26class MatchCompetitor:
27 """One side of a match — home or away team with its score."""
29 team: Team
30 home_away: str
31 score: str | None = None
32 winner: bool | None = None
35@dataclass
36class Match:
37 """A single MLS match (scheduled, in-progress, or completed).
39 status_type values from the ESPN API:
40 "pre" — scheduled, not yet started
41 "in" — in progress
42 "post" — final
43 """
45 id: str
46 date: str
47 name: str
48 short_name: str
49 status_type: str
50 status_detail: str
51 competitors: list[MatchCompetitor] = field(default_factory=list)
54@dataclass
55class NewsArticle:
56 """A single MLS news article from the ESPN news feed."""
58 id: str
59 headline: str
60 description: str
61 published: str
62 link: str | None = None
65@dataclass
66class Player:
67 """An MLS player on a team's roster.
69 Optional fields (jersey, position, citizenship, age) may be missing for
70 unsigned, recently traded, or international players whose data ESPN has
71 not fully populated.
72 """
74 id: str
75 full_name: str
76 jersey: str | None = None
77 position: str | None = None
78 position_abbr: str | None = None
79 citizenship: str | None = None
80 age: int | None = None
83@dataclass
84class MatchEvent:
85 """A single key event within a match (goal, substitution, card, etc.).
87 Mirrors ESPN's keyEvents entries: type is the raw event tag
88 (e.g. "goal", "goal---header", "yellow-card", "substitution"); scoring
89 is a convenience flag for goal events.
90 """
92 clock: str
93 period: int
94 type: str
95 scoring: bool
96 text: str | None = None
97 team_name: str | None = None
100@dataclass
101class MatchDetails:
102 """Detailed information about a single MLS match.
104 Combines header data (teams, score, status) with venue, attendance, and
105 a chronological list of key in-game events.
106 """
108 id: str
109 date: str
110 status_detail: str
111 home_team: str
112 away_team: str
113 home_score: str | None = None
114 away_score: str | None = None
115 venue: str | None = None
116 venue_city: str | None = None
117 attendance: int | None = None
118 key_events: list[MatchEvent] = field(default_factory=list)
121@dataclass
122class Standing:
123 """A team's position in the MLS league table.
125 MLS is split into Eastern and Western Conferences; the ``conference``
126 field carries the human-readable name so callers (formatters, analytics)
127 can group by it. When absent the row came from an ungrouped feed and
128 should be treated as league-wide.
129 """
131 team: Team
132 wins: int
133 losses: int
134 ties: int
135 points: int
136 goals_for: int
137 goals_against: int
138 goal_difference: int
139 conference: str | None = None
142@dataclass
143class OpponentPPG:
144 """One opponent a team has played, paired with that opponent's current PPG.
146 ``points_per_game`` is computed from the opponent's full league record (no
147 self-exclusion), so it reflects current standings position rather than a
148 strict RPI-style adjustment.
149 """
151 team: Team
152 matches_played: int
153 points: int
154 points_per_game: float
157@dataclass
158class StrengthOfSchedule:
159 """Opponent-quality summary for a single team.
161 Aggregates the current points-per-game of every opponent the team has
162 actually faced (completed matches only). Useful for "who has played the
163 tougher schedule so far?" questions early in the season.
164 """
166 team: Team
167 matches_played: int
168 opponents: list[OpponentPPG]
169 average_opponent_ppg: float
172@dataclass
173class TierRecord:
174 """A team's W-L-T record against opponents in one current-standings tier."""
176 label: str
177 rank_low: int
178 rank_high: int
179 wins: int
180 losses: int
181 ties: int
184@dataclass
185class ResultsByOpponentTier:
186 """A team's results split by the current-standings tier of each opponent.
188 Tiers are derived from the live league table at call time, not the table
189 at the time each match was played — interpret as "how have you done
190 against teams that are currently strong/middle/weak?"
191 """
193 team: Team
194 tier_size: int
195 tiers: list[TierRecord]
198@dataclass
199class AdjustedPointsPerGame:
200 """Raw vs. opponent-quality-adjusted PPG for a single team.
202 ``adjusted_ppg`` scales raw PPG by ``average_opponent_ppg / league_average_ppg``,
203 so values above raw PPG mean the team has earned points against a tougher
204 schedule than league average.
205 """
207 team: Team
208 matches_played: int
209 points: int
210 raw_ppg: float
211 average_opponent_ppg: float
212 league_average_ppg: float
213 adjusted_ppg: float