Coverage for dao/lineup_dao.py: 9.78%

70 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2026-04-13 14:11 +0000

1""" 

2Lineup Data Access Object. 

3 

4Handles all database operations for match lineups/formations: 

5- Get lineup for a team in a match 

6- Save/update lineup (upsert) 

7""" 

8 

9import structlog 

10 

11from dao.base_dao import BaseDAO, invalidates_cache 

12 

13logger = structlog.get_logger() 

14 

15# Cache patterns for invalidation 

16LINEUP_CACHE_PATTERN = "mt:dao:lineup:*" 

17 

18 

19class LineupDAO(BaseDAO): 

20 """Data access object for match lineup operations.""" 

21 

22 def get_lineup(self, match_id: int, team_id: int) -> dict | None: 

23 """ 

24 Get lineup for a team in a specific match. 

25 

26 Args: 

27 match_id: Match ID 

28 team_id: Team ID 

29 

30 Returns: 

31 Lineup dict with enriched player details or None if not found 

32 """ 

33 try: 

34 response = ( 

35 self.client.table("match_lineups") 

36 .select("*") 

37 .eq("match_id", match_id) 

38 .eq("team_id", team_id) 

39 .execute() 

40 ) 

41 

42 if not response.data or len(response.data) == 0: 

43 return None 

44 

45 lineup = response.data[0] 

46 

47 # Enrich positions with player details 

48 positions = lineup.get("positions", []) 

49 if positions: 

50 player_ids = [p.get("player_id") for p in positions if p.get("player_id")] 

51 if player_ids: 

52 players_response = ( 

53 self.client.table("players") 

54 .select("id, jersey_number, first_name, last_name") 

55 .in_("id", player_ids) 

56 .execute() 

57 ) 

58 players_map = {p["id"]: p for p in (players_response.data or [])} 

59 

60 # Enrich each position with player info 

61 for pos in positions: 

62 player_id = pos.get("player_id") 

63 if player_id and player_id in players_map: 

64 player = players_map[player_id] 

65 pos["jersey_number"] = player.get("jersey_number") 

66 pos["first_name"] = player.get("first_name") 

67 pos["last_name"] = player.get("last_name") 

68 first = player.get("first_name", "") 

69 last = player.get("last_name", "") 

70 pos["display_name"] = f"{first} {last}".strip() 

71 

72 lineup["positions"] = positions 

73 

74 return lineup 

75 

76 except Exception as e: 

77 logger.error("lineup_get_error", match_id=match_id, team_id=team_id, error=str(e)) 

78 return None 

79 

80 @invalidates_cache(LINEUP_CACHE_PATTERN) 

81 def save_lineup( 

82 self, 

83 match_id: int, 

84 team_id: int, 

85 formation_name: str, 

86 positions: list[dict], 

87 user_id: str | None = None, 

88 ) -> dict | None: 

89 """ 

90 Save or update lineup for a team in a match (upsert). 

91 

92 Args: 

93 match_id: Match ID 

94 team_id: Team ID 

95 formation_name: Formation name (e.g., "4-3-3") 

96 positions: List of {player_id, position} dicts 

97 user_id: ID of user making the change 

98 

99 Returns: 

100 Saved lineup dict 

101 """ 

102 try: 

103 # Check if lineup exists 

104 existing = ( 

105 self.client.table("match_lineups") 

106 .select("id") 

107 .eq("match_id", match_id) 

108 .eq("team_id", team_id) 

109 .execute() 

110 ) 

111 

112 lineup_data = { 

113 "match_id": match_id, 

114 "team_id": team_id, 

115 "formation_name": formation_name, 

116 "positions": positions, 

117 } 

118 

119 if existing.data and len(existing.data) > 0: 

120 # Update existing 

121 lineup_data["updated_by"] = user_id 

122 response = ( 

123 self.client.table("match_lineups") 

124 .update(lineup_data) 

125 .eq("match_id", match_id) 

126 .eq("team_id", team_id) 

127 .execute() 

128 ) 

129 else: 

130 # Insert new 

131 lineup_data["created_by"] = user_id 

132 lineup_data["updated_by"] = user_id 

133 response = self.client.table("match_lineups").insert(lineup_data).execute() 

134 

135 if response.data and len(response.data) > 0: 

136 logger.info( 

137 "lineup_saved", 

138 match_id=match_id, 

139 team_id=team_id, 

140 formation=formation_name, 

141 player_count=len(positions), 

142 ) 

143 return response.data[0] 

144 return None 

145 

146 except Exception as e: 

147 logger.error( 

148 "lineup_save_error", 

149 match_id=match_id, 

150 team_id=team_id, 

151 error=str(e), 

152 ) 

153 return None 

154 

155 def get_lineups_for_match(self, match_id: int) -> dict: 

156 """ 

157 Get lineups for both teams in a match. 

158 

159 Args: 

160 match_id: Match ID 

161 

162 Returns: 

163 Dict with 'home' and 'away' keys containing lineups (or None) 

164 """ 

165 try: 

166 response = ( 

167 self.client.table("match_lineups") 

168 .select("*") 

169 .eq("match_id", match_id) 

170 .execute() 

171 ) 

172 

173 # Get match to determine home/away team IDs 

174 match_response = ( 

175 self.client.table("matches") 

176 .select("home_team_id, away_team_id") 

177 .eq("id", match_id) 

178 .execute() 

179 ) 

180 

181 result = {"home": None, "away": None} 

182 

183 if not match_response.data: 

184 return result 

185 

186 match = match_response.data[0] 

187 home_team_id = match.get("home_team_id") 

188 away_team_id = match.get("away_team_id") 

189 

190 for lineup in (response.data or []): 

191 team_id = lineup.get("team_id") 

192 if team_id == home_team_id: 

193 result["home"] = lineup 

194 elif team_id == away_team_id: 

195 result["away"] = lineup 

196 

197 return result 

198 

199 except Exception as e: 

200 logger.error("lineup_get_match_error", match_id=match_id, error=str(e)) 

201 return {"home": None, "away": None}