diff --git a/backend/database.py b/backend/database.py index 023f2ba..02f0ad6 100644 --- a/backend/database.py +++ b/backend/database.py @@ -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" diff --git a/backend/main.py b/backend/main.py index 2bf755d..8a00bc6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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 @@ -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, @@ -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 diff --git a/backend/models.py b/backend/models.py index decc9a0..d0970d3 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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) \ No newline at end of file + 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") \ No newline at end of file diff --git a/backend/test.db b/backend/test.db new file mode 100644 index 0000000..a69cebd Binary files /dev/null and b/backend/test.db differ diff --git a/backend/test_players_api.py b/backend/test_players_api.py new file mode 100644 index 0000000..2b02bbc --- /dev/null +++ b/backend/test_players_api.py @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ca8e18d..5415ddc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ # docker-compose.yml -version: '3.8' services: db: