commit 337a6377de002af57859643dbbd054c45feabbd3 Author: Michael Simard Date: Sun Nov 23 17:13:58 2025 -0600 Initial commit: CLEAN architecture foundation for fantasy hockey backend Implemented CLEAN architecture with clear separation of concerns: - Domain layer with entities (Player, Team, Stats, FantasyTeam) and repository interfaces - Application layer with use case implementations - Infrastructure layer with NHL API and Yahoo Fantasy API adapters - Presentation layer with FastAPI configuration and dependency injection Key features: - Swappable data source adapters (NHL API, Yahoo Fantasy API) - Repository pattern for data access abstraction - Dependency injection for loose coupling - FastAPI framework with async support - PostgreSQL database configuration - Environment-based configuration management Technology stack: Python 3.11+, FastAPI, PostgreSQL, nhl-api-py, yfpy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c3b58ed --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Application Configuration +DEBUG=false + +# Database +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/fantasy_hockey + +# Yahoo Fantasy API Credentials +YAHOO_CONSUMER_KEY=your_consumer_key_here +YAHOO_CONSUMER_SECRET=your_consumer_secret_here + +# API Configuration +API_PREFIX=/api/v1 +CORS_ORIGINS=["http://localhost:3000"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0ed134 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Environment variables +.env +.env.local + +# Database +*.db +*.sqlite3 + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + +# Logs +*.log +logs/ + +# OS +.DS_Store +Thumbs.db diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..a3a2882 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,94 @@ +# Architecture Documentation + +## CLEAN Architecture Overview + +This application implements CLEAN architecture principles to ensure maintainability, testability, and flexibility. + +## Layer Responsibilities + +### 1. Domain Layer (`src/domain/`) +**Purpose**: Contains enterprise business rules and core entities. + +- **Entities** (`entities/`): Core business objects that represent the problem domain + - `Player`: NHL player information + - `SkaterStats`, `GoalieStats`: Player statistics + - `NHLTeam`: NHL team information + - `FantasyTeam`: Yahoo Fantasy team data + +- **Repository Interfaces** (`repositories/`): Abstract contracts for data access + - `PlayerRepository`: Interface for player data operations + - `TeamRepository`: Interface for team data operations + - `FantasyRepository`: Interface for fantasy team operations + +**Dependencies**: None. This layer is completely independent. + +### 2. Application Layer (`src/application/`) +**Purpose**: Contains application-specific business rules. + +- **Use Cases** (`use_cases/`): Application business logic + - `GetPlayerStatsUseCase`: Example use case for retrieving player statistics + - Future: `AnalyzeFantasyRosterUseCase`, `ComparePlayersUseCase`, etc. + +- **DTOs** (`dto/`): Data transfer objects for cross-layer communication + +**Dependencies**: Depends only on Domain layer abstractions. + +### 3. Infrastructure Layer (`src/infrastructure/`) +**Purpose**: Contains implementations of external interfaces and frameworks. + +- **Adapters** (`adapters/`): Implementations of repository interfaces + - `nhl/NHLPlayerAdapter`: Implements PlayerRepository using nhl-api-py + - `nhl/NHLTeamAdapter`: Implements TeamRepository using nhl-api-py + - `yahoo_fantasy/YahooFantasyAdapter`: Implements FantasyRepository using yfpy + +- **Database** (`database/`): Database models and ORM configurations +- **Config** (`config/`): Application configuration and settings + +**Dependencies**: Implements Domain interfaces using external libraries. + +### 4. Presentation Layer (`src/presentation/`) +**Purpose**: Contains API routes and controllers. + +- **API Routes** (`api/routes/`): FastAPI endpoint definitions +- **Dependencies** (`api/dependencies.py`): Dependency injection configuration + +**Dependencies**: Uses Application use cases and Infrastructure implementations. + +## Dependency Flow + +``` +Presentation Layer + ↓ +Application Layer + ↓ +Domain Layer (Abstractions) + ↑ +Infrastructure Layer (Implementations) +``` + +**Key Principle**: Dependencies point inward. Outer layers depend on inner layers, never the reverse. + +## Swapping Data Sources + +To replace the NHL API with a different data source: + +1. Create a new adapter implementing `PlayerRepository` or `TeamRepository` +2. Update the dependency injection in `src/presentation/api/dependencies.py` +3. **No changes required** to domain entities, use cases, or API routes + +Example: + +```python +# In dependencies.py +def get_player_repository() -> PlayerRepository: + # Change this line to use a different implementation + return AlternativePlayerAdapter() # Instead of NHLPlayerAdapter() +``` + +## Testing Strategy + +- **Unit Tests**: Test domain entities and use cases in isolation +- **Integration Tests**: Test adapters against real or mocked external APIs +- **API Tests**: Test FastAPI routes using TestClient + +All business logic can be tested without external dependencies by using mock repository implementations. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc69411 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Project Kempe - Fantasy Hockey Backend + +A CLEAN architecture backend application for managing and analyzing Yahoo Fantasy Hockey teams using NHL live data. + +## Architecture + +This application follows CLEAN architecture principles with clear separation of concerns: + +``` +src/ +├── domain/ # Enterprise business rules +│ ├── entities/ # Core business entities +│ └── repositories/ # Repository interfaces (ports) +├── application/ # Application business rules +│ ├── use_cases/ # Use case implementations +│ └── dto/ # Data Transfer Objects +├── infrastructure/ # Frameworks and drivers +│ ├── adapters/ # External service adapters +│ │ ├── nhl/ # NHL API implementation +│ │ └── yahoo_fantasy/ # Yahoo Fantasy API implementation +│ ├── database/ # Database models and repositories +│ └── config/ # Configuration management +└── presentation/ # Interface adapters + └── api/ # FastAPI routes and controllers +``` + +## Key Design Principles + +- **Dependency Inversion**: Core business logic depends on abstractions, not implementations +- **Separation of Concerns**: Each layer has a single, well-defined responsibility +- **Testability**: Business logic can be tested without external dependencies +- **Swappable Adapters**: Data sources (NHL API, Yahoo Fantasy API) can be replaced without changing business logic + +## Technology Stack + +- **Framework**: FastAPI +- **Database**: PostgreSQL +- **Language**: Python 3.11+ +- **Data Sources**: + - NHL Unofficial API (via nhl-api-py) + - Yahoo Fantasy Sports API (via yfpy) + +## Setup + +### Prerequisites +- Python 3.11 or higher +- PostgreSQL 14 or higher +- Yahoo Developer Account (for Fantasy API access) + +### Installation + +1. Clone the repository: +```bash +cd /Users/michaelsimard/dev/services/project-kempe-backend +``` + +2. Create and activate a virtual environment: +```bash +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +``` + +3. Install dependencies: +```bash +pip install -r requirements.txt +``` + +4. Configure environment variables: +```bash +cp .env.example .env +# Edit .env with your actual credentials +``` + +5. Set up the database: +```bash +# Create PostgreSQL database +createdb fantasy_hockey + +# Run migrations (once implemented) +alembic upgrade head +``` + +### Running the Application + +Development server: +```bash +uvicorn src.presentation.api.main:app --reload --host 0.0.0.0 --port 8000 +``` + +The API will be available at: +- Base URL: http://localhost:8000 +- Interactive docs: http://localhost:8000/docs +- Alternative docs: http://localhost:8000/redoc + +## Development + +### Running Tests +```bash +pytest tests/ -v +pytest tests/ --cov=src --cov-report=html +``` + +### Code Quality +```bash +# Format code +black src/ tests/ + +# Lint code +ruff check src/ tests/ + +# Type checking +mypy src/ +``` + +### Project Structure +See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed architecture documentation. + +## API Documentation + +Once running, visit http://localhost:8000/docs for interactive API documentation. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..544e339 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +# Web Framework +fastapi==0.115.5 +uvicorn[standard]==0.32.1 +pydantic==2.10.3 +pydantic-settings==2.6.1 + +# Database +sqlalchemy==2.0.36 +asyncpg==0.30.0 +alembic==1.14.0 + +# External APIs +nhl-api-py==1.1.0 +yfpy==14.1.1 + +# Utilities +python-dotenv==1.0.1 +httpx==0.28.1 + +# Development +pytest==8.3.4 +pytest-asyncio==0.24.0 +pytest-cov==6.0.0 +black==24.10.0 +ruff==0.8.4 +mypy==1.13.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..f11020d --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""Project Kempe backend package.""" diff --git a/src/application/use_cases/__init__.py b/src/application/use_cases/__init__.py new file mode 100644 index 0000000..db3d199 --- /dev/null +++ b/src/application/use_cases/__init__.py @@ -0,0 +1,4 @@ +"""Application use cases package.""" +from .get_player_stats import GetPlayerStatsUseCase + +__all__ = ["GetPlayerStatsUseCase"] diff --git a/src/application/use_cases/get_player_stats.py b/src/application/use_cases/get_player_stats.py new file mode 100644 index 0000000..d22b478 --- /dev/null +++ b/src/application/use_cases/get_player_stats.py @@ -0,0 +1,53 @@ +"""Use case for retrieving player statistics.""" +from typing import Optional, Union + +from src.domain.entities import Player, SkaterStats, GoalieStats +from src.domain.repositories import PlayerRepository + + +class GetPlayerStatsUseCase: + """ + Use case for retrieving a player's current season statistics. + + This use case demonstrates CLEAN architecture principles: + - Depends on repository abstraction, not concrete implementation + - Contains business logic independent of external frameworks + - Can be tested without any infrastructure dependencies + """ + + 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, player_id: str + ) -> Optional[tuple[Player, Union[SkaterStats, GoalieStats]]]: + """ + Retrieves a player and their current season statistics. + + Args: + player_id: The unique identifier of the player + + Returns: + A tuple containing the Player and their statistics, or None if not found + """ + # Get player information + player = await self.player_repository.get_player_by_id(player_id) + if not player: + return None + + # Get appropriate statistics based on position + if player.is_goalie(): + stats = await self.player_repository.get_goalie_stats(player_id) + else: + stats = await self.player_repository.get_skater_stats(player_id) + + if not stats: + return None + + return (player, stats) diff --git a/src/domain/entities/__init__.py b/src/domain/entities/__init__.py new file mode 100644 index 0000000..7ce7a17 --- /dev/null +++ b/src/domain/entities/__init__.py @@ -0,0 +1,13 @@ +"""Domain entities package.""" +from .player import Player +from .player_stats import GoalieStats, SkaterStats +from .team import NHLTeam +from .fantasy_team import FantasyTeam + +__all__ = [ + "Player", + "SkaterStats", + "GoalieStats", + "NHLTeam", + "FantasyTeam", +] diff --git a/src/domain/entities/fantasy_team.py b/src/domain/entities/fantasy_team.py new file mode 100644 index 0000000..0b179fd --- /dev/null +++ b/src/domain/entities/fantasy_team.py @@ -0,0 +1,33 @@ +"""Fantasy team domain entity.""" +from dataclasses import dataclass +from typing import List + + +@dataclass +class FantasyTeam: + """Represents a Yahoo Fantasy Hockey team.""" + + id: str + name: str + manager_name: str + league_id: str + player_ids: List[str] + wins: int + losses: int + ties: int + points_for: float + points_against: float + rank: int + + @property + def win_percentage(self) -> float: + """Calculates the team's winning percentage.""" + total_games = self.wins + self.losses + self.ties + if total_games == 0: + return 0.0 + return self.wins / total_games + + @property + def roster_size(self) -> int: + """Returns the number of players on the roster.""" + return len(self.player_ids) diff --git a/src/domain/entities/player.py b/src/domain/entities/player.py new file mode 100644 index 0000000..29258a9 --- /dev/null +++ b/src/domain/entities/player.py @@ -0,0 +1,37 @@ +"""Player domain entity.""" +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + + +@dataclass +class Player: + """Represents an NHL player in the domain model.""" + + id: str + first_name: str + last_name: str + jersey_number: Optional[int] + position: str # C, LW, RW, D, G + team_id: str + is_active: bool + birth_date: Optional[datetime] + height_inches: Optional[int] + weight_pounds: Optional[int] + + @property + def full_name(self) -> str: + """Returns the player's full name.""" + return f"{self.first_name} {self.last_name}" + + def is_forward(self) -> bool: + """Checks if the player is a forward.""" + return self.position in ["C", "LW", "RW"] + + def is_defenseman(self) -> bool: + """Checks if the player is a defenseman.""" + return self.position == "D" + + def is_goalie(self) -> bool: + """Checks if the player is a goalie.""" + return self.position == "G" diff --git a/src/domain/entities/player_stats.py b/src/domain/entities/player_stats.py new file mode 100644 index 0000000..8495994 --- /dev/null +++ b/src/domain/entities/player_stats.py @@ -0,0 +1,48 @@ +"""Player statistics domain entity.""" +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + + +@dataclass +class SkaterStats: + """Statistics for skaters (forwards and defensemen).""" + + 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 + updated_at: datetime + + +@dataclass +class GoalieStats: + """Statistics for goalies.""" + + 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] + updated_at: datetime diff --git a/src/domain/entities/team.py b/src/domain/entities/team.py new file mode 100644 index 0000000..7cd479c --- /dev/null +++ b/src/domain/entities/team.py @@ -0,0 +1,22 @@ +"""Team domain entity.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class NHLTeam: + """Represents an NHL team in the domain model.""" + + id: str + name: str + abbreviation: str + city: str + division: str + conference: str + venue_name: Optional[str] + is_active: bool + + @property + def full_name(self) -> str: + """Returns the team's full name with city.""" + return f"{self.city} {self.name}" diff --git a/src/domain/repositories/__init__.py b/src/domain/repositories/__init__.py new file mode 100644 index 0000000..7a4be98 --- /dev/null +++ b/src/domain/repositories/__init__.py @@ -0,0 +1,10 @@ +"""Domain repository interfaces package.""" +from .player_repository import PlayerRepository +from .team_repository import TeamRepository +from .fantasy_repository import FantasyRepository + +__all__ = [ + "PlayerRepository", + "TeamRepository", + "FantasyRepository", +] diff --git a/src/domain/repositories/fantasy_repository.py b/src/domain/repositories/fantasy_repository.py new file mode 100644 index 0000000..b1d5da0 --- /dev/null +++ b/src/domain/repositories/fantasy_repository.py @@ -0,0 +1,31 @@ +"""Fantasy team repository interface.""" +from abc import ABC, abstractmethod +from typing import List, Optional + +from src.domain.entities import FantasyTeam + + +class FantasyRepository(ABC): + """Abstract interface for Yahoo Fantasy Hockey data access.""" + + @abstractmethod + async def get_fantasy_team( + self, league_id: str, team_id: str + ) -> Optional[FantasyTeam]: + """Retrieves a specific fantasy team from a league.""" + pass + + @abstractmethod + async def get_user_teams(self, user_id: str) -> List[FantasyTeam]: + """Retrieves all fantasy teams for a user.""" + pass + + @abstractmethod + async def get_team_roster(self, league_id: str, team_id: str) -> List[str]: + """Retrieves the player IDs on a fantasy team's roster.""" + pass + + @abstractmethod + async def get_league_standings(self, league_id: str) -> List[FantasyTeam]: + """Retrieves all teams in a league ordered by standings.""" + pass diff --git a/src/domain/repositories/player_repository.py b/src/domain/repositories/player_repository.py new file mode 100644 index 0000000..e129f0e --- /dev/null +++ b/src/domain/repositories/player_repository.py @@ -0,0 +1,36 @@ +"""Player repository interface.""" +from abc import ABC, abstractmethod +from typing import List, Optional + +from src.domain.entities import Player, SkaterStats, GoalieStats + + +class PlayerRepository(ABC): + """Abstract interface for player data access.""" + + @abstractmethod + async def get_player_by_id(self, player_id: str) -> Optional[Player]: + """Retrieves a player by their unique identifier.""" + pass + + @abstractmethod + async def get_players_by_team(self, team_id: str) -> List[Player]: + """Retrieves all players for a specific team.""" + pass + + @abstractmethod + async def search_players( + self, name: str, position: Optional[str] = None + ) -> List[Player]: + """Searches for players by name and optionally by position.""" + pass + + @abstractmethod + async def get_skater_stats(self, player_id: str) -> Optional[SkaterStats]: + """Retrieves current season statistics for a skater.""" + pass + + @abstractmethod + async def get_goalie_stats(self, player_id: str) -> Optional[GoalieStats]: + """Retrieves current season statistics for a goalie.""" + pass diff --git a/src/domain/repositories/team_repository.py b/src/domain/repositories/team_repository.py new file mode 100644 index 0000000..dd40e72 --- /dev/null +++ b/src/domain/repositories/team_repository.py @@ -0,0 +1,29 @@ +"""Team repository interface.""" +from abc import ABC, abstractmethod +from typing import List, Optional + +from src.domain.entities import NHLTeam + + +class TeamRepository(ABC): + """Abstract interface for NHL team data access.""" + + @abstractmethod + async def get_team_by_id(self, team_id: str) -> Optional[NHLTeam]: + """Retrieves a team by its unique identifier.""" + pass + + @abstractmethod + async def get_all_teams(self) -> List[NHLTeam]: + """Retrieves all NHL teams.""" + pass + + @abstractmethod + async def get_teams_by_division(self, division: str) -> List[NHLTeam]: + """Retrieves all teams in a specific division.""" + pass + + @abstractmethod + async def get_teams_by_conference(self, conference: str) -> List[NHLTeam]: + """Retrieves all teams in a specific conference.""" + pass diff --git a/src/infrastructure/adapters/nhl/__init__.py b/src/infrastructure/adapters/nhl/__init__.py new file mode 100644 index 0000000..c003f0c --- /dev/null +++ b/src/infrastructure/adapters/nhl/__init__.py @@ -0,0 +1,4 @@ +"""NHL API adapter package.""" +from .nhl_adapter import NHLPlayerAdapter, NHLTeamAdapter + +__all__ = ["NHLPlayerAdapter", "NHLTeamAdapter"] diff --git a/src/infrastructure/adapters/nhl/nhl_adapter.py b/src/infrastructure/adapters/nhl/nhl_adapter.py new file mode 100644 index 0000000..d7f3fc8 --- /dev/null +++ b/src/infrastructure/adapters/nhl/nhl_adapter.py @@ -0,0 +1,109 @@ +"""NHL API adapter implementation.""" +from typing import List, Optional +from datetime import datetime + +from nhl_api import NHLClient + +from src.domain.entities import Player, SkaterStats, GoalieStats, NHLTeam +from src.domain.repositories import PlayerRepository, TeamRepository + + +class NHLPlayerAdapter(PlayerRepository): + """Adapter for NHL API player data access.""" + + def __init__(self): + """Initializes the NHL API client.""" + self.client = NHLClient() + + async def get_player_by_id(self, player_id: str) -> Optional[Player]: + """Retrieves a player by their unique identifier.""" + try: + # TODO: Implement actual NHL API calls + # This is a placeholder for the actual implementation + # The nhl-api-py library will be used here + pass + except Exception as e: + # Log error appropriately + return None + + async def get_players_by_team(self, team_id: str) -> List[Player]: + """Retrieves all players for a specific team.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def search_players( + self, name: str, position: Optional[str] = None + ) -> List[Player]: + """Searches for players by name and optionally by position.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def get_skater_stats(self, player_id: str) -> Optional[SkaterStats]: + """Retrieves current season statistics for a skater.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return None + + async def get_goalie_stats(self, player_id: str) -> Optional[GoalieStats]: + """Retrieves current season statistics for a goalie.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return None + + +class NHLTeamAdapter(TeamRepository): + """Adapter for NHL API team data access.""" + + def __init__(self): + """Initializes the NHL API client.""" + self.client = NHLClient() + + async def get_team_by_id(self, team_id: str) -> Optional[NHLTeam]: + """Retrieves a team by its unique identifier.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return None + + async def get_all_teams(self) -> List[NHLTeam]: + """Retrieves all NHL teams.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def get_teams_by_division(self, division: str) -> List[NHLTeam]: + """Retrieves all teams in a specific division.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def get_teams_by_conference(self, conference: str) -> List[NHLTeam]: + """Retrieves all teams in a specific conference.""" + try: + # TODO: Implement actual NHL API calls + pass + except Exception as e: + # Log error appropriately + return [] diff --git a/src/infrastructure/adapters/yahoo_fantasy/__init__.py b/src/infrastructure/adapters/yahoo_fantasy/__init__.py new file mode 100644 index 0000000..300af53 --- /dev/null +++ b/src/infrastructure/adapters/yahoo_fantasy/__init__.py @@ -0,0 +1,4 @@ +"""Yahoo Fantasy API adapter package.""" +from .yahoo_adapter import YahooFantasyAdapter + +__all__ = ["YahooFantasyAdapter"] diff --git a/src/infrastructure/adapters/yahoo_fantasy/yahoo_adapter.py b/src/infrastructure/adapters/yahoo_fantasy/yahoo_adapter.py new file mode 100644 index 0000000..64d354b --- /dev/null +++ b/src/infrastructure/adapters/yahoo_fantasy/yahoo_adapter.py @@ -0,0 +1,62 @@ +"""Yahoo Fantasy API adapter implementation.""" +from typing import List, Optional + +from yfpy.query import YahooFantasySportsQuery + +from src.domain.entities import FantasyTeam +from src.domain.repositories import FantasyRepository + + +class YahooFantasyAdapter(FantasyRepository): + """Adapter for Yahoo Fantasy Sports API data access.""" + + def __init__(self, consumer_key: str, consumer_secret: str): + """ + Initializes the Yahoo Fantasy API client. + + Args: + consumer_key: Yahoo API consumer key + consumer_secret: Yahoo API consumer secret + """ + # TODO: Initialize YFPY client with OAuth credentials + # self.client = YahooFantasySportsQuery(...) + self.consumer_key = consumer_key + self.consumer_secret = consumer_secret + + async def get_fantasy_team( + self, league_id: str, team_id: str + ) -> Optional[FantasyTeam]: + """Retrieves a specific fantasy team from a league.""" + try: + # TODO: Implement actual Yahoo Fantasy API calls + pass + except Exception as e: + # Log error appropriately + return None + + async def get_user_teams(self, user_id: str) -> List[FantasyTeam]: + """Retrieves all fantasy teams for a user.""" + try: + # TODO: Implement actual Yahoo Fantasy API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def get_team_roster(self, league_id: str, team_id: str) -> List[str]: + """Retrieves the player IDs on a fantasy team's roster.""" + try: + # TODO: Implement actual Yahoo Fantasy API calls + pass + except Exception as e: + # Log error appropriately + return [] + + async def get_league_standings(self, league_id: str) -> List[FantasyTeam]: + """Retrieves all teams in a league ordered by standings.""" + try: + # TODO: Implement actual Yahoo Fantasy API calls + pass + except Exception as e: + # Log error appropriately + return [] diff --git a/src/infrastructure/config/__init__.py b/src/infrastructure/config/__init__.py new file mode 100644 index 0000000..3dc09bb --- /dev/null +++ b/src/infrastructure/config/__init__.py @@ -0,0 +1,4 @@ +"""Configuration package.""" +from .settings import Settings, get_settings + +__all__ = ["Settings", "get_settings"] diff --git a/src/infrastructure/config/settings.py b/src/infrastructure/config/settings.py new file mode 100644 index 0000000..ec5ed6e --- /dev/null +++ b/src/infrastructure/config/settings.py @@ -0,0 +1,33 @@ +"""Application configuration settings.""" +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + ) + + # Application + app_name: str = "Project Kempe - Fantasy Hockey Backend" + app_version: str = "0.1.0" + debug: bool = False + + # Database + database_url: str = "postgresql+asyncpg://user:password@localhost:5432/fantasy_hockey" + + # Yahoo Fantasy API + yahoo_consumer_key: str = "" + yahoo_consumer_secret: str = "" + + # API Configuration + api_prefix: str = "/api/v1" + cors_origins: list[str] = ["http://localhost:3000"] + + +def get_settings() -> Settings: + """Returns the application settings singleton.""" + return Settings() diff --git a/src/presentation/api/dependencies.py b/src/presentation/api/dependencies.py new file mode 100644 index 0000000..d30fb2d --- /dev/null +++ b/src/presentation/api/dependencies.py @@ -0,0 +1,47 @@ +"""FastAPI dependency injection configuration.""" +from functools import lru_cache + +from src.domain.repositories import PlayerRepository, TeamRepository, FantasyRepository +from src.infrastructure.adapters.nhl import NHLPlayerAdapter, NHLTeamAdapter +from src.infrastructure.adapters.yahoo_fantasy import YahooFantasyAdapter +from src.infrastructure.config import Settings, get_settings + + +@lru_cache() +def get_cached_settings() -> Settings: + """Returns cached application settings.""" + return get_settings() + + +def get_player_repository() -> PlayerRepository: + """ + Provides the PlayerRepository implementation. + + This function can be modified to return different implementations + without changing any business logic or API code. + """ + return NHLPlayerAdapter() + + +def get_team_repository() -> TeamRepository: + """ + Provides the TeamRepository implementation. + + This function can be modified to return different implementations + without changing any business logic or API code. + """ + return NHLTeamAdapter() + + +def get_fantasy_repository() -> FantasyRepository: + """ + Provides the FantasyRepository implementation. + + This function can be modified to return different implementations + without changing any business logic or API code. + """ + settings = get_cached_settings() + return YahooFantasyAdapter( + consumer_key=settings.yahoo_consumer_key, + consumer_secret=settings.yahoo_consumer_secret, + ) diff --git a/src/presentation/api/main.py b/src/presentation/api/main.py new file mode 100644 index 0000000..e6589d8 --- /dev/null +++ b/src/presentation/api/main.py @@ -0,0 +1,45 @@ +"""FastAPI application entry point.""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.infrastructure.config import get_settings + +settings = get_settings() + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + debug=settings.debug, +) + +# Configure CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + """Root endpoint.""" + return { + "name": settings.app_name, + "version": settings.app_version, + "status": "operational", + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint.""" + 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"]) diff --git a/src/presentation/api/routes/players.py b/src/presentation/api/routes/players.py new file mode 100644 index 0000000..8504762 --- /dev/null +++ b/src/presentation/api/routes/players.py @@ -0,0 +1,41 @@ +"""Player API routes.""" +from fastapi import APIRouter, Depends, HTTPException +from typing import Union + +from src.domain.repositories import PlayerRepository +from src.application.use_cases import GetPlayerStatsUseCase +from src.presentation.api.dependencies import get_player_repository + +router = APIRouter() + + +@router.get("/{player_id}/stats") +async def get_player_stats( + player_id: str, + player_repo: PlayerRepository = Depends(get_player_repository), +): + """ + Retrieves a player's current season statistics. + + This endpoint demonstrates CLEAN architecture: + - The route depends on the repository abstraction + - Business logic is in the use case, not the route + - The concrete implementation is injected via FastAPI's dependency system + """ + use_case = GetPlayerStatsUseCase(player_repo) + result = await use_case.execute(player_id) + + if not result: + raise HTTPException(status_code=404, detail="Player not found") + + player, stats = result + + return { + "player": { + "id": player.id, + "name": player.full_name, + "position": player.position, + "team_id": player.team_id, + }, + "stats": stats.__dict__, + }