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 <noreply@anthropic.com>
This commit is contained in:
13
.env.example
Normal file
13
.env.example
Normal file
@@ -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"]
|
||||||
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -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
|
||||||
94
ARCHITECTURE.md
Normal file
94
ARCHITECTURE.md
Normal file
@@ -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.
|
||||||
120
README.md
Normal file
120
README.md
Normal file
@@ -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.
|
||||||
26
requirements.txt
Normal file
26
requirements.txt
Normal file
@@ -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
|
||||||
1
src/__init__.py
Normal file
1
src/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Project Kempe backend package."""
|
||||||
4
src/application/use_cases/__init__.py
Normal file
4
src/application/use_cases/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Application use cases package."""
|
||||||
|
from .get_player_stats import GetPlayerStatsUseCase
|
||||||
|
|
||||||
|
__all__ = ["GetPlayerStatsUseCase"]
|
||||||
53
src/application/use_cases/get_player_stats.py
Normal file
53
src/application/use_cases/get_player_stats.py
Normal file
@@ -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)
|
||||||
13
src/domain/entities/__init__.py
Normal file
13
src/domain/entities/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
33
src/domain/entities/fantasy_team.py
Normal file
33
src/domain/entities/fantasy_team.py
Normal file
@@ -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)
|
||||||
37
src/domain/entities/player.py
Normal file
37
src/domain/entities/player.py
Normal file
@@ -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"
|
||||||
48
src/domain/entities/player_stats.py
Normal file
48
src/domain/entities/player_stats.py
Normal file
@@ -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
|
||||||
22
src/domain/entities/team.py
Normal file
22
src/domain/entities/team.py
Normal file
@@ -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}"
|
||||||
10
src/domain/repositories/__init__.py
Normal file
10
src/domain/repositories/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
31
src/domain/repositories/fantasy_repository.py
Normal file
31
src/domain/repositories/fantasy_repository.py
Normal file
@@ -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
|
||||||
36
src/domain/repositories/player_repository.py
Normal file
36
src/domain/repositories/player_repository.py
Normal file
@@ -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
|
||||||
29
src/domain/repositories/team_repository.py
Normal file
29
src/domain/repositories/team_repository.py
Normal file
@@ -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
|
||||||
4
src/infrastructure/adapters/nhl/__init__.py
Normal file
4
src/infrastructure/adapters/nhl/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""NHL API adapter package."""
|
||||||
|
from .nhl_adapter import NHLPlayerAdapter, NHLTeamAdapter
|
||||||
|
|
||||||
|
__all__ = ["NHLPlayerAdapter", "NHLTeamAdapter"]
|
||||||
109
src/infrastructure/adapters/nhl/nhl_adapter.py
Normal file
109
src/infrastructure/adapters/nhl/nhl_adapter.py
Normal file
@@ -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 []
|
||||||
4
src/infrastructure/adapters/yahoo_fantasy/__init__.py
Normal file
4
src/infrastructure/adapters/yahoo_fantasy/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Yahoo Fantasy API adapter package."""
|
||||||
|
from .yahoo_adapter import YahooFantasyAdapter
|
||||||
|
|
||||||
|
__all__ = ["YahooFantasyAdapter"]
|
||||||
62
src/infrastructure/adapters/yahoo_fantasy/yahoo_adapter.py
Normal file
62
src/infrastructure/adapters/yahoo_fantasy/yahoo_adapter.py
Normal file
@@ -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 []
|
||||||
4
src/infrastructure/config/__init__.py
Normal file
4
src/infrastructure/config/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Configuration package."""
|
||||||
|
from .settings import Settings, get_settings
|
||||||
|
|
||||||
|
__all__ = ["Settings", "get_settings"]
|
||||||
33
src/infrastructure/config/settings.py
Normal file
33
src/infrastructure/config/settings.py
Normal file
@@ -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()
|
||||||
47
src/presentation/api/dependencies.py
Normal file
47
src/presentation/api/dependencies.py
Normal file
@@ -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,
|
||||||
|
)
|
||||||
45
src/presentation/api/main.py
Normal file
45
src/presentation/api/main.py
Normal file
@@ -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"])
|
||||||
41
src/presentation/api/routes/players.py
Normal file
41
src/presentation/api/routes/players.py
Normal file
@@ -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__,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user