Coverage for manage_live_match.py: 0.00%
209 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 13:38 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 13:38 +0000
1#!/usr/bin/env python3
2"""
3Live Match Management CLI Tool
5This tool helps manage live matches during development and testing.
6Works directly with the database via DAOs.
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"""
15import os
16import sys
17from pathlib import Path
19import typer
20from rich import box
21from rich.console import Console
22from rich.prompt import Confirm
23from rich.table import Table
25# Add parent directory to path for imports
26sys.path.insert(0, str(Path(__file__).parent))
28from dao.match_dao import MatchDAO, SupabaseConnection
29from dao.match_event_dao import MatchEventDAO
31# Initialize Typer app and Rich console
32app = typer.Typer(help="Live Match Management CLI Tool")
33console = Console()
35# Shared connection holder (initialized lazily)
36_connection: SupabaseConnection | None = None
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
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}"
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)
61# Load environment on module import
62load_env()
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)
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)
78 # Get event count
79 event_count = event_dao.get_events_count(match_id)
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")
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")))
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 "-"))
102 # Events
103 table.add_row("", "") # Spacer
104 table.add_row("[bold]Events[/bold]", "")
105 table.add_row("Total Events", str(event_count))
107 console.print(table)
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)
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 """)
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)"
141 response = query.order("id", desc=True).limit(20).execute()
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"
163 if not matches:
164 console.print("[yellow]No matches found[/yellow]")
165 return
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")
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
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 )
193 console.print(table)
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)
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)
212 # Get event count
213 event_count = event_dao.get_events_count(match_id)
215 if event_count == 0:
216 console.print(f"[yellow]No events found for match {match_id}[/yellow]")
217 return
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}")
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
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
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)
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)
252 event_count = event_dao.get_events_count(match_id)
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'")
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
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
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 )
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)
306 except Exception as e:
307 console.print(f"[red]❌ Error resetting match: {e}[/red]")
308 raise typer.Exit(code=1) from e
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)
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)
325 if match.get("match_status") == "live":
326 console.print(f"[yellow]Match {match_id} is already live[/yellow]")
327 return
329 try:
330 response = match_dao.client.table("matches").update({"match_status": "live"}).eq("id", match_id).execute()
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)
338 except Exception as e:
339 console.print(f"[red]❌ Error: {e}[/red]")
340 raise typer.Exit(code=1) from e
343@app.command("find-with-events")
344def find_with_events():
345 """Find all matches that have events (for cleanup)."""
346 conn = get_connection()
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()
353 if not events_response.data:
354 console.print("[yellow]No matches with events found[/yellow]")
355 return
357 # Get unique match IDs and count events
358 from collections import Counter
360 match_counts = Counter(e["match_id"] for e in events_response.data)
362 if not match_counts:
363 console.print("[yellow]No matches with events found[/yellow]")
364 return
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 )
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")
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 )
398 console.print(table)
399 console.print()
400 console.print("[dim]To clear events: manage_live_match.py clear-events <ID>[/dim]")
402 except Exception as e:
403 console.print(f"[red]❌ Error: {e}[/red]")
404 raise typer.Exit(code=1) from e
407if __name__ == "__main__":
408 app()