Coverage for manage_live_match.py: 0.00%

209 statements  

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

1#!/usr/bin/env python3 

2""" 

3Live Match Management CLI Tool 

4 

5This tool helps manage live matches during development and testing. 

6Works directly with the database via DAOs. 

7 

8Usage: 

9 python manage_live_match.py status <match_id> # Show live match status 

10 python manage_live_match.py list # List all live matches 

11 python manage_live_match.py clear-events <id> # Delete all events for a match 

12 python manage_live_match.py reset <match_id> # Reset match to pre-live state 

13""" 

14 

15import os 

16import sys 

17from pathlib import Path 

18 

19import typer 

20from rich import box 

21from rich.console import Console 

22from rich.prompt import Confirm 

23from rich.table import Table 

24 

25# Add parent directory to path for imports 

26sys.path.insert(0, str(Path(__file__).parent)) 

27 

28from dao.match_dao import MatchDAO, SupabaseConnection 

29from dao.match_event_dao import MatchEventDAO 

30 

31# Initialize Typer app and Rich console 

32app = typer.Typer(help="Live Match Management CLI Tool") 

33console = Console() 

34 

35# Shared connection holder (initialized lazily) 

36_connection: SupabaseConnection | None = None 

37 

38 

39def get_connection() -> SupabaseConnection: 

40 """Get or create shared Supabase connection.""" 

41 global _connection 

42 if _connection is None: 

43 _connection = SupabaseConnection() 

44 return _connection 

45 

46 

47def load_env(): 

48 """Load environment variables from .env file.""" 

49 env = os.getenv("APP_ENV", "local") 

50 backend_dir = Path(__file__).parent 

51 env_file = backend_dir / f".env.{env}" 

52 

53 if env_file.exists(): 

54 with open(env_file) as f: 

55 for line in f: 

56 if line.strip() and not line.startswith("#") and "=" in line: 

57 key, value = line.strip().split("=", 1) 

58 os.environ.setdefault(key, value) 

59 

60 

61# Load environment on module import 

62load_env() 

63 

64 

65@app.command() 

66def status(match_id: int = typer.Argument(..., help="Match ID to check")): 

67 """Show the current status of a live match.""" 

68 conn = get_connection() 

69 match_dao = MatchDAO(conn) 

70 event_dao = MatchEventDAO(conn) 

71 

72 # Get match details 

73 match = match_dao.get_match_by_id(match_id) 

74 if not match: 

75 console.print(f"[red]❌ Match {match_id} not found[/red]") 

76 raise typer.Exit(code=1) 

77 

78 # Get event count 

79 event_count = event_dao.get_events_count(match_id) 

80 

81 # Create status table 

82 table = Table(title=f"Live Match #{match_id}", box=box.ROUNDED) 

83 table.add_column("Field", style="cyan") 

84 table.add_column("Value", style="white") 

85 

86 # Match info 

87 table.add_row("Status", match.get("match_status", "N/A")) 

88 table.add_row("Home Team", f"{match.get('home_team_name', 'N/A')} (ID: {match.get('home_team_id')})") 

89 table.add_row("Away Team", f"{match.get('away_team_name', 'N/A')} (ID: {match.get('away_team_id')})") 

90 table.add_row("Score", f"{match.get('home_score', 0)} - {match.get('away_score', 0)}") 

91 table.add_row("Half Duration", f"{match.get('half_duration', 45)} minutes") 

92 table.add_row("Match Date", str(match.get("match_date", "N/A"))) 

93 

94 # Clock timestamps 

95 table.add_row("", "") # Spacer 

96 table.add_row("[bold]Clock Timestamps[/bold]", "") 

97 table.add_row("Kickoff Time", str(match.get("kickoff_time") or "Not started")) 

98 table.add_row("Halftime Start", str(match.get("halftime_start") or "-")) 

99 table.add_row("Second Half Start", str(match.get("second_half_start") or "-")) 

100 table.add_row("Match End Time", str(match.get("match_end_time") or "-")) 

101 

102 # Events 

103 table.add_row("", "") # Spacer 

104 table.add_row("[bold]Events[/bold]", "") 

105 table.add_row("Total Events", str(event_count)) 

106 

107 console.print(table) 

108 

109 

110@app.command("list") 

111def list_live( 

112 all_: bool = typer.Option(False, "--all", "-a", help="Show matches with live data OR completed status"), 

113 status: str = typer.Option(None, "--status", "-s", help="Filter by status (live, completed, scheduled)"), 

114): 

115 """List matches by status. Default shows only live matches.""" 

116 conn = get_connection() 

117 match_dao = MatchDAO(conn) 

118 

119 if all_ or status: 

120 # Build query for matches 

121 query = conn.client.table("matches").select(""" 

122 id, 

123 match_status, 

124 match_date, 

125 home_score, 

126 away_score, 

127 kickoff_time, 

128 home_team:teams!matches_home_team_id_fkey(id, name), 

129 away_team:teams!matches_away_team_id_fkey(id, name) 

130 """) 

131 

132 if status: 

133 # Filter by specific status 

134 query = query.eq("match_status", status) 

135 title = f"Matches with status '{status}'" 

136 else: 

137 # Show live or completed or those with kickoff_time 

138 query = query.or_("match_status.eq.live,match_status.eq.completed,kickoff_time.not.is.null") 

139 title = "Live & Completed Matches (recent 20)" 

140 

141 response = query.order("id", desc=True).limit(20).execute() 

142 

143 # Flatten the response 

144 matches = [] 

145 for match in response.data or []: 

146 matches.append( 

147 { 

148 "match_id": match["id"], 

149 "match_status": match["match_status"], 

150 "match_date": match["match_date"], 

151 "home_score": match["home_score"], 

152 "away_score": match["away_score"], 

153 "kickoff_time": match["kickoff_time"], 

154 "home_team_name": match["home_team"]["name"] if match.get("home_team") else "N/A", 

155 "away_team_name": match["away_team"]["name"] if match.get("away_team") else "N/A", 

156 } 

157 ) 

158 else: 

159 # Get only currently live matches 

160 matches = match_dao.get_live_matches() 

161 title = "Live Matches" 

162 

163 if not matches: 

164 console.print("[yellow]No matches found[/yellow]") 

165 return 

166 

167 table = Table(title=title, box=box.ROUNDED) 

168 table.add_column("ID", style="cyan") 

169 table.add_column("Home", style="white") 

170 table.add_column("Away", style="white") 

171 table.add_column("Score", style="green") 

172 table.add_column("Status", style="yellow") 

173 table.add_column("Kickoff", style="white") 

174 

175 for match in matches: 

176 score = f"{match.get('home_score', 0)} - {match.get('away_score', 0)}" 

177 kickoff = match.get("kickoff_time") or "Not started" 

178 if kickoff and kickoff != "Not started": 

179 # Format timestamp for display 

180 kickoff = str(kickoff)[:19] # Truncate to seconds 

181 

182 # get_live_matches returns match_id instead of id 

183 match_id = match.get("match_id") or match.get("id") 

184 table.add_row( 

185 str(match_id), 

186 match.get("home_team_name", "N/A"), 

187 match.get("away_team_name", "N/A"), 

188 score, 

189 match.get("match_status", "N/A"), 

190 str(kickoff), 

191 ) 

192 

193 console.print(table) 

194 

195 

196@app.command("clear-events") 

197def clear_events( 

198 match_id: int = typer.Argument(..., help="Match ID to clear events for"), 

199 force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"), 

200): 

201 """Delete all events for a match (goals, messages, status changes).""" 

202 conn = get_connection() 

203 match_dao = MatchDAO(conn) 

204 event_dao = MatchEventDAO(conn) 

205 

206 # Verify match exists 

207 match = match_dao.get_match_by_id(match_id) 

208 if not match: 

209 console.print(f"[red]❌ Match {match_id} not found[/red]") 

210 raise typer.Exit(code=1) 

211 

212 # Get event count 

213 event_count = event_dao.get_events_count(match_id) 

214 

215 if event_count == 0: 

216 console.print(f"[yellow]No events found for match {match_id}[/yellow]") 

217 return 

218 

219 console.print(f"[cyan]Match:[/cyan] {match.get('home_team_name')} vs {match.get('away_team_name')}") 

220 console.print(f"[cyan]Events to delete:[/cyan] {event_count}") 

221 

222 if not force and not Confirm.ask("Are you sure you want to delete all events?"): 

223 console.print("[yellow]Cancelled[/yellow]") 

224 return 

225 

226 # Delete all events for this match (hard delete for testing) 

227 try: 

228 response = event_dao.client.table("match_events").delete().eq("match_id", match_id).execute() 

229 deleted = len(response.data) if response.data else 0 

230 console.print(f"[green]✓ Deleted {deleted} events[/green]") 

231 except Exception as e: 

232 console.print(f"[red]❌ Error deleting events: {e}[/red]") 

233 raise typer.Exit(code=1) from e 

234 

235 

236@app.command() 

237def reset( 

238 match_id: int = typer.Argument(..., help="Match ID to reset"), 

239 force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"), 

240): 

241 """Reset a match to pre-live state (clears events and resets clock/score).""" 

242 conn = get_connection() 

243 match_dao = MatchDAO(conn) 

244 event_dao = MatchEventDAO(conn) 

245 

246 # Verify match exists 

247 match = match_dao.get_match_by_id(match_id) 

248 if not match: 

249 console.print(f"[red]❌ Match {match_id} not found[/red]") 

250 raise typer.Exit(code=1) 

251 

252 event_count = event_dao.get_events_count(match_id) 

253 

254 console.print(f"[cyan]Match:[/cyan] {match.get('home_team_name')} vs {match.get('away_team_name')}") 

255 console.print(f"[cyan]Current status:[/cyan] {match.get('match_status')}") 

256 console.print(f"[cyan]Current score:[/cyan] {match.get('home_score', 0)} - {match.get('away_score', 0)}") 

257 console.print(f"[cyan]Events to delete:[/cyan] {event_count}") 

258 console.print() 

259 console.print("[yellow]This will:[/yellow]") 

260 console.print(" - Delete all match events (goals, messages)") 

261 console.print(" - Reset score to 0-0") 

262 console.print(" - Clear all clock timestamps") 

263 console.print(" - Set match status to 'scheduled'") 

264 

265 if not force and not Confirm.ask("Are you sure you want to reset this match?"): 

266 console.print("[yellow]Cancelled[/yellow]") 

267 return 

268 

269 # Delete all events 

270 try: 

271 if event_count > 0: 

272 response = event_dao.client.table("match_events").delete().eq("match_id", match_id).execute() 

273 deleted = len(response.data) if response.data else 0 

274 console.print(f"[green]✓ Deleted {deleted} events[/green]") 

275 except Exception as e: 

276 console.print(f"[red]❌ Error deleting events: {e}[/red]") 

277 raise typer.Exit(code=1) from e 

278 

279 # Reset match fields 

280 try: 

281 response = ( 

282 match_dao.client.table("matches") 

283 .update( 

284 { 

285 "match_status": "scheduled", 

286 "home_score": None, 

287 "away_score": None, 

288 "kickoff_time": None, 

289 "halftime_start": None, 

290 "second_half_start": None, 

291 "match_end_time": None, 

292 "half_duration": 45, # Reset to default 

293 } 

294 ) 

295 .eq("id", match_id) 

296 .execute() 

297 ) 

298 

299 if response.data: 

300 console.print("[green]✓ Reset match clock and score[/green]") 

301 console.print(f"[green]✓ Match {match_id} fully reset[/green]") 

302 else: 

303 console.print("[red]❌ Failed to update match[/red]") 

304 raise typer.Exit(code=1) 

305 

306 except Exception as e: 

307 console.print(f"[red]❌ Error resetting match: {e}[/red]") 

308 raise typer.Exit(code=1) from e 

309 

310 

311@app.command("set-live") 

312def set_live( 

313 match_id: int = typer.Argument(..., help="Match ID to set as live"), 

314): 

315 """Set a match status to 'live' without starting the clock.""" 

316 conn = get_connection() 

317 match_dao = MatchDAO(conn) 

318 

319 # Verify match exists 

320 match = match_dao.get_match_by_id(match_id) 

321 if not match: 

322 console.print(f"[red]❌ Match {match_id} not found[/red]") 

323 raise typer.Exit(code=1) 

324 

325 if match.get("match_status") == "live": 

326 console.print(f"[yellow]Match {match_id} is already live[/yellow]") 

327 return 

328 

329 try: 

330 response = match_dao.client.table("matches").update({"match_status": "live"}).eq("id", match_id).execute() 

331 

332 if response.data: 

333 console.print(f"[green]✓ Match {match_id} set to live[/green]") 

334 else: 

335 console.print("[red]❌ Failed to update match[/red]") 

336 raise typer.Exit(code=1) 

337 

338 except Exception as e: 

339 console.print(f"[red]❌ Error: {e}[/red]") 

340 raise typer.Exit(code=1) from e 

341 

342 

343@app.command("find-with-events") 

344def find_with_events(): 

345 """Find all matches that have events (for cleanup).""" 

346 conn = get_connection() 

347 

348 # Direct query to find matches with events 

349 try: 

350 # Get distinct match_ids from match_events 

351 events_response = conn.client.table("match_events").select("match_id").execute() 

352 

353 if not events_response.data: 

354 console.print("[yellow]No matches with events found[/yellow]") 

355 return 

356 

357 # Get unique match IDs and count events 

358 from collections import Counter 

359 

360 match_counts = Counter(e["match_id"] for e in events_response.data) 

361 

362 if not match_counts: 

363 console.print("[yellow]No matches with events found[/yellow]") 

364 return 

365 

366 # Get match details for these IDs 

367 match_ids = list(match_counts.keys()) 

368 matches_response = ( 

369 conn.client.table("matches") 

370 .select(""" 

371 id, 

372 match_status, 

373 match_date, 

374 home_team:teams!matches_home_team_id_fkey(name), 

375 away_team:teams!matches_away_team_id_fkey(name) 

376 """) 

377 .in_("id", match_ids) 

378 .execute() 

379 ) 

380 

381 table = Table(title="Matches With Events", box=box.ROUNDED) 

382 table.add_column("ID", style="cyan") 

383 table.add_column("Home", style="white") 

384 table.add_column("Away", style="white") 

385 table.add_column("Status", style="yellow") 

386 table.add_column("Events", style="green") 

387 

388 for match in matches_response.data or []: 

389 match_id = match["id"] 

390 table.add_row( 

391 str(match_id), 

392 match["home_team"]["name"] if match.get("home_team") else "N/A", 

393 match["away_team"]["name"] if match.get("away_team") else "N/A", 

394 match.get("match_status", "N/A"), 

395 str(match_counts.get(match_id, 0)), 

396 ) 

397 

398 console.print(table) 

399 console.print() 

400 console.print("[dim]To clear events: manage_live_match.py clear-events <ID>[/dim]") 

401 

402 except Exception as e: 

403 console.print(f"[red]❌ Error: {e}[/red]") 

404 raise typer.Exit(code=1) from e 

405 

406 

407if __name__ == "__main__": 

408 app()