Coverage for manage_teams.py: 0.00%
169 statements
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 12:24 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2026-04-15 12:24 +0000
1#!/usr/bin/env python3
2"""
3Team Management CLI Tool
5This tool manages teams via CSV import/export using the backend API.
6Supports importing teams from CSV and exporting current teams to CSV.
8Usage:
9 python manage_teams.py export-teams <csv_file> # Export teams to CSV
10 python manage_teams.py import-teams <csv_file> # Import teams from CSV
11 python manage_teams.py list # List all teams
12"""
14import csv
15import os
16from pathlib import Path
17from typing import Any
19import requests
20import typer
21from rich import box
22from rich.console import Console
23from rich.progress import Progress, SpinnerColumn, TextColumn
24from rich.table import Table
26# Initialize Typer app and Rich console
27app = typer.Typer(help="Team Management CLI Tool")
28console = Console()
30# API Configuration
31API_URL = os.getenv("API_URL", "http://localhost:8000")
34# ============================================================================
35# Authentication & API Helper Functions
36# ============================================================================
39def get_auth_token() -> str:
40 """Get authentication token for API requests."""
41 # For local development, use admin credentials
42 env = os.getenv("APP_ENV", "local")
44 # Load environment file from backend directory
45 backend_dir = Path(__file__).parent
46 env_file = backend_dir / f".env.{env}"
48 if env_file.exists():
49 # Load environment variables from file
50 with open(env_file) as f:
51 for line in f:
52 if line.strip() and not line.startswith("#"):
53 key, value = line.strip().split("=", 1)
54 os.environ[key] = value
56 # Try to login as admin user
57 username = "tom"
58 password = os.getenv("TEST_USER_PASSWORD_TOM", "admin123")
60 response = requests.post(
61 f"{API_URL}/api/auth/login",
62 json={"username": username, "password": password},
63 headers={"Content-Type": "application/json"},
64 )
66 if response.status_code == 200:
67 data = response.json()
68 return data["access_token"]
69 else:
70 console.print(f"[red]❌ Authentication failed: {response.text}[/red]")
71 raise typer.Exit(code=1)
74def api_request(method: str, endpoint: str, token: str, data: dict[str, Any] | None = None) -> requests.Response:
75 """Make an authenticated API request."""
76 headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
78 url = f"{API_URL}{endpoint}"
80 if method.upper() == "GET":
81 response = requests.get(url, headers=headers)
82 elif method.upper() == "POST":
83 response = requests.post(url, headers=headers, json=data)
84 elif method.upper() == "PUT":
85 response = requests.put(url, headers=headers, json=data)
86 elif method.upper() == "DELETE":
87 response = requests.delete(url, headers=headers)
88 else:
89 raise ValueError(f"Unsupported HTTP method: {method}")
91 return response
94# ============================================================================
95# Data Fetching Functions
96# ============================================================================
99def get_all_teams(token: str) -> list[dict]:
100 """Fetch all teams from the API."""
101 response = api_request("GET", "/api/teams", token)
102 if response.status_code == 200:
103 return response.json()
104 else:
105 console.print(f"[red]❌ Failed to fetch teams: {response.text}[/red]")
106 return []
109def get_all_clubs(token: str) -> list[dict]:
110 """Fetch all clubs for lookup."""
111 response = api_request("GET", "/api/clubs", token)
112 if response.status_code == 200:
113 return response.json()
114 else:
115 console.print(f"[red]❌ Failed to fetch clubs: {response.text}[/red]")
116 return []
119def get_all_age_groups(token: str) -> list[dict]:
120 """Fetch all age groups for lookup."""
121 response = api_request("GET", "/api/age-groups", token)
122 if response.status_code == 200:
123 return response.json()
124 else:
125 console.print(f"[red]❌ Failed to fetch age groups: {response.text}[/red]")
126 return []
129def get_all_match_types(token: str) -> list[dict]:
130 """Fetch all match types for lookup."""
131 response = api_request("GET", "/api/match-types", token)
132 if response.status_code == 200:
133 return response.json()
134 else:
135 console.print(f"[red]❌ Failed to fetch match types: {response.text}[/red]")
136 return []
139# ============================================================================
140# Export Functions
141# ============================================================================
144def determine_team_type(club_name: str, academy_team: bool) -> str:
145 """Determine team type based on club affiliation and academy status."""
146 if not club_name:
147 return "Guest Team"
148 elif academy_team:
149 return "Tournament Team" # Academy teams are typically tournament-focused
150 else:
151 return "League Team"
154@app.command()
155def export_teams(
156 csv_file: str,
157 club: str | None = typer.Option(None, "--club", help="Filter by club name (case-insensitive)"),
158):
159 """
160 Export all teams to a CSV file.
162 The CSV will contain: name,city,club_name,team_type,age_groups,match_types,academy_team
164 Use --club to filter teams by club name (case-insensitive partial match).
165 """
166 console.print("[bold cyan]📤 Team Export Tool[/bold cyan]")
167 console.print(f"📁 Exporting to: {csv_file}")
168 if club:
169 console.print(f"🏟️ Filtering by club: {club}")
170 console.print("[bold cyan]📤 Team Export Tool[/bold cyan]")
171 console.print(f"📁 Exporting to: {csv_file}")
173 # Authenticate
174 with console.status("[bold yellow]🔐 Authenticating...", spinner="dots"):
175 token = get_auth_token()
176 console.print("✅ Authenticated as admin")
178 # Fetch all data
179 with console.status("[bold yellow]📡 Fetching teams and related data...", spinner="dots"):
180 teams = get_all_teams(token)
181 clubs = get_all_clubs(token)
182 age_groups = get_all_age_groups(token)
183 match_types = get_all_match_types(token)
185 console.print(
186 f"✅ Found {len(teams)} teams, {len(clubs)} clubs, {len(age_groups)} age groups, {len(match_types)} match types"
187 )
189 # Create lookup dictionaries
190 club_lookup = {club["id"]: club["name"] for club in clubs}
191 {ag["id"]: ag["name"] for ag in age_groups}
192 {mt["id"]: mt["name"] for mt in match_types}
194 # Filter teams by club if specified
195 if club:
196 original_count = len(teams)
197 teams = [team for team in teams if club_lookup.get(team.get("club_id"), "").lower().find(club.lower()) != -1]
198 console.print(f"✅ Filtered to {len(teams)} teams from clubs matching '{club}' (was {original_count})")
200 if not teams:
201 filter_msg = f" matching '{club}'" if club else ""
202 console.print(f"[yellow]⚠️ No teams found{filter_msg} to export[/yellow]")
203 return
205 # Prepare CSV data
206 csv_data = []
207 processed_teams = 0
209 with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
210 task = progress.add_task("[cyan]Processing teams...", total=len(teams))
212 for team in teams:
213 team["id"]
214 team_name = team["name"]
215 city = team.get("city", "")
216 club_id = team.get("club_id")
217 academy_team = team.get("academy_team", False)
219 # Get club name
220 club_name = club_lookup.get(club_id, "") if club_id else ""
222 # Get age group names from the team's age_groups array
223 age_groups_list = team.get("age_groups", [])
224 age_group_names = [ag["name"] for ag in age_groups_list]
225 age_groups_str = ",".join(sorted(age_group_names))
227 # Determine team type based on club and academy status
228 team_type = determine_team_type(club_name, academy_team)
230 # For now, default to "Friendly" for match types
231 # TODO: Fetch actual match type data from team_match_types table
232 match_types_str = "Friendly"
234 # Add to CSV data
235 csv_data.append(
236 {
237 "name": team_name,
238 "city": city,
239 "club_name": club_name,
240 "team_type": team_type,
241 "age_groups": age_groups_str,
242 "match_types": match_types_str,
243 "academy_team": str(academy_team).lower(),
244 }
245 )
247 processed_teams += 1
248 progress.update(
249 task,
250 advance=1,
251 description=f"[cyan]Processed {processed_teams}/{len(teams)} teams...",
252 )
254 # Write to CSV
255 try:
256 with open(csv_file, "w", newline="", encoding="utf-8") as f:
257 if csv_data:
258 fieldnames = [
259 "name",
260 "city",
261 "club_name",
262 "team_type",
263 "age_groups",
264 "match_types",
265 "academy_team",
266 ]
267 writer = csv.DictWriter(f, fieldnames=fieldnames)
268 writer.writeheader()
269 writer.writerows(csv_data)
271 console.print(f"✅ Successfully exported {len(csv_data)} teams to {csv_file}")
273 # Show sample of exported data
274 if csv_data:
275 console.print("\n[bold]Sample exported data:[/bold]")
276 table = Table(box=box.ROUNDED)
277 table.add_column("Name", style="cyan")
278 table.add_column("City", style="magenta")
279 table.add_column("Club", style="green")
280 table.add_column("Team Type", style="yellow")
281 table.add_column("Age Groups", style="blue")
282 table.add_column("Match Types", style="red")
284 for row in csv_data[:5]: # Show first 5 rows
285 table.add_row(
286 row["name"][:30] + "..." if len(row["name"]) > 30 else row["name"],
287 row["city"][:20] + "..." if len(row["city"]) > 20 else row["city"],
288 row["club_name"][:20] + "..." if len(row["club_name"]) > 20 else row["club_name"],
289 row["team_type"],
290 row["age_groups"],
291 row["match_types"],
292 )
293 console.print(table)
295 except Exception as e:
296 console.print(f"[red]❌ Failed to write CSV file: {e}[/red]")
297 raise typer.Exit(code=1) from e
300# ============================================================================
301# List Command
302# ============================================================================
305@app.command()
306def list():
307 """List all teams in a table format."""
308 console.print("[bold cyan]📋 Team List[/bold cyan]")
310 # Authenticate
311 with console.status("[bold yellow]🔐 Authenticating...", spinner="dots"):
312 token = get_auth_token()
313 console.print("✅ Authenticated as admin")
315 # Fetch data
316 with console.status("[bold yellow]📡 Fetching teams...", spinner="dots"):
317 teams = get_all_teams(token)
318 clubs = get_all_clubs(token)
320 console.print(f"✅ Found {len(teams)} teams")
322 if not teams:
323 console.print("[yellow]⚠️ No teams found[/yellow]")
324 return
326 # Create lookup
327 club_lookup = {club["id"]: club["name"] for club in clubs}
329 # Display table
330 table = Table(box=box.ROUNDED)
331 table.add_column("ID", style="dim", width=6)
332 table.add_column("Name", style="cyan", width=25)
333 table.add_column("City", style="magenta", width=20)
334 table.add_column("Club", style="green", width=20)
335 table.add_column("Academy", style="yellow", width=8)
337 for team in teams[:50]: # Limit to first 50 for readability
338 club_name = club_lookup.get(team.get("club_id"), "") if team.get("club_id") else ""
339 academy = "Yes" if team.get("academy_team") else "No"
341 table.add_row(
342 str(team["id"]),
343 team["name"][:24] + "..." if len(team["name"]) > 24 else team["name"],
344 team.get("city", "")[:19] + "..." if len(team.get("city", "")) > 19 else team.get("city", ""),
345 club_name[:19] + "..." if len(club_name) > 19 else club_name,
346 academy,
347 )
349 console.print(table)
351 if len(teams) > 50:
352 console.print(f"\n[dim]Showing first 50 of {len(teams)} teams[/dim]")
355if __name__ == "__main__":
356 app()