Skip to content
Open
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
1 change: 1 addition & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion api/router.py
Original file line number Diff line number Diff line change
@@ -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"])
51 changes: 51 additions & 0 deletions api/routes/project.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
66 changes: 66 additions & 0 deletions schemas/project.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 1 addition & 2 deletions schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 98 additions & 0 deletions services/project_service.py
Original file line number Diff line number Diff line change
@@ -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