Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions backend/database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# backend/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import sessionmaker, declarative_base

SQLALCHEMY_DATABASE_URL = "postgresql://admin:password123@db/wnba_db"

Expand Down
86 changes: 56 additions & 30 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# backend/main.py
import logging
logging.basicConfig(level=logging.INFO)

from contextlib import asynccontextmanager # Lifespan manager
from fastapi import FastAPI, Depends, HTTPException
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from pydantic import BaseModel # Import Pydantic
from pydantic import BaseModel, ConfigDict
from typing import List

# Import your SQLAlchemy models and session management
import models
Expand All @@ -13,17 +17,13 @@
@asynccontextmanager
async def lifespan(app: FastAPI):
print("Application startup: creating database tables...")
# Create the database tables
database.Base.metadata.create_all(bind=database.engine)
yield
print("Application shutdown.")

app = FastAPI(lifespan=lifespan)

origins = [
"http://localhost:3000",
]

origins = ["http://localhost:3000"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
Expand All @@ -40,64 +40,90 @@ def get_db():
finally:
db.close()

# Pydantic model for creating a player (data validation)
class PlayerCreate(BaseModel):
# --- Pydantic Schemas ---
class PlayerStatBase(BaseModel):
season: str
points_per_game: int
rebounds_per_game: int
assists_per_game: int

class PlayerStatCreate(PlayerStatBase):
pass

class PlayerStat(PlayerStatBase):
id: int
player_id: int
model_config = ConfigDict(from_attributes=True)

class PlayerBase(BaseModel):
first_name: str
last_name: str
team: str

# ---- API ENDPOINTS ----
class PlayerCreate(PlayerBase):
pass

class Player(PlayerBase):
id: int
stats: List[PlayerStat] = []
model_config = ConfigDict(from_attributes=True)

# ---- API ENDPOINTS ----
@app.get("/api")
def read_root():
return {"message": "WNBA Analytics API is running!"}

# Endpoint to CREATE a new player
@app.post("/api/players")
@app.post("/api/players", response_model=Player)
def create_player(player: PlayerCreate, db: Session = Depends(get_db)):
new_player = models.Player(
first_name=player.first_name,
last_name=player.last_name,
team=player.team
)
new_player = models.Player(**player.model_dump())
db.add(new_player)
db.commit()
db.refresh(new_player)
return new_player

# Endpoint to READ all players
@app.get("/api/players")
@app.get("/api/players", response_model=List[Player])
def get_players(db: Session = Depends(get_db)):
players = db.query(models.Player).all()
return players

@app.get("/api/players/{player_id}", response_model=Player)
def get_player(player_id: int, db: Session = Depends(get_db)):
player = db.query(models.Player).filter(models.Player.id == player_id).first()
if player is None: raise HTTPException(status_code=404, detail="Player not found")
return player

# Endpoint to DELETE a player
@app.delete("/api/players/{player_id}")
def delete_player(player_id: int, db: Session = Depends(get_db)):
player_to_delete = db.query(models.Player).filter(models.Player.id == player_id).first()

if player_to_delete is None:
raise HTTPException(status_code=404, detail="Player not found")

if player_to_delete is None: raise HTTPException(status_code=404, detail="Player not found")
db.delete(player_to_delete)
db.commit()

return {"message": "Player deleted successfully"}

# Endpoint to UPDATE a player
@app.put("/api/players/{player_id}")
@app.put("/api/players/{player_id}", response_model=Player)
def update_player(player_id: int, player_update: PlayerCreate, db: Session = Depends(get_db)):
player_to_update = db.query(models.Player).filter(models.Player.id == player_id).first()

if player_to_update is None:
raise HTTPException(status_code=404, detail="Player not found")
if player_to_update is None: raise HTTPException(status_code=404, detail="Player not found")

# Update the player's attributes
player_to_update.first_name = player_update.first_name
player_to_update.last_name = player_update.last_name
player_to_update.team = player_update.team
for key, value in player_update.model_dump().items():
setattr(player_to_update, key, value)

db.commit()
db.refresh(player_to_update)

return player_to_update

@app.post("/api/players/{player_id}/stats", response_model=PlayerStat)
def create_stats_for_player(player_id: int, stat: PlayerStatCreate,
db: Session = Depends(get_db)):
db_player = db.query(models.Player).filter(models.Player.id == player_id).first()
if db_player is None: raise HTTPException(status_code=404, detail="Player not found")
db_stat = models.PlayerStat(**stat.model_dump(), player_id=player_id)
db.add(db_stat)
db.commit()
db.refresh(db_stat)
return db_stat
17 changes: 14 additions & 3 deletions backend/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
# backend/models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base

class Player(Base):
__tablename__ = "players"

id = Column(Integer, primary_key=True, index=True)
first_name = Column(String, index=True)
last_name = Column(String, index=True)
team = Column(String, index=True)
team = Column(String, index=True)
stats = relationship("PlayerStat", back_populates="player", cascade="all, delete-orphan") # if you delete a player, all of their associated stats will be automatically deleted too

class PlayerStat(Base):
__tablename__ = "player_stats"
id = Column(Integer, primary_key=True, index=True)
season = Column(String, index=True)
points_per_game = Column(Integer)
rebounds_per_game = Column(Integer)
assists_per_game = Column(Integer)
player_id = Column(Integer, ForeignKey("players.id"))
player = relationship("Player", back_populates="stats")
Binary file added backend/test.db
Binary file not shown.
87 changes: 87 additions & 0 deletions backend/test_players_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# backend/test_players_api.py
from fastapi.testclient import TestClient
from main import app, get_db
from database import Base, engine
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# --- Test Database Setup ---
# Use a separate in-memory SQLite database for testing
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Create the tables in the test database
Base.metadata.create_all(bind=engine)

# This is a pytest "fixture" - it runs before each test that needs it
@pytest.fixture(scope="function")
def db_session():
"""Create a new database session for a test."""
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()

# This overrides the get_db dependency in your app for the duration of the tests
def override_get_db():
try:
db = TestingSessionLocal()
yield db
finally:
db.close()

app.dependency_overrides[get_db] = override_get_db
client = TestClient(app)

# --- Actual Tests ---
def test_create_and_get_player(db_session):
"""
Test creating a player and then retrieving them.
"""
# Create a player
response = client.post(
"/api/players",
json={"first_name": "Caitlin", "last_name": "Clark", "team": "Indiana Fever"}
)
assert response.status_code == 200, response.text
data = response.json()
assert data["first_name"] == "Caitlin"
assert "id" in data
player_id = data["id"]

# Get the player
response = client.get(f"/api/players/{player_id}")
assert response.status_code == 200, response.text
data = response.json()
assert data["first_name"] == "Caitlin"

def test_add_stats_to_player(db_session):
"""
Test adding stats to an existing player.
"""
# First, create a player to add stats to
player_response = client.post(
"/api/players",
json={"first_name": "Aliyah", "last_name": "Boston", "team": "Indiana Fever"}
)
player_id = player_response.json()["id"]

# Now, add stats to that player
stats_response = client.post(
f"/api/players/{player_id}/stats",
json={"season": "2023", "points_per_game": 14, "rebounds_per_game": 8, "assists_per_game": 2}
)
assert stats_response.status_code == 200, stats_response.text
stats_data = stats_response.json()
assert stats_data["season"] == "2023"

# Verify that the player now has these stats
response = client.get(f"/api/players/{player_id}")
player_data = response.json()
assert len(player_data["stats"]) == 1
assert player_data["stats"][0]["points_per_game"] == 14
1 change: 0 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# docker-compose.yml
version: '3.8'

services:
db:
Expand Down