diff --git a/alembic/env.py b/alembic/env.py index ed11da8..7564985 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -5,6 +5,7 @@ from alembic import context from sqlalchemy import engine_from_config, pool + # Ensure Alembic can import the app packages when executed from the CLI. PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: diff --git a/api/router.py b/api/router.py index 847b67e..6e3920a 100644 --- a/api/router.py +++ b/api/router.py @@ -1,8 +1,10 @@ -from fastapi import APIRouter +from fastapi import APIRouter, FastAPI from api.routes.test import router as test_router from api.routes.user import router as user_router +from api.routes.project import router as project_router api_router = APIRouter() api_router.include_router(test_router, prefix="/tests", tags=["tests"]) api_router.include_router(user_router, prefix="/user", tags=["user"]) +api_router.include_router(project_router, prefix="/project", tags=["project"]) \ No newline at end of file diff --git a/api/routes/project.py b/api/routes/project.py new file mode 100644 index 0000000..64d806c --- /dev/null +++ b/api/routes/project.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Body, Depends +from sqlalchemy.orm import Session +from db.session import get_db +from schemas.project import ProjectResponse, ProjectCU +from typing import Annotated +from services.project_service import create_project, delete_project, read_all_project, read_project, update_project +from api.deps import verify_internal_api_key +from uuid import UUID + +router = APIRouter() + +@router.post("/create", response_model=ProjectResponse, status_code=201) +def create_route( + _: Annotated[None, Depends(verify_internal_api_key)], + payload: Annotated[ProjectCU, Body(...)], + db: Session = Depends(get_db) +): + return create_project(db, payload) + +@router.get("/read/{project_id}",response_model=ProjectResponse, status_code=201) +def read_route( + _: Annotated[None, Depends(verify_internal_api_key)], + project_id: UUID, + db: Session = Depends(get_db) +): + return read_project(db, project_id) + +@router.get("/read/all/{user_id}", response_model=ProjectResponse, status_code=201) +def read_all_route( + _: Annotated[None, Depends(verify_internal_api_key)], + user_id: UUID, + db: Session = Depends(get_db) +): + return read_all_project(db, user_id) + +@router.put("/update/{project_id}", response_model=ProjectResponse, status_code=201) +def update_route( + _: Annotated[None, Depends(verify_internal_api_key)], + payload: Annotated[ProjectCU, Body(...)], + project_id: UUID, + db: Session = Depends(get_db) +): + return update_project(db, payload) + +@router.delete("/delete/{project_id}", response_model=ProjectResponse, status_code=201) +def delete_route( + project_id: UUID, + _: Annotated[None, Depends(verify_internal_api_key)], + db: Session = Depends(get_db), +): + return delete_project(db, project_id) diff --git a/main.py b/main.py index 6574980..ede064a 100644 --- a/main.py +++ b/main.py @@ -10,4 +10,4 @@ def read_root(): return {"message": "Cody API"} -app.include_router(api_router, prefix="/api") +app.include_router(api_router, prefix="/api/v1") diff --git a/schemas/project.py b/schemas/project.py new file mode 100644 index 0000000..aa63018 --- /dev/null +++ b/schemas/project.py @@ -0,0 +1,66 @@ +from pydantic import BaseModel, Field, field_validator, ConfigDict +from datetime import date, datetime +from uuid import UUID + +class ProjectCU(BaseModel): + user_id: UUID + name: str = Field( + ..., + max_length= 64 + ) + description: str = Field( + ..., + max_length= 1000 + ) + due_date: date + min_session_time: int = Field( + ..., + ge=900 + ) + @field_validator("name", "description") + @classmethod + def validate_text_fields(cls, value: str) -> str: + value = value.strip() + + if(not value): + raise ValueError("Text fields cannot be empty!") + return value + + @field_validator("user_id") + @classmethod + def validate_user_id(cls, value: UUID) -> UUID: + value = value.strip() + + if(not value): + raise ValueError("User ID cannot be empty!") + return value + + @field_validator("due_date") + @classmethod + def validate_due_date(cls, value: date) -> date: + if(value < date.today()): + raise ValueError("Cannot have a deadline in the past!") + if(not value): + raise ValueError("Deadline cannot be empty!") + return value + + @field_validator("min_session_time") + @classmethod + def validate_min_session_time(cls, value: int)->int: + value = value.strip() + + if(not value): + raise ValueError("Minimum session time cannot be empty!") + return value + +class ProjectResponse(BaseModel): + id: int + user_id: UUID + name: str + description: str + due_date: date + min_session_time: int + created_at: datetime + modified_at: datetime + +model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/schemas/user.py b/schemas/user.py index 9fa1602..ffb957b 100644 --- a/schemas/user.py +++ b/schemas/user.py @@ -33,10 +33,9 @@ def validate_discord_user_id(cls, value: str) -> str: if(not normalized_value): raise ValueError("Discord user ID must not be blank.") if(not normalized_value.isdigit()): - raise ValueError("Discord useer ID must contian only digits.") + raise ValueError("Discord user ID must contain only digits.") return normalized_value - class UserResponse(BaseModel): id: UUID discord_user_id: str diff --git a/services/project_service.py b/services/project_service.py new file mode 100644 index 0000000..0c78f2e --- /dev/null +++ b/services/project_service.py @@ -0,0 +1,98 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session +from fastapi import HTTPException +from uuid import UUID + +from models.project import Project +from schemas.project import ProjectCU + +def create_project(db: Session, payload: ProjectCU)-> Project: + record = Project(user_id = payload.user_id, + name = payload.name, + description = payload.description, + due_date = payload.due_date, + min_session_time = payload.min_session_time) + db.add(record) + try: + db.commit() + except IntegrityError as exc: + db.rollback() + raise HTTPException( + status_code= 409, + detail="user not found" + ) from exc + db.refresh(record) + return record + +def delete_project(db: Session, project_id: UUID) -> str: + project = db.query(Project).filter( + Project.id == project_id + ).first() + + if not project: + raise HTTPException( + status_code=404, + detail="Project not found" + ) + + db.delete(project) + + try: + db.commit() + except IntegrityError as exc: + db.rollback() + raise HTTPException( + status_code=409, + detail="Delete failed" + ) from exc + + return "Delete successful!" +def read_project(db: Session, project_id:UUID)-> Project: + project = db.query(Project).filter( + Project.id == project_id + ).first() + if(not project): + raise HTTPException( + status_code=404, + detail="Project not found!" + ) + return project + +def read_all_project(db: Session, user_id: UUID)-> Project: + project = db.query(Project).filter( + Project.user_id == user_id + ).get() + if(not project): + raise HTTPException( + status_code=404, + detail="User has no project!" + ) + return project + +def update_project(db: Session,project_id: UUID, payload: ProjectCU)-> Project: + project = db.query(Project).filter( + Project.id == project_id + ).first() + + if(not project): + raise HTTPException( + status_code=404, + detail="Project not found" + ) + + project.user_id = payload.user_id + project.name= payload.name + project.description = payload.description + project.due_date = payload.due_date + project.min_session_time = payload.min_session_time + + try: + db.commit() + except IntegrityError as exc: + db.rollback() + raise HTTPException( + status_code=409, + detail="Update failed!" + ) + db.refresh(project) + return project \ No newline at end of file