Coverage for search_matches.py: 0.00%

205 statements  

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

1#!/usr/bin/env python3 

2""" 

3Match Search Tool 

4 

5A focused CLI tool for searching matches with comprehensive filters. 

6 

7Features: 

8- Filter by Match Type (default: League) 

9- Filter by League 

10- Filter by Division/Conference 

11- Filter by Age Group 

12- Filter by Team (home or away) 

13- Filter by Season (default: 2025-2026) 

14- Beautiful rich table output 

15- Environment-aware (local/dev/prod) 

16 

17Usage: 

18 # Search with defaults (League matches, 2025-2026 season) 

19 python scripts/utilities/search_matches.py 

20 # Or from backend directory: 

21 uv run python scripts/utilities/search_matches.py 

22 

23 # Search by team 

24 python scripts/utilities/search_matches.py --team IFA 

25 

26 # Search by MLS ID (external match identifier) 

27 python scripts/utilities/search_matches.py --mls-id 100502 

28 

29 # Search by age group and division 

30 python scripts/utilities/search_matches.py --age-group U14 --division "Elite Division" 

31 

32 # Search by league 

33 python scripts/utilities/search_matches.py --league Academy 

34 

35 # Search specific match type 

36 python scripts/utilities/search_matches.py --match-type Friendly 

37 

38 # Search different season 

39 python scripts/utilities/search_matches.py --season 2024-2025 

40 

41 # Combine filters 

42 python scripts/utilities/search_matches.py --team IFA --age-group U14 --season 2025-2026 

43""" 

44 

45import os 

46import sys 

47from pathlib import Path 

48 

49import typer 

50from rich.console import Console 

51from rich.panel import Panel 

52from rich.table import Table 

53 

54# Add backend root directory to path for imports (scripts/utilities/ -> backend/) 

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

56 

57# Import DAO layer 

58from dao.match_dao import MatchDAO, SupabaseConnection 

59 

60app = typer.Typer(help="Match Search Tool - Search matches with comprehensive filters") 

61console = Console() 

62 

63# Defaults 

64DEFAULT_MATCH_TYPE = "League" 

65DEFAULT_SEASON = "2025-2026" 

66 

67 

68def load_environment(): 

69 """Load environment variables from .env file based on APP_ENV""" 

70 app_env = os.getenv("APP_ENV", "local") 

71 console.print(f"[green]✓[/green] Environment: [cyan]{app_env}[/cyan]") 

72 return app_env 

73 

74 

75def get_dao() -> MatchDAO: 

76 """Get DAO instance using proper data access layer""" 

77 try: 

78 connection = SupabaseConnection() 

79 dao = MatchDAO(connection) 

80 return dao 

81 except Exception as e: 

82 console.print(f"[red]Error connecting to database: {e}[/red]") 

83 raise typer.Exit(1) from e 

84 

85 

86def get_reference_data(dao: MatchDAO): 

87 """Get reference data for validation and filtering using DAO""" 

88 try: 

89 match_types = {mt["name"]: mt["id"] for mt in dao.get_all_match_types()} 

90 seasons = {s["name"]: s["id"] for s in dao.get_all_seasons()} 

91 age_groups = {ag["name"]: ag["id"] for ag in dao.get_all_age_groups()} 

92 divisions = {d["name"]: d["id"] for d in dao.get_all_divisions()} 

93 leagues = {l["name"]: l["id"] for l in dao.get_all_leagues()} 

94 teams = {t["name"]: t["id"] for t in dao.get_all_teams()} 

95 

96 return { 

97 "match_types": match_types, 

98 "seasons": seasons, 

99 "age_groups": age_groups, 

100 "divisions": divisions, 

101 "leagues": leagues, 

102 "teams": teams, 

103 } 

104 except Exception as e: 

105 console.print(f"[red]Error loading reference data: {e}[/red]") 

106 raise typer.Exit(1) from e 

107 

108 

109@app.command() 

110def search( 

111 match_type: str | None = typer.Option( 

112 DEFAULT_MATCH_TYPE, "--match-type", "-m", help=f"Match type (default: {DEFAULT_MATCH_TYPE})" 

113 ), 

114 season: str | None = typer.Option(DEFAULT_SEASON, "--season", "-s", help=f"Season (default: {DEFAULT_SEASON})"), 

115 league: str | None = typer.Option(None, "--league", "-l", help="Filter by league"), 

116 division: str | None = typer.Option(None, "--division", "-d", help="Filter by division/conference"), 

117 age_group: str | None = typer.Option(None, "--age-group", "-a", help="Filter by age group (e.g., U14, U15)"), 

118 team: str | None = typer.Option(None, "--team", "-t", help="Filter by team name (searches both home and away)"), 

119 mls_id: str | None = typer.Option( 

120 None, "--mls-id", help="Filter by MLS match ID (external identifier from mlssoccer.com)" 

121 ), 

122 limit: int = typer.Option(100, "--limit", help="Maximum number of matches to show"), 

123 verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information"), 

124 show_filters: bool = typer.Option(True, "--show-filters/--no-filters", help="Show applied filters"), 

125): 

126 """ 

127 Search for matches with comprehensive filters. 

128 

129 Defaults: 

130 - Match Type: League 

131 - Season: 2025-2026 

132 

133 Examples: 

134 # Default search (League matches, 2025-2026) 

135 python scripts/utilities/search_matches.py 

136 

137 # Search by team 

138 python scripts/utilities/search_matches.py --team IFA 

139 

140 # Search by MLS ID (external match identifier) 

141 python scripts/utilities/search_matches.py --mls-id 100502 

142 

143 # Search by age group and division 

144 python scripts/utilities/search_matches.py --age-group U14 --division "Elite Division" 

145 

146 # Search different season 

147 python scripts/utilities/search_matches.py --season 2024-2025 --match-type Tournament 

148 """ 

149 env = load_environment() 

150 dao = get_dao() 

151 

152 # Load reference data 

153 ref_data = get_reference_data(dao) 

154 

155 # Validate filters 

156 if match_type and match_type not in ref_data["match_types"]: 

157 console.print(f"[red]Error: Match type '{match_type}' not found.[/red]") 

158 console.print(f"[yellow]Available match types: {', '.join(ref_data['match_types'].keys())}[/yellow]") 

159 raise typer.Exit(1) 

160 

161 if season and season not in ref_data["seasons"]: 

162 console.print(f"[red]Error: Season '{season}' not found.[/red]") 

163 console.print(f"[yellow]Available seasons: {', '.join(ref_data['seasons'].keys())}[/yellow]") 

164 raise typer.Exit(1) 

165 

166 if age_group and age_group not in ref_data["age_groups"]: 

167 console.print(f"[red]Error: Age group '{age_group}' not found.[/red]") 

168 console.print(f"[yellow]Available age groups: {', '.join(ref_data['age_groups'].keys())}[/yellow]") 

169 raise typer.Exit(1) 

170 

171 if division and division not in ref_data["divisions"]: 

172 console.print(f"[red]Error: Division '{division}' not found.[/red]") 

173 console.print(f"[yellow]Available divisions: {', '.join(ref_data['divisions'].keys())}[/yellow]") 

174 raise typer.Exit(1) 

175 

176 # Look up team ID if team name is provided (exact match) 

177 team_id = None 

178 if team: 

179 team_obj = dao.get_team_by_name(team) 

180 if not team_obj: 

181 console.print(f"[red]Error: Team '{team}' not found.[/red]") 

182 console.print("[yellow]Tip: Use 'list-teams' command to see available teams[/yellow]") 

183 raise typer.Exit(1) 

184 team_id = team_obj["id"] 

185 

186 # Get matches using DAO layer - all filtering done in database 

187 matches_data = dao.get_all_matches( 

188 season_id=ref_data["seasons"].get(season) if season else None, 

189 age_group_id=ref_data["age_groups"].get(age_group) if age_group else None, 

190 division_id=ref_data["divisions"].get(division) if division else None, 

191 team_id=team_id, # Exact team ID lookup 

192 match_type=match_type, # DAO handles match_type by name 

193 ) 

194 

195 # Apply client-side filters (league and mls_id not in DAO) 

196 original_count = len(matches_data) 

197 

198 # Filter by MLS ID if provided 

199 if mls_id: 

200 matches_data = [m for m in matches_data if m.get("match_id") == mls_id] 

201 

202 if league: 

203 if league not in ref_data["leagues"]: 

204 console.print(f"[red]Error: League '{league}' not found.[/red]") 

205 console.print(f"[yellow]Available leagues: {', '.join(ref_data['leagues'].keys())}[/yellow]") 

206 raise typer.Exit(1) 

207 

208 def get_match_league(match): 

209 """Extract the league name from the division""" 

210 division = match.get("division") 

211 if division and isinstance(division, dict): 

212 leagues_data = division.get("leagues!divisions_league_id_fkey") or division.get("leagues") 

213 if isinstance(leagues_data, dict): 

214 return leagues_data.get("name") 

215 return None 

216 

217 matches_data = [m for m in matches_data if get_match_league(m) == league] 

218 

219 # Apply limit AFTER filtering 

220 matches_data = matches_data[:limit] 

221 

222 # Display results 

223 if show_filters: 

224 filters_applied = [] 

225 if match_type: 

226 filters_applied.append(f"Match Type: [cyan]{match_type}[/cyan]") 

227 if season: 

228 filters_applied.append(f"Season: [cyan]{season}[/cyan]") 

229 if league: 

230 filters_applied.append(f"League: [cyan]{league}[/cyan]") 

231 if division: 

232 filters_applied.append(f"Division: [cyan]{division}[/cyan]") 

233 if age_group: 

234 filters_applied.append(f"Age Group: [cyan]{age_group}[/cyan]") 

235 if team: 

236 filters_applied.append(f"Team: [cyan]{team}[/cyan]") 

237 if mls_id: 

238 filters_applied.append(f"MLS ID: [cyan]{mls_id}[/cyan]") 

239 

240 if filters_applied: 

241 console.print( 

242 Panel( 

243 "\n".join(filters_applied), 

244 title="[bold]Applied Filters[/bold]", 

245 border_style="blue", 

246 ) 

247 ) 

248 console.print() 

249 

250 # Create results table 

251 table = Table(title=f"Matches ({len(matches_data)} found)", show_header=True, header_style="bold magenta") 

252 

253 table.add_column("ID", style="cyan", width=5) 

254 table.add_column("MLS ID", style="dim", width=12) 

255 table.add_column("Date", style="yellow", width=10) 

256 table.add_column("Home Team", style="green", width=20) 

257 table.add_column("H.ID", style="dim", width=5) 

258 table.add_column("Score", style="white", width=7, justify="center") 

259 table.add_column("Away Team", style="blue", width=20) 

260 table.add_column("A.ID", style="dim", width=5) 

261 table.add_column("League", style="magenta", width=10) 

262 if verbose: 

263 table.add_column("Age Group", style="dim", width=8) 

264 table.add_column("Division", style="dim", width=15) 

265 table.add_column("Status", style="dim", width=10) 

266 

267 for match in matches_data: 

268 # DAO returns flattened structure 

269 home_team = match.get("home_team_name", "Unknown") 

270 away_team = match.get("away_team_name", "Unknown") 

271 home_team_id = match.get("home_team_id", "-") 

272 away_team_id = match.get("away_team_id", "-") 

273 home_score = match.get("home_score") 

274 away_score = match.get("away_score") 

275 mls_match_id = match.get("match_id", "-") or "-" # External match ID from MLSNext 

276 

277 # Extract league name from division 

278 league_name = "-" 

279 division = match.get("division") 

280 if division and isinstance(division, dict): 

281 leagues_data = division.get("leagues!divisions_league_id_fkey") or division.get("leagues") 

282 if isinstance(leagues_data, dict): 

283 league_name = leagues_data.get("name", "-") 

284 

285 # Format score 

286 score = f"{home_score}-{away_score}" if home_score is not None and away_score is not None else "TBD" 

287 

288 row = [ 

289 str(match["id"]), # Primary key 

290 mls_match_id, # External match ID from MLSNext website 

291 match["match_date"], 

292 home_team, 

293 str(home_team_id), 

294 score, 

295 away_team, 

296 str(away_team_id), 

297 league_name, 

298 ] 

299 

300 if verbose: 

301 row.append(match.get("age_group_name", "-")) 

302 row.append(match.get("division_name", "-") or "-") 

303 row.append(match.get("match_status", "-")) 

304 

305 table.add_row(*row) 

306 

307 console.print(table) 

308 

309 # Summary 

310 if original_count != len(matches_data): 

311 console.print(f"\n[dim]Filtered from {original_count} to {len(matches_data)} matches[/dim]") 

312 

313 console.print(f"\n[green]Environment:[/green] {env}") 

314 

315 

316@app.command() 

317def list_options(): 

318 """List all available filter options (match types, seasons, leagues, etc.)""" 

319 load_environment() 

320 dao = get_dao() 

321 ref_data = get_reference_data(dao) 

322 

323 console.print(Panel("[bold]Available Filter Options[/bold]", border_style="green")) 

324 console.print() 

325 

326 # Match Types 

327 console.print("[bold cyan]Match Types:[/bold cyan]") 

328 for name in sorted(ref_data["match_types"].keys()): 

329 default = " [yellow](default)[/yellow]" if name == DEFAULT_MATCH_TYPE else "" 

330 console.print(f"{name}{default}") 

331 console.print() 

332 

333 # Seasons 

334 console.print("[bold cyan]Seasons:[/bold cyan]") 

335 for name in sorted(ref_data["seasons"].keys(), reverse=True): 

336 default = " [yellow](default)[/yellow]" if name == DEFAULT_SEASON else "" 

337 console.print(f"{name}{default}") 

338 console.print() 

339 

340 # Leagues 

341 console.print("[bold cyan]Leagues:[/bold cyan]") 

342 for name in sorted(ref_data["leagues"].keys()): 

343 console.print(f"{name}") 

344 console.print() 

345 

346 # Age Groups 

347 console.print("[bold cyan]Age Groups:[/bold cyan]") 

348 for name in sorted(ref_data["age_groups"].keys()): 

349 console.print(f"{name}") 

350 console.print() 

351 

352 # Divisions 

353 console.print("[bold cyan]Divisions:[/bold cyan]") 

354 for name in sorted(ref_data["divisions"].keys()): 

355 console.print(f"{name}") 

356 

357 

358@app.command() 

359def list_teams( 

360 league: str | None = typer.Option(None, "--league", "-l", help="Filter teams by league"), 

361 age_group: str | None = typer.Option(None, "--age-group", "-a", help="Filter teams by age group"), 

362 search: str | None = typer.Option(None, "--search", "-s", help="Search teams by name (partial match)"), 

363): 

364 """List all teams with optional filtering""" 

365 load_environment() 

366 dao = get_dao() 

367 ref_data = get_reference_data(dao) 

368 

369 # Validate filters 

370 if league and league not in ref_data["leagues"]: 

371 console.print(f"[red]Error: League '{league}' not found.[/red]") 

372 console.print(f"[yellow]Available leagues: {', '.join(ref_data['leagues'].keys())}[/yellow]") 

373 raise typer.Exit(1) 

374 

375 if age_group and age_group not in ref_data["age_groups"]: 

376 console.print(f"[red]Error: Age group '{age_group}' not found.[/red]") 

377 console.print(f"[yellow]Available age groups: {', '.join(ref_data['age_groups'].keys())}[/yellow]") 

378 raise typer.Exit(1) 

379 

380 # Get all teams 

381 all_teams = dao.get_all_teams() 

382 

383 # Apply filters 

384 teams = all_teams 

385 

386 # Filter by search term (partial match) 

387 if search: 

388 teams = [t for t in teams if search.lower() in t["name"].lower()] 

389 

390 # Filter by league (check team's league_id) 

391 if league: 

392 league_id = ref_data["leagues"][league] 

393 teams = [t for t in teams if t.get("league_id") == league_id] 

394 

395 # Note: Can't easily filter by age_group since teams can play in multiple age groups via team_mappings 

396 # For now, skip age_group filtering 

397 

398 # Display results 

399 table = Table(title=f"Teams ({len(teams)} found)", show_header=True, header_style="bold magenta") 

400 

401 table.add_column("Name", style="green", width=40) 

402 table.add_column("City", style="yellow", width=20) 

403 table.add_column("League", style="cyan", width=15) 

404 

405 for team in sorted(teams, key=lambda t: t["name"]): 

406 league_name = "-" 

407 if team.get("league_id"): 

408 # Find league name from ref_data 

409 for l_name, l_id in ref_data["leagues"].items(): 

410 if l_id == team["league_id"]: 

411 league_name = l_name 

412 break 

413 

414 table.add_row(team["name"], team.get("city", "-") or "-", league_name) 

415 

416 console.print(table) 

417 

418 if search or league: 

419 console.print(f"\n[dim]Showing {len(teams)} of {len(all_teams)} total teams[/dim]") 

420 

421 

422if __name__ == "__main__": 

423 app()