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

1"""Domain models for the MLS MCP server. 

2 

3Pure Python dataclasses with zero framework dependencies. Adapters are 

4responsible for translating to/from these types from the ESPN API wire format. 

5""" 

6 

7from dataclasses import dataclass, field 

8 

9 

10@dataclass 

11class Team: 

12 """An MLS franchise. 

13 

14 id and abbreviation are the stable identifiers used by the ESPN API. 

15 """ 

16 

17 id: str 

18 name: str 

19 abbreviation: str 

20 location: str 

21 display_name: str 

22 logo_url: str | None = None 

23 

24 

25@dataclass 

26class MatchCompetitor: 

27 """One side of a match — home or away team with its score.""" 

28 

29 team: Team 

30 home_away: str 

31 score: str | None = None 

32 winner: bool | None = None 

33 

34 

35@dataclass 

36class Match: 

37 """A single MLS match (scheduled, in-progress, or completed). 

38 

39 status_type values from the ESPN API: 

40 "pre" — scheduled, not yet started 

41 "in" — in progress 

42 "post" — final 

43 """ 

44 

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) 

52 

53 

54@dataclass 

55class NewsArticle: 

56 """A single MLS news article from the ESPN news feed.""" 

57 

58 id: str 

59 headline: str 

60 description: str 

61 published: str 

62 link: str | None = None 

63 

64 

65@dataclass 

66class Player: 

67 """An MLS player on a team's roster. 

68 

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 """ 

73 

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 

81 

82 

83@dataclass 

84class MatchEvent: 

85 """A single key event within a match (goal, substitution, card, etc.). 

86 

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 """ 

91 

92 clock: str 

93 period: int 

94 type: str 

95 scoring: bool 

96 text: str | None = None 

97 team_name: str | None = None 

98 

99 

100@dataclass 

101class MatchDetails: 

102 """Detailed information about a single MLS match. 

103 

104 Combines header data (teams, score, status) with venue, attendance, and 

105 a chronological list of key in-game events. 

106 """ 

107 

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) 

119 

120 

121@dataclass 

122class Standing: 

123 """A team's position in the MLS league table. 

124 

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 """ 

130 

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 

140 

141 

142@dataclass 

143class OpponentPPG: 

144 """One opponent a team has played, paired with that opponent's current PPG. 

145 

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 """ 

150 

151 team: Team 

152 matches_played: int 

153 points: int 

154 points_per_game: float 

155 

156 

157@dataclass 

158class StrengthOfSchedule: 

159 """Opponent-quality summary for a single team. 

160 

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 """ 

165 

166 team: Team 

167 matches_played: int 

168 opponents: list[OpponentPPG] 

169 average_opponent_ppg: float 

170 

171 

172@dataclass 

173class TierRecord: 

174 """A team's W-L-T record against opponents in one current-standings tier.""" 

175 

176 label: str 

177 rank_low: int 

178 rank_high: int 

179 wins: int 

180 losses: int 

181 ties: int 

182 

183 

184@dataclass 

185class ResultsByOpponentTier: 

186 """A team's results split by the current-standings tier of each opponent. 

187 

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 """ 

192 

193 team: Team 

194 tier_size: int 

195 tiers: list[TierRecord] 

196 

197 

198@dataclass 

199class AdjustedPointsPerGame: 

200 """Raw vs. opponent-quality-adjusted PPG for a single team. 

201 

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 """ 

206 

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