diff --git a/API_GUIDE.md b/API_GUIDE.md new file mode 100644 index 0000000..8ad3823 --- /dev/null +++ b/API_GUIDE.md @@ -0,0 +1,265 @@ +# REST API Guide - Project Kempe + +This guide explains how to set up and use the REST API to retrieve NHL player statistics for entire teams. + +## Setup Instructions + +### 1. Install Dependencies + +```bash +cd /Users/michaelsimard/dev/services/project-kempe-backend + +# Install all required packages +pip3 install -r requirements.txt +``` + +###2. Start the FastAPI Server + +```bash +# Option 1: Using uvicorn directly +uvicorn src.presentation.api.main:app --reload --host 0.0.0.0 --port 8000 + +# Option 2: Using python module +python3 -m uvicorn src.presentation.api.main:app --reload --host 0.0.0.0 --port 8000 +``` + +The server will start on `http://localhost:8000` + +### 3. Verify Server is Running + +Open your browser and visit: +- **API Documentation**: http://localhost:8000/docs (Interactive Swagger UI) +- **Alternative Docs**: http://localhost:8000/redoc +- **Health Check**: http://localhost:8000/health + +## Available Endpoints + +### Get All Players with Stats for a Team + +**Endpoint**: `GET /api/v1/teams/{team_id}/players` + +**Example**: +```bash +curl http://localhost:8000/api/v1/teams/TOR/players +``` + +**Response**: JSON array of all players (forwards, defensemen, goalies) with their current season statistics. + +**Response Structure**: +```json +[ + { + "player": { + "id": "8478104", + "first_name": "Sammy", + "last_name": "Blais", + "full_name": "Sammy Blais", + "jersey_number": 79, + "position": "F", + "team_id": "TOR" + }, + "stats": { + "player_id": "8478104", + "games_played": 8, + "goals": 1, + "assists": 2, + "points": 3, + "plus_minus": -2, + "penalty_minutes": 4, + "shots": 12, + "shooting_percentage": 8.33, + "time_on_ice_per_game": 14.5, + "powerplay_goals": 0, + "powerplay_points": 0, + "shorthanded_goals": 0, + "game_winning_goals": 0, + "faceoff_percentage": null, + "hits": 15, + "blocked_shots": 3 + }, + "stats_type": "skater" + } +] +``` + +### Get Only Skaters for a Team + +**Endpoint**: `GET /api/v1/teams/{team_id}/players/skaters` + +**Example**: +```bash +curl http://localhost:8000/api/v1/teams/TOR/players/skaters +``` + +**Response**: JSON array of only forwards and defensemen with their statistics. + +### Get Only Goalies for a Team + +**Endpoint**: `GET /api/v1/teams/{team_id}/players/goalies` + +**Example**: +```bash +curl http://localhost:8000/api/v1/teams/TOR/players/goalies +``` + +**Response**: JSON array of only goalies with their statistics. + +**Goalie Stats Structure**: +```json +{ + "player": { + "id": "8471679", + "first_name": "Joseph", + "last_name": "Woll", + "full_name": "Joseph Woll", + "jersey_number": 60, + "position": "G", + "team_id": "TOR" + }, + "stats": { + "player_id": "8471679", + "games_played": 10, + "games_started": 10, + "wins": 6, + "losses": 2, + "overtime_losses": 2, + "saves": 285, + "shots_against": 308, + "goals_against": 23, + "save_percentage": 92.53, + "goals_against_average": 2.30, + "shutouts": 1, + "time_on_ice": 600.5 + }, + "stats_type": "goalie" +} +``` + +## NHL Team Abbreviations + +Use these three-letter abbreviations when making API calls: + +**Atlantic Division**: +- TOR - Toronto Maple Leafs +- BOS - Boston Bruins +- FLA - Florida Panthers +- TBL - Tampa Bay Lightning +- BUF - Buffalo Sabres +- DET - Detroit Red Wings +- OTT - Ottawa Senators +- MTL - Montréal Canadiens + +**Metropolitan Division**: +- CAR - Carolina Hurricanes +- NYR - New York Rangers +- NJD - New Jersey Devils +- PHI - Philadelphia Flyers +- PIT - Pittsburgh Penguins +- WSH - Washington Capitals +- NYI - New York Islanders +- CBJ - Columbus Blue Jackets + +**Central Division**: +- DAL - Dallas Stars +- COL - Colorado Avalanche +- WPG - Winnipeg Jets +- MIN - Minnesota Wild +- NSH - Nashville Predators +- STL - St. Louis Blues +- CHI - Chicago Blackhawks +- UTA - Utah Mammoth + +**Pacific Division**: +- VGK - Vegas Golden Knights +- EDM - Edmonton Oilers +- LAK - Los Angeles Kings +- VAN - Vancouver Canucks +- SEA - Seattle Kraken +- CGY - Calgary Flames +- ANA - Anaheim Ducks +- SJS - San Jose Sharks + +## Using the API in Your Application + +### Python Example + +```python +import httpx +import asyncio + +async def get_team_stats(team_id: str): + async with httpx.AsyncClient() as client: + response = await client.get( + f"http://localhost:8000/api/v1/teams/{team_id}/players" + ) + return response.json() + +# Get Toronto Maple Leafs stats +stats = asyncio.run(get_team_stats("TOR")) +for player_data in stats: + player = player_data["player"] + print(f"{player['full_name']}: {player_data['stats_type']}") +``` + +### JavaScript Example + +```javascript +async function getTeamStats(teamId) { + const response = await fetch( + `http://localhost:8000/api/v1/teams/${teamId}/players` + ); + const data = await response.json(); + return data; +} + +// Get Toronto Maple Leafs stats +getTeamStats('TOR').then(players => { + players.forEach(playerData => { + console.log(`${playerData.player.full_name}: ${playerData.stats_type}`); + }); +}); +``` + +### cURL Example + +```bash +# Get all players for Toronto Maple Leafs +curl http://localhost:8000/api/v1/teams/TOR/players | jq '.' + +# Get only top scorers (skaters) +curl http://localhost:8000/api/v1/teams/TOR/players/skaters | jq '.[] | select(.stats.points > 10)' + +# Get goalie stats +curl http://localhost:8000/api/v1/teams/TOR/players/goalies | jq '.[] | .stats' +``` + +## Error Handling + +### Team Not Found (404) +```json +{ + "detail": "No players found for team XYZ. Please verify the team abbreviation." +} +``` + +### Server Error (500) +Check server logs for detailed error information. + +## Architecture + +This REST API follows CLEAN architecture principles: + +1. **Domain Layer**: Pure business entities (Player, Stats) +2. **Application Layer**: Use cases orchestrating business logic +3. **Infrastructure Layer**: NHL API adapter (swappable) +4. **Presentation Layer**: FastAPI REST endpoints + +You can swap the NHL data source by changing the dependency injection configuration in `src/presentation/api/dependencies.py` without modifying any business logic or API endpoints. + +## Next Steps + +- Add authentication for private endpoints +- Implement caching for frequently requested data +- Add filtering and sorting query parameters +- Create endpoints for player comparisons +- Integrate Yahoo Fantasy API data diff --git a/debug_api.py b/debug_api.py new file mode 100644 index 0000000..26072da --- /dev/null +++ b/debug_api.py @@ -0,0 +1,53 @@ +"""Debug script to examine raw NHL API responses.""" +import sys + +sys.path.insert(0, "/Users/michaelsimard/dev/services/project-kempe-backend") + +from nhlpy import NHLClient + +client = NHLClient() + +print("=" * 60) +print("RAW API RESPONSE DEBUGGING") +print("=" * 60) + +# Test teams endpoint +print("\n1. Testing teams endpoint...") +try: + teams_data = client.teams.teams() + print(f" Response type: {type(teams_data)}") + if isinstance(teams_data, dict): + print(f" Keys: {list(teams_data.keys())}") + if "data" in teams_data: + print(f" Number of teams in 'data': {len(teams_data['data'])}") + if teams_data["data"]: + print(f" First team keys: {list(teams_data['data'][0].keys())}") + print(f" First team sample: {teams_data['data'][0]}") + else: + print(f" Response: {teams_data}") +except Exception as e: + print(f" ERROR: {e}") + import traceback + traceback.print_exc() + +# Test player game log +print("\n2. Testing player game log (Sammy Blais ID: 8478104)...") +try: + game_log = client.stats.player_game_log( + player_id="8478104", season_id="20242025", game_type=2 + ) + print(f" Response type: {type(game_log)}") + if isinstance(game_log, dict): + print(f" Keys: {list(game_log.keys())}") + if "gameLog" in game_log: + print(f" Number of games: {len(game_log['gameLog'])}") + if game_log["gameLog"]: + print(f" First game keys: {list(game_log['gameLog'][0].keys())}") + else: + print(f" Response: {game_log}") +except Exception as e: + print(f" ERROR: {e}") + import traceback + traceback.print_exc() + +print("\n" + "=" * 60) diff --git a/src/application/dto/__init__.py b/src/application/dto/__init__.py new file mode 100644 index 0000000..4061fe2 --- /dev/null +++ b/src/application/dto/__init__.py @@ -0,0 +1,14 @@ +"""Data Transfer Objects package.""" +from .player_dto import ( + PlayerDTO, + SkaterStatsDTO, + GoalieStatsDTO, + PlayerWithStatsDTO, +) + +__all__ = [ + "PlayerDTO", + "SkaterStatsDTO", + "GoalieStatsDTO", + "PlayerWithStatsDTO", +] diff --git a/src/application/dto/player_dto.py b/src/application/dto/player_dto.py new file mode 100644 index 0000000..62e4ae4 --- /dev/null +++ b/src/application/dto/player_dto.py @@ -0,0 +1,63 @@ +"""Player Data Transfer Objects.""" +from typing import Optional +from pydantic import BaseModel + + +class PlayerDTO(BaseModel): + """Data transfer object for player information.""" + + id: str + first_name: str + last_name: str + full_name: str + jersey_number: Optional[int] + position: str + team_id: str + + +class SkaterStatsDTO(BaseModel): + """Data transfer object for skater statistics.""" + + player_id: str + games_played: int + goals: int + assists: int + points: int + plus_minus: int + penalty_minutes: int + shots: int + shooting_percentage: float + time_on_ice_per_game: Optional[float] + powerplay_goals: int + powerplay_points: int + shorthanded_goals: int + game_winning_goals: int + faceoff_percentage: Optional[float] + hits: int + blocked_shots: int + + +class GoalieStatsDTO(BaseModel): + """Data transfer object for goalie statistics.""" + + player_id: str + games_played: int + games_started: int + wins: int + losses: int + overtime_losses: int + saves: int + shots_against: int + goals_against: int + save_percentage: float + goals_against_average: float + shutouts: int + time_on_ice: Optional[float] + + +class PlayerWithStatsDTO(BaseModel): + """Combined player information and statistics.""" + + player: PlayerDTO + stats: Optional[SkaterStatsDTO | GoalieStatsDTO] + stats_type: str # "skater", "goalie", or "none" diff --git a/src/application/use_cases/__init__.py b/src/application/use_cases/__init__.py index db3d199..1f85ca1 100644 --- a/src/application/use_cases/__init__.py +++ b/src/application/use_cases/__init__.py @@ -1,4 +1,5 @@ """Application use cases package.""" from .get_player_stats import GetPlayerStatsUseCase +from .get_team_player_stats import GetTeamPlayerStatsUseCase -__all__ = ["GetPlayerStatsUseCase"] +__all__ = ["GetPlayerStatsUseCase", "GetTeamPlayerStatsUseCase"] diff --git a/src/application/use_cases/get_team_player_stats.py b/src/application/use_cases/get_team_player_stats.py new file mode 100644 index 0000000..8e5d78a --- /dev/null +++ b/src/application/use_cases/get_team_player_stats.py @@ -0,0 +1,127 @@ +"""Use case for retrieving all player statistics for a team.""" +from typing import List +import logging + +from src.domain.repositories import PlayerRepository +from src.application.dto import ( + PlayerDTO, + SkaterStatsDTO, + GoalieStatsDTO, + PlayerWithStatsDTO, +) + +logger = logging.getLogger(__name__) + + +class GetTeamPlayerStatsUseCase: + """ + Use case for retrieving all players and their statistics for a team. + + This use case demonstrates CLEAN architecture principles: + - Orchestrates multiple repository calls + - Transforms domain entities to DTOs for API response + - Contains business logic independent of frameworks + """ + + def __init__(self, player_repository: PlayerRepository): + """ + Initializes the use case with required dependencies. + + Args: + player_repository: Repository for accessing player data + """ + self.player_repository = player_repository + + async def execute(self, team_id: str) -> List[PlayerWithStatsDTO]: + """ + Retrieves all players and their statistics for a team. + + Args: + team_id: The team abbreviation or ID (e.g., "TOR") + + Returns: + List of players with their current season statistics + """ + # Get team roster + players = await self.player_repository.get_players_by_team(team_id) + + if not players: + logger.warning(f"No players found for team {team_id}") + return [] + + # Get statistics for each player + players_with_stats = [] + + for player in players: + # Convert player entity to DTO + player_dto = PlayerDTO( + id=player.id, + first_name=player.first_name, + last_name=player.last_name, + full_name=player.full_name, + jersey_number=player.jersey_number, + position=player.position, + team_id=player.team_id, + ) + + # Get appropriate statistics based on position + stats_dto = None + stats_type = "none" + + try: + if player.is_goalie(): + goalie_stats = await self.player_repository.get_goalie_stats( + player.id + ) + if goalie_stats: + stats_dto = GoalieStatsDTO( + player_id=goalie_stats.player_id, + games_played=goalie_stats.games_played, + games_started=goalie_stats.games_started, + wins=goalie_stats.wins, + losses=goalie_stats.losses, + overtime_losses=goalie_stats.overtime_losses, + saves=goalie_stats.saves, + shots_against=goalie_stats.shots_against, + goals_against=goalie_stats.goals_against, + save_percentage=goalie_stats.save_percentage, + goals_against_average=goalie_stats.goals_against_average, + shutouts=goalie_stats.shutouts, + time_on_ice=goalie_stats.time_on_ice, + ) + stats_type = "goalie" + else: + skater_stats = await self.player_repository.get_skater_stats( + player.id + ) + if skater_stats: + stats_dto = SkaterStatsDTO( + player_id=skater_stats.player_id, + games_played=skater_stats.games_played, + goals=skater_stats.goals, + assists=skater_stats.assists, + points=skater_stats.points, + plus_minus=skater_stats.plus_minus, + penalty_minutes=skater_stats.penalty_minutes, + shots=skater_stats.shots, + shooting_percentage=skater_stats.shooting_percentage, + time_on_ice_per_game=skater_stats.time_on_ice_per_game, + powerplay_goals=skater_stats.powerplay_goals, + powerplay_points=skater_stats.powerplay_points, + shorthanded_goals=skater_stats.shorthanded_goals, + game_winning_goals=skater_stats.game_winning_goals, + faceoff_percentage=skater_stats.faceoff_percentage, + hits=skater_stats.hits, + blocked_shots=skater_stats.blocked_shots, + ) + stats_type = "skater" + except Exception as e: + logger.error(f"Error fetching stats for player {player.id}: {e}") + + players_with_stats.append( + PlayerWithStatsDTO( + player=player_dto, stats=stats_dto, stats_type=stats_type + ) + ) + + return players_with_stats diff --git a/src/presentation/api/main.py b/src/presentation/api/main.py index e6589d8..5078532 100644 --- a/src/presentation/api/main.py +++ b/src/presentation/api/main.py @@ -38,8 +38,9 @@ async def health_check(): return {"status": "healthy"} -# TODO: Include routers for players, teams, and fantasy endpoints -# from src.presentation.api.routes import players, teams, fantasy -# app.include_router(players.router, prefix=f"{settings.api_prefix}/players", tags=["players"]) -# app.include_router(teams.router, prefix=f"{settings.api_prefix}/teams", tags=["teams"]) -# app.include_router(fantasy.router, prefix=f"{settings.api_prefix}/fantasy", tags=["fantasy"]) +# Include routers +from src.presentation.api.routes import teams + +app.include_router( + teams.router, prefix=f"{settings.api_prefix}/teams", tags=["teams"] +) diff --git a/src/presentation/api/routes/__init__.py b/src/presentation/api/routes/__init__.py new file mode 100644 index 0000000..d1e594b --- /dev/null +++ b/src/presentation/api/routes/__init__.py @@ -0,0 +1 @@ +"""API routes package.""" diff --git a/src/presentation/api/routes/teams.py b/src/presentation/api/routes/teams.py new file mode 100644 index 0000000..6316851 --- /dev/null +++ b/src/presentation/api/routes/teams.py @@ -0,0 +1,103 @@ +"""Team API routes.""" +from typing import List +from fastapi import APIRouter, Depends, HTTPException + +from src.domain.repositories import PlayerRepository +from src.application.use_cases import GetTeamPlayerStatsUseCase +from src.application.dto import PlayerWithStatsDTO +from src.presentation.api.dependencies import get_player_repository + +router = APIRouter() + + +@router.get("/{team_id}/players") +async def get_team_players_with_stats( + team_id: str, + player_repo: PlayerRepository = Depends(get_player_repository), +) -> List[PlayerWithStatsDTO]: + """ + Retrieves all players and their statistics for a specific team. + + Args: + team_id: Team abbreviation (e.g., "TOR", "BOS", "MTL") + player_repo: Injected player repository + + Returns: + List of players with their current season statistics + + Example: + GET /api/v1/teams/TOR/players + + Response includes: + - Player information (name, position, jersey number) + - Current season statistics (goals, assists, points for skaters) + - Current season statistics (wins, saves, GAA for goalies) + """ + use_case = GetTeamPlayerStatsUseCase(player_repo) + players_with_stats = await use_case.execute(team_id) + + if not players_with_stats: + raise HTTPException( + status_code=404, + detail=f"No players found for team {team_id}. Please verify the team abbreviation.", + ) + + return players_with_stats + + +@router.get("/{team_id}/players/skaters") +async def get_team_skaters_with_stats( + team_id: str, + player_repo: PlayerRepository = Depends(get_player_repository), +) -> List[PlayerWithStatsDTO]: + """ + Retrieves only skaters (forwards and defensemen) for a team. + + Args: + team_id: Team abbreviation + player_repo: Injected player repository + + Returns: + List of skaters with their statistics + """ + use_case = GetTeamPlayerStatsUseCase(player_repo) + all_players = await use_case.execute(team_id) + + # Filter to only skaters + skaters = [p for p in all_players if p.stats_type == "skater"] + + if not skaters: + raise HTTPException( + status_code=404, detail=f"No skaters found for team {team_id}" + ) + + return skaters + + +@router.get("/{team_id}/players/goalies") +async def get_team_goalies_with_stats( + team_id: str, + player_repo: PlayerRepository = Depends(get_player_repository), +) -> List[PlayerWithStatsDTO]: + """ + Retrieves only goalies for a team. + + Args: + team_id: Team abbreviation + player_repo: Injected player repository + + Returns: + List of goalies with their statistics + """ + use_case = GetTeamPlayerStatsUseCase(player_repo) + all_players = await use_case.execute(team_id) + + # Filter to only goalies + goalies = [p for p in all_players if p.stats_type == "goalie"] + + if not goalies: + raise HTTPException( + status_code=404, detail=f"No goalies found for team {team_id}" + ) + + return goalies diff --git a/test_nhl_api.py b/test_nhl_api.py new file mode 100644 index 0000000..dfb661c --- /dev/null +++ b/test_nhl_api.py @@ -0,0 +1,120 @@ +"""Manual test script for NHL API integration.""" +import asyncio +import sys +from datetime import datetime + +# Add src to path for imports +sys.path.insert(0, "/Users/michaelsimard/dev/services/project-kempe-backend") + +from src.infrastructure.adapters.nhl import NHLPlayerAdapter, NHLTeamAdapter + + +async def test_teams(): + """Test team retrieval.""" + print("\n" + "=" * 60) + print("TESTING TEAM ADAPTER") + print("=" * 60) + + team_adapter = NHLTeamAdapter() + + # Test getting all teams + print("\n1. Getting all NHL teams...") + teams = await team_adapter.get_all_teams() + print(f" Found {len(teams)} teams") + + if teams: + print("\n First 5 teams:") + for team in teams[:5]: + print(f" - {team.full_name} ({team.abbreviation}) - {team.division}") + + # Test getting a specific team Wings + print("\n2. Getting Detroit Red Wings by abbreviation (DET)...") + wings = await team_adapter.get_team_by_id("DET") + if wings: + print(f" ✓ Team: {wings.full_name}") + print(f" City: {wings.city}") + print(f" Division: {wings.division}") + print(f" Conference: {wings.conference}") + else: + print(" ✗ Team not found") + + # Test getting teams by division + print("\n3. Getting teams from Atlantic Division...") + atlantic_teams = await team_adapter.get_teams_by_division("Atlantic") + print(f" Found {len(atlantic_teams)} teams in Atlantic Division") + for team in atlantic_teams: + print(f" - {team.full_name}") + + +async def test_players(): + """Test player retrieval.""" + print("\n" + "=" * 60) + print("TESTING PLAYER ADAPTER") + print("=" * 60) + + player_adapter = NHLPlayerAdapter() + + # Test getting team roster (Toronto Maple Leafs) + print("\n1. Getting Detroit Red Wings roster...") + players = await player_adapter.get_players_by_team("DET") + print(f" Found {len(players)} players on roster") + + if players: + print("\n Sample players:") + for player in players[:5]: + print( + f" - #{player.jersey_number or '??'} {player.full_name} ({player.position})" + ) + + # Get a specific player for stats testing + test_player = players[0] + print(f"\n2. Getting stats for {test_player.full_name} (ID: {test_player.id})...") + + if test_player.is_goalie(): + stats = await player_adapter.get_goalie_stats(test_player.id) + if stats: + print(f" ✓ Goalie Stats:") + print(f" Games Played: {stats.games_played}") + print(f" Wins: {stats.wins}") + print(f" Save %: {stats.save_percentage:.1f}%") + print(f" GAA: {stats.goals_against_average:.2f}") + else: + print(" ✗ No stats found") + else: + stats = await player_adapter.get_skater_stats(test_player.id) + if stats: + print(f" ✓ Skater Stats:") + print(f" Games Played: {stats.games_played}") + print(f" Goals: {stats.goals}") + print(f" Assists: {stats.assists}") + print(f" Points: {stats.points}") + print(f" Plus/Minus: {stats.plus_minus:+d}") + else: + print(" ✗ No stats found") + + +async def main(): + """Run all tests.""" + print("\n" + "=" * 60) + print("NHL API INTEGRATION TEST") + print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print("=" * 60) + + try: + await test_teams() + await test_players() + + print("\n" + "=" * 60) + print("ALL TESTS COMPLETED") + print("=" * 60 + "\n") + + except Exception as e: + print(f"\n✗ ERROR: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/test_rest_api.py b/test_rest_api.py new file mode 100644 index 0000000..8add6a7 --- /dev/null +++ b/test_rest_api.py @@ -0,0 +1,176 @@ +"""Test script for REST API endpoints.""" +import sys +import httpx +import asyncio +from datetime import datetime + +# Test base URL +BASE_URL = "http://localhost:8000" + + +async def test_health_check(): + """Test health check endpoint.""" + print("\n" + "=" * 60) + print("TESTING HEALTH CHECK") + print("=" * 60) + + async with httpx.AsyncClient() as client: + response = await client.get(f"{BASE_URL}/health") + print(f"Status: {response.status_code}") + print(f"Response: {response.json()}") + + +async def test_root(): + """Test root endpoint.""" + print("\n" + "=" * 60) + print("TESTING ROOT ENDPOINT") + print("=" * 60) + + async with httpx.AsyncClient() as client: + response = await client.get(f"{BASE_URL}/") + print(f"Status: {response.status_code}") + data = response.json() + print(f"App Name: {data.get('name')}") + print(f"Version: {data.get('version')}") + print(f"Status: {data.get('status')}") + + +async def test_team_players(): + """Test getting team players with stats.""" + print("\n" + "=" * 60) + print("TESTING TEAM PLAYERS ENDPOINT") + print("=" * 60) + + team_id = "TOR" + print(f"\nFetching players for team: {team_id}") + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(f"{BASE_URL}/api/v1/teams/{team_id}/players") + + print(f"Status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f"Total players: {len(data)}") + + # Show first 3 players + print("\nFirst 3 players:") + for player_data in data[:3]: + player = player_data["player"] + stats = player_data["stats"] + stats_type = player_data["stats_type"] + + print(f"\n #{player.get('jersey_number', '??')} {player['full_name']}") + print(f" Position: {player['position']}") + + if stats_type == "skater" and stats: + print(f" GP: {stats['games_played']}, G: {stats['goals']}, A: {stats['assists']}, P: {stats['points']}") + elif stats_type == "goalie" and stats: + print(f" GP: {stats['games_played']}, W: {stats['wins']}, SV%: {stats['save_percentage']:.3f}") + else: + print(" No stats available") + else: + print(f"Error: {response.text}") + + +async def test_team_skaters(): + """Test getting team skaters only.""" + print("\n" + "=" * 60) + print("TESTING TEAM SKATERS ENDPOINT") + print("=" * 60) + + team_id = "TOR" + print(f"\nFetching skaters for team: {team_id}") + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(f"{BASE_URL}/api/v1/teams/{team_id}/players/skaters") + + print(f"Status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f"Total skaters: {len(data)}") + + # Show top 5 scorers + sorted_skaters = sorted( + data, + key=lambda x: x["stats"]["points"] if x["stats"] else 0, + reverse=True, + ) + + print("\nTop 5 scorers:") + for player_data in sorted_skaters[:5]: + player = player_data["player"] + stats = player_data["stats"] + + if stats: + print(f" {player['full_name']}: {stats['points']} pts ({stats['goals']}G, {stats['assists']}A)") + else: + print(f"Error: {response.text}") + + +async def test_team_goalies(): + """Test getting team goalies only.""" + print("\n" + "=" * 60) + print("TESTING TEAM GOALIES ENDPOINT") + print("=" * 60) + + team_id = "TOR" + print(f"\nFetching goalies for team: {team_id}") + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(f"{BASE_URL}/api/v1/teams/{team_id}/players/goalies") + + print(f"Status: {response.status_code}") + + if response.status_code == 200: + data = response.json() + print(f"Total goalies: {len(data)}") + + print("\nGoalie stats:") + for player_data in data: + player = player_data["player"] + stats = player_data["stats"] + + if stats: + print(f" {player['full_name']}:") + print(f" Record: {stats['wins']}-{stats['losses']}-{stats['overtime_losses']}") + print(f" SV%: {stats['save_percentage']:.3f}, GAA: {stats['goals_against_average']:.2f}") + else: + print(f"Error: {response.text}") + + +async def main(): + """Run all tests.""" + print("\n" + "=" * 60) + print("REST API INTEGRATION TEST") + print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"Base URL: {BASE_URL}") + print("=" * 60) + + try: + await test_health_check() + await test_root() + await test_team_players() + await test_skaters() + await test_team_goalies() + + print("\n" + "=" * 60) + print("ALL TESTS COMPLETED") + print("=" * 60 + "\n") + + except httpx.ConnectError: + print("\n✗ ERROR: Cannot connect to server.") + print("Make sure the FastAPI server is running:") + print(" uvicorn src.presentation.api.main:app --reload") + sys.exit(1) + except Exception as e: + print(f"\n✗ ERROR: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main())