Coverage for src/usls/domain/models.py: 100%

105 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-05 10:11 +0000

1"""Domain models for the USL Super League 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 USL Super League 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 USL Super League 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 USL Super League 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 USL Super League 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 USL Super League 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 USL Super League table.""" 

124 

125 team: Team 

126 wins: int 

127 losses: int 

128 ties: int 

129 points: int 

130 goals_for: int 

131 goals_against: int 

132 goal_difference: int 

133 

134 

135@dataclass 

136class OpponentPPG: 

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

138 

139 ``points_per_game`` is computed from the opponent's full league record (no 

140 self-exclusion), so it reflects current standings position rather than a 

141 strict RPI-style adjustment. 

142 """ 

143 

144 team: Team 

145 matches_played: int 

146 points: int 

147 points_per_game: float 

148 

149 

150@dataclass 

151class StrengthOfSchedule: 

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

153 

154 Aggregates the current points-per-game of every opponent the team has 

155 actually faced (completed matches only). Useful for "who has played the 

156 tougher schedule so far?" questions early in the season. 

157 """ 

158 

159 team: Team 

160 matches_played: int 

161 opponents: list[OpponentPPG] 

162 average_opponent_ppg: float 

163 

164 

165@dataclass 

166class TierRecord: 

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

168 

169 label: str 

170 rank_low: int 

171 rank_high: int 

172 wins: int 

173 losses: int 

174 ties: int 

175 

176 

177@dataclass 

178class ResultsByOpponentTier: 

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

180 

181 Tiers are derived from the live league table at call time, not the table 

182 at the time each match was played — interpret as "how have you done 

183 against teams that are currently strong/middle/weak?" 

184 """ 

185 

186 team: Team 

187 tier_size: int 

188 tiers: list[TierRecord] 

189 

190 

191@dataclass 

192class AdjustedPointsPerGame: 

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

194 

195 ``adjusted_ppg`` scales raw PPG by ``average_opponent_ppg / league_average_ppg``, 

196 so values above raw PPG mean the team has earned points against a tougher 

197 schedule than league average. 

198 """ 

199 

200 team: Team 

201 matches_played: int 

202 points: int 

203 raw_ppg: float 

204 average_opponent_ppg: float 

205 league_average_ppg: float 

206 adjusted_ppg: float