From 390493724a0ab9f48346e632e4df13962d77beab Mon Sep 17 00:00:00 2001 From: Abby Seseri Date: Wed, 11 Jun 2025 12:22:04 -0700 Subject: [PATCH 1/2] feat(backend): Add player_stats model and API endpoints --- backend/main.py | 86 +++++++++++++++++++++++++++++++---------------- backend/models.py | 17 ++++++++-- 2 files changed, 71 insertions(+), 32 deletions(-) diff --git a/backend/main.py b/backend/main.py index 2bf755d..b565734 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 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,92 @@ 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 + class Config: + 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] = [] + class Config: + 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 From bf006a074e8fc8ee03b4ff9d8a0e19d062d95077 Mon Sep 17 00:00:00 2001 From: Abby Seseri Date: Wed, 11 Jun 2025 12:30:42 -0700 Subject: [PATCH 2/2] test(backend): Add tests for player creation and stats (and edit to remove warnings) --- backend/database.py | 3 +- backend/main.py | 8 ++-- backend/test.db | Bin 0 -> 36864 bytes backend/test_players_api.py | 87 ++++++++++++++++++++++++++++++++++++ docker-compose.yml | 1 - 5 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 backend/test.db create mode 100644 backend/test_players_api.py 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 b565734..8a00bc6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,7 +6,7 @@ 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 @@ -53,8 +53,7 @@ class PlayerStatCreate(PlayerStatBase): class PlayerStat(PlayerStatBase): id: int player_id: int - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) class PlayerBase(BaseModel): first_name: str @@ -67,8 +66,7 @@ class PlayerCreate(PlayerBase): class Player(PlayerBase): id: int stats: List[PlayerStat] = [] - class Config: - from_attributes = True + model_config = ConfigDict(from_attributes=True) # ---- API ENDPOINTS ---- @app.get("/api") diff --git a/backend/test.db b/backend/test.db new file mode 100644 index 0000000000000000000000000000000000000000..a69cebde403845a00f3d646bbb866d989ab51541 GIT binary patch literal 36864 zcmeI)&u`LT7{KwqFeoJxc3J91VtOGP(HPxgj5pcVO_Z_Oq0x(^8=W>XSST}_iHFU9 z&OgJO$L+N2;K8?)ABBNug>M6)ynWy2d7n?;OKIrd>!RCojKfCr$Zi>{%A6vE^4u^K zMcI~bO}@L^hTNIyevzLdv_EclTX}dO?#QEw*+nINm|jf%NFAgiv%hA)$_6$B5I_I{ z1Q0*~0R#~EM+813;`-yPIC1M$=hQu|9M|kKr&;k@cFU`{)$mts-7@oK(W$s_M_O;ygAof#=v>qaGfV_-m4&|LsIUd+y?HT-CE#@m)@TzvhJF zhWFJ@j&gpjXbw#|skv2IVcFa=Eu*wkHcD@b#Ro=G>!g00x7J_eEx+Zs;npS6vE=$_ zA35Do{-LI`-#DpPM<293&-KO(>`b#i2S3`bRoKp3?~PaH`?jgywz*@Og{_j`w%i+( zGc0q{w9L}Fx!0-7YmZ3gDzUi!BqP2Jo(8W{v%OZOE~g&^@#3TXLmm72GInox?qEjM zpJv3l7|w9$HV0G0#--@@9LUjc&h19y`u&V>hjX+X`>0dE%(&=&pMki3L+)iHt}kVT zJsh#S^u6e~c=8v@M8+m(en z4X>AE&c8Mn>7R;xu_1r}0tg_000IagfB*srAb`MsDsWH4l!b+*e9b+xKdm*qR-<01 zS6#bq8=KA-riS%zpzSs~z009ILKmY**5I_I{1Q0-A0tKQ`O$)C7FO~G=1Qtk1 z2q1s}0tg_000IagfB*srOsBwHbV(a=|NmM^Ur#3pRU?1^0tg_000IagfB*srATYTC zT694R{t^(}|L6LDa?drDA%Fk^2q1s}0tg_000Ib1uYiC3A3O{oc>dp&lD?W=4Jt900IagfB*srAb