diff --git a/genotype_api/api/endpoints/analyses.py b/genotype_api/api/endpoints/analyses.py index a50fd4f..327d2b5 100644 --- a/genotype_api/api/endpoints/analyses.py +++ b/genotype_api/api/endpoints/analyses.py @@ -1,6 +1,8 @@ """Routes for analysis""" -from fastapi import APIRouter, Depends, File, Query, UploadFile, status +from http import HTTPStatus + +from fastapi import APIRouter, Depends, File, Query, UploadFile, status, HTTPException from fastapi.responses import JSONResponse from sqlmodel import Session @@ -9,6 +11,7 @@ from genotype_api.dto.analysis import AnalysisResponse from genotype_api.database.session_handler import get_session +from genotype_api.exceptions import AnalysisNotFoundError from genotype_api.security import get_active_user from genotype_api.services.analysis_service.analysis_service import AnalysisService @@ -26,7 +29,13 @@ def read_analysis( current_user: User = Depends(get_active_user), ): """Return analysis.""" - return analysis_service.get_analysis_with_genotype(analysis_id) + try: + return analysis_service.get_analysis(analysis_id) + except AnalysisNotFoundError: + raise HTTPException( + detail=f"Could not find analysis with id: {analysis_id}", + status_code=HTTPStatus.BAD_REQUEST, + ) @router.get("/", response_model=list[AnalysisResponse], response_model_exclude={"genotypes"}) @@ -37,7 +46,13 @@ def read_analyses( current_user: User = Depends(get_active_user), ): """Return all analyses.""" - return analysis_service.get_analyses_to_display(skip=skip, limit=limit) + try: + return analysis_service.get_analyses(skip=skip, limit=limit) + except AnalysisNotFoundError: + raise HTTPException( + detail="Could not fetch analyses from backend.", + status_code=HTTPStatus.BAD_REQUEST, + ) @router.delete("/{analysis_id}") @@ -46,9 +61,15 @@ def delete_analysis( analysis_service: AnalysisService = Depends(get_analysis_service), current_user: User = Depends(get_active_user), ): - """Delete analysis based on analysis_id""" - analysis_service.delete_analysis(analysis_id) - return JSONResponse(f"Deleted analysis: {analysis_id}", status_code=status.HTTP_200_OK) + """Delete analysis based on analysis id.""" + try: + analysis_service.delete_analysis(analysis_id) + except AnalysisNotFoundError: + raise HTTPException( + detail=f"Could not find analysis with id: {analysis_id}", + status_code=HTTPStatus.BAD_REQUEST, + ) + return JSONResponse(content=f"Deleted analysis: {analysis_id}", status_code=status.HTTP_200_OK) @router.post( diff --git a/genotype_api/api/endpoints/plates.py b/genotype_api/api/endpoints/plates.py index 469f5f7..a8038d0 100644 --- a/genotype_api/api/endpoints/plates.py +++ b/genotype_api/api/endpoints/plates.py @@ -1,13 +1,15 @@ """Routes for plates""" +from http import HTTPStatus from typing import Literal -from fastapi import APIRouter, Depends, File, Query, UploadFile, status +from fastapi import APIRouter, Depends, File, Query, UploadFile, status, HTTPException from fastapi.responses import JSONResponse from sqlmodel import Session from genotype_api.database.filter_models.plate_models import PlateOrderParams from genotype_api.database.models import User from genotype_api.database.session_handler import get_session from genotype_api.dto.plate import PlateResponse +from genotype_api.exceptions import PlateNotFoundError from genotype_api.security import get_active_user from genotype_api.services.plate_service.plate_service import PlateService @@ -85,8 +87,12 @@ def read_plate( current_user: User = Depends(get_active_user), ): """Display information about a plate.""" - - return plate_service.read_plate(plate_id=plate_id) + try: + return plate_service.get_plate(plate_id=plate_id) + except PlateNotFoundError: + raise HTTPException( + detail=f"Could not find plate with id: {plate_id}", status_code=HTTPStatus.BAD_REQUEST + ) @router.get( @@ -107,7 +113,12 @@ async def read_plates( order_params = PlateOrderParams( order_by=order_by, skip=skip, limit=limit, sort_order=sort_order ) - return plate_service.read_plates(order_params=order_params) + try: + return plate_service.get_plates(order_params=order_params) + except PlateNotFoundError: + raise HTTPException( + detail="Could not fetch plates from backend.", status_code=HTTPStatus.BAD_REQUEST + ) @router.delete("/{plate_id}") @@ -117,8 +128,13 @@ def delete_plate( current_user: User = Depends(get_active_user), ): """Delete plate.""" - analysis_ids = plate_service.delete_plate(plate_id) - return JSONResponse( - f"Deleted plate: {plate_id} and analyses: {analysis_ids}", - status_code=status.HTTP_200_OK, - ) + try: + analysis_ids = plate_service.delete_plate(plate_id) + return JSONResponse( + f"Deleted plate: {plate_id} and analyses: {analysis_ids}", + status_code=status.HTTP_200_OK, + ) + except PlateNotFoundError: + raise HTTPException( + detail=f"Could not find plate with id: {plate_id}", status_code=HTTPStatus.BAD_REQUEST + ) diff --git a/genotype_api/api/endpoints/users.py b/genotype_api/api/endpoints/users.py index 48db70d..2977478 100644 --- a/genotype_api/api/endpoints/users.py +++ b/genotype_api/api/endpoints/users.py @@ -1,90 +1,87 @@ """Routes for users""" -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, Query, HTTPException from pydantic import EmailStr from sqlmodel import Session -from sqlmodel.sql.expression import Select, SelectOfScalar from starlette import status + from starlette.responses import JSONResponse -from genotype_api.database.crud.read import ( - get_user_by_id, - get_user_by_email, - get_users_with_skip_and_limit, -) -from genotype_api.database.crud.update import update_user_email -from genotype_api.database.crud import delete, create from genotype_api.database.models import User -from genotype_api.dto.dto import UserRead, UserCreate, UserReadWithPlates from genotype_api.database.session_handler import get_session +from genotype_api.dto.user import UserRequest, UserResponse +from genotype_api.exceptions import UserNotFoundError, UserArchiveError, UserExistsError from genotype_api.security import get_active_user +from genotype_api.services.user_service.user_service import UserService -SelectOfScalar.inherit_cache = True -Select.inherit_cache = True +router = APIRouter() -router = APIRouter() +def get_user_service(session: Session = Depends(get_session)) -> UserService: + return UserService(session) -@router.get("/{user_id}", response_model=UserReadWithPlates) +@router.get("/{user_id}", response_model=UserResponse) def read_user( user_id: int, - session: Session = Depends(get_session), + user_service: UserService = Depends(get_user_service), current_user: User = Depends(get_active_user), -) -> User: - return get_user_by_id(session=session, user_id=user_id) +) -> UserResponse: + try: + return user_service.get_user(user_id) + except UserNotFoundError: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") @router.delete("/{user_id}") def delete_user( user_id: int, - session: Session = Depends(get_session), + user_service: UserService = Depends(get_user_service), current_user: User = Depends(get_active_user), ) -> JSONResponse: - user: User = get_user_by_id(session=session, user_id=user_id) - if not user: + try: + user_service.delete_user(user_id) + except UserNotFoundError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - if user.plates: - raise HTTPException( + except UserArchiveError: + HTTPException( status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="User previously signed plates, please archive instead", ) - delete.delete_user(session=session, user=user) - return JSONResponse(content="User deleted successfully", status_code=status.HTTP_200_OK) + return JSONResponse(content=f"Deleted user with id: {user_id}.", status_code=status.HTTP_200_OK) -@router.put("/{user_id}/email", response_model=User) +@router.put("/{user_id}/email", response_model=UserResponse, response_model_exclude={"plates"}) def change_user_email( user_id: int, email: EmailStr, - session: Session = Depends(get_session), + user_service: UserService = Depends(get_user_service), current_user: User = Depends(get_active_user), -) -> User: - user: User = get_user_by_id(session=session, user_id=user_id) - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") - user: User = update_user_email(session=session, user=user, email=email) - return user +) -> UserResponse: + try: + return user_service.update_user_email(user_id=user_id, email=email) + except UserNotFoundError: + HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") -@router.get("/", response_model=list[UserRead]) +@router.get("/", response_model=list[UserResponse], response_model_exclude={"plates"}) def read_users( skip: int = 0, limit: int = Query(default=100, lte=100), - session: Session = Depends(get_session), + user_service: UserService = Depends(get_user_service), current_user: User = Depends(get_active_user), -) -> list[User]: - return get_users_with_skip_and_limit(session=session, skip=skip, limit=limit) +) -> list[UserResponse]: + + return user_service.get_users(skip=skip, limit=limit) -@router.post("/", response_model=User) +@router.post("/", response_model=UserResponse, response_model_exclude={"plates"}) def create_user( - user: UserCreate, - session: Session = Depends(get_session), + user: UserRequest, + user_service: UserService = Depends(get_user_service), current_user: User = Depends(get_active_user), ): - existing_user: User = get_user_by_email(session=session, email=user.email) - if existing_user: - raise HTTPException(status_code=409, detail="Email already registered") - new_user: User = create.create_user(session=session, user=user) - return new_user + try: + return user_service.create_user(user) + except UserExistsError: + HTTPException(status_code=409, detail="Email already registered.") diff --git a/genotype_api/database/crud/create.py b/genotype_api/database/crud/create.py index 57cbcaf..e4eb8af 100644 --- a/genotype_api/database/crud/create.py +++ b/genotype_api/database/crud/create.py @@ -6,6 +6,7 @@ from genotype_api.database.models import Analysis, Plate, Sample, User, SNP from genotype_api.dto.dto import UserCreate, PlateCreate +from genotype_api.dto.user import UserRequest SelectOfScalar.inherit_cache = True Select.inherit_cache = True @@ -51,7 +52,7 @@ def create_analyses_samples(session: Session, analyses: list[Analysis]) -> list[ ] -def create_user(session: Session, user: UserCreate): +def create_user(session: Session, user: UserRequest): db_user = User.from_orm(user) session.add(db_user) session.commit() diff --git a/genotype_api/dto/analysis.py b/genotype_api/dto/analysis.py index 73dbb3b..3ac0b4b 100644 --- a/genotype_api/dto/analysis.py +++ b/genotype_api/dto/analysis.py @@ -6,7 +6,6 @@ from genotype_api.constants import Sexes, Types from genotype_api.dto.genotype import GenotypeResponse -from genotype_api.dto.sample import SampleStatusResponse class AnalysisResponse(BaseModel): @@ -18,14 +17,3 @@ class AnalysisResponse(BaseModel): plate_id: str | None id: int | None genotypes: list[GenotypeResponse] | None = None - - -class AnalysisSampleResponse(BaseModel): - type: Types | None - source: str | None - sex: Sexes | None - created_at: datetime | None - sample_id: str | None - plate_id: str | None - id: int | None - sample: SampleStatusResponse | None = None diff --git a/genotype_api/dto/plate.py b/genotype_api/dto/plate.py index 057b038..eadac09 100644 --- a/genotype_api/dto/plate.py +++ b/genotype_api/dto/plate.py @@ -3,9 +3,11 @@ from collections import Counter from datetime import datetime -from pydantic import BaseModel, validator, Field -from genotype_api.dto.analysis import AnalysisSampleResponse -from genotype_api.dto.user import UserResponse +from pydantic import BaseModel, validator, Field, EmailStr + +from genotype_api.constants import Types, Sexes + +from genotype_api.dto.sample import SampleStatusResponse class PlateStatusCounts(BaseModel): @@ -20,6 +22,23 @@ class Config: allow_population_by_field_name = True +class UserOnPlate(BaseModel): + email: EmailStr | None = None + name: str | None = None + id: int | None = None + + +class AnalysisOnPlate(BaseModel): + type: Types | None + source: str | None + sex: Sexes | None + created_at: datetime | None + sample_id: str | None + plate_id: str | None + id: int | None + sample: SampleStatusResponse | None = None + + class PlateResponse(BaseModel): created_at: datetime | None = None plate_id: str | None = None @@ -28,8 +47,8 @@ class PlateResponse(BaseModel): method_document: str | None = None method_version: str | None = None id: str | None = None - user: UserResponse | None = None - analyses: list[AnalysisSampleResponse] | None = None + user: UserOnPlate | None = None + analyses: list[AnalysisOnPlate] | None = None plate_status_counts: PlateStatusCounts | None = None @validator("plate_status_counts") diff --git a/genotype_api/dto/user.py b/genotype_api/dto/user.py index 1556680..d9c4221 100644 --- a/genotype_api/dto/user.py +++ b/genotype_api/dto/user.py @@ -1,9 +1,25 @@ """Module for the plate DTOs.""" +from datetime import datetime + from pydantic import BaseModel, EmailStr +class PlateOnUser(BaseModel): + created_at: datetime | None = None + plate_id: str | None = None + signed_by: int | None = None + signed_at: datetime | None = None + id: str | None = None + + class UserResponse(BaseModel): email: EmailStr | None = None name: str | None = None id: int | None = None + plates: list[PlateOnUser] | None = None + + +class UserRequest(BaseModel): + email: EmailStr + name: str diff --git a/genotype_api/exceptions.py b/genotype_api/exceptions.py index ea8135d..0d1b891 100644 --- a/genotype_api/exceptions.py +++ b/genotype_api/exceptions.py @@ -11,3 +11,27 @@ class UnknownAllelesError(Exception): class InsufficientAnalysesError(Exception): pass + + +class PlateExistsError(Exception): + pass + + +class PlateNotFoundError(Exception): + pass + + +class UserNotFoundError(Exception): + pass + + +class UserArchiveError(Exception): + pass + + +class UserExistsError(Exception): + pass + + +class AnalysisNotFoundError(Exception): + pass diff --git a/genotype_api/services/analysis_service/analysis_service.py b/genotype_api/services/analysis_service/analysis_service.py index 948e6c5..4e33bf8 100644 --- a/genotype_api/services/analysis_service/analysis_service.py +++ b/genotype_api/services/analysis_service/analysis_service.py @@ -17,6 +17,7 @@ from genotype_api.database.models import Analysis from genotype_api.dto.analysis import AnalysisResponse +from genotype_api.exceptions import AnalysisNotFoundError from genotype_api.file_parsing.files import check_file from genotype_api.file_parsing.vcf import SequenceAnalysis @@ -28,15 +29,32 @@ class AnalysisService: def __init__(self, session: Session): self.session: Session = session - def get_analysis_with_genotype(self, analysis_id: int) -> AnalysisResponse: + @staticmethod + def _create_analysis_response(analysis: Analysis) -> AnalysisResponse: + return AnalysisResponse( + type=analysis.type, + source=analysis.source, + sex=analysis.sex, + created_at=analysis.created_at, + sample_id=analysis.sample_id, + plate_id=analysis.plate_id, + id=analysis.id, + genotypes=analysis.genotypes, + ) + + def get_analysis(self, analysis_id: int) -> AnalysisResponse: analysis: Analysis = get_analysis_by_id(session=self.session, analysis_id=analysis_id) - return self._get_analyses_response(analysis) + if not analysis: + raise AnalysisNotFoundError + return self._create_analysis_response(analysis) - def get_analyses_to_display(self, skip: int, limit: int) -> list[AnalysisResponse]: + def get_analyses(self, skip: int, limit: int) -> list[AnalysisResponse]: analyses: list[Analysis] = get_analyses_with_skip_and_limit( session=self.session, skip=skip, limit=limit ) - return [self._get_analyses_response(analysis) for analysis in analyses] + if not analyses: + raise AnalysisNotFoundError + return [self._create_analysis_response(analysis) for analysis in analyses] def get_upload_sequence_analyses(self, file: UploadFile) -> list[AnalysisResponse]: """ @@ -54,21 +72,10 @@ def get_upload_sequence_analyses(self, file: UploadFile) -> list[AnalysisRespons analysis: Analysis = create_analysis(session=self.session, analysis=analysis) refresh_sample_status(session=self.session, sample=analysis.sample) - return [self._get_analyses_response(analysis) for analysis in analyses] - - @staticmethod - def _get_analyses_response(analysis: Analysis) -> AnalysisResponse: - return AnalysisResponse( - type=analysis.type, - source=analysis.source, - sex=analysis.sex, - created_at=analysis.created_at, - sample_id=analysis.sample_id, - plate_id=analysis.plate_id, - id=analysis.id, - genotypes=analysis.genotypes, - ) + return [self._create_analysis_response(analysis) for analysis in analyses] def delete_analysis(self, analysis_id: int) -> None: analysis: Analysis = get_analysis_by_id(session=self.session, analysis_id=analysis_id) + if not analysis: + raise AnalysisNotFoundError delete_analysis(session=self.session, analysis=analysis) diff --git a/genotype_api/services/plate_service/plate_service.py b/genotype_api/services/plate_service/plate_service.py index b88cef0..bd557f2 100644 --- a/genotype_api/services/plate_service/plate_service.py +++ b/genotype_api/services/plate_service/plate_service.py @@ -30,11 +30,10 @@ ) from genotype_api.database.filter_models.plate_models import PlateSignOff, PlateOrderParams from genotype_api.database.models import Plate, Analysis, User -from genotype_api.dto.analysis import AnalysisSampleResponse from genotype_api.dto.dto import PlateCreate -from genotype_api.dto.plate import PlateResponse +from genotype_api.dto.plate import PlateResponse, UserOnPlate, AnalysisOnPlate from genotype_api.dto.sample import SampleStatusResponse -from genotype_api.dto.user import UserResponse +from genotype_api.exceptions import PlateNotFoundError, UserNotFoundError from genotype_api.file_parsing.excel import GenotypeAnalysis from genotype_api.file_parsing.files import check_file @@ -45,14 +44,14 @@ def __init__(self, session: Session): self.session: Session = session @staticmethod - def _get_analyses_on_plate(plate: Plate) -> list[AnalysisSampleResponse] | None: - analyses_response: list[AnalysisSampleResponse] = [] + def _get_analyses_on_plate(plate: Plate) -> list[AnalysisOnPlate] | None: + analyses_response: list[AnalysisOnPlate] = [] for analysis in plate.analyses: if analysis: sample_status = SampleStatusResponse( status=analysis.sample.status, comment=analysis.sample.comment ) - analysis_response = AnalysisSampleResponse( + analysis_response = AnalysisOnPlate( type=analysis.type, source=analysis.source, sex=analysis.sex, @@ -65,15 +64,15 @@ def _get_analyses_on_plate(plate: Plate) -> list[AnalysisSampleResponse] | None: analyses_response.append(analysis_response) return analyses_response if analyses_response else None - def _get_plate_user(self, plate: Plate) -> UserResponse | None: + def _get_plate_user(self, plate: Plate) -> UserOnPlate | None: if plate.signed_by: user: User = get_user_by_id(session=self.session, user_id=plate.signed_by) - return UserResponse(email=user.email, name=user.name, id=user.id) + return UserOnPlate(email=user.email, name=user.name, id=user.id) return None - def _get_plate_response(self, plate: Plate) -> PlateResponse: - analyses_response: list[AnalysisSampleResponse] = self._get_analyses_on_plate(plate) - user: UserResponse = self._get_plate_user(plate) + def _create_plate_response(self, plate: Plate) -> PlateResponse: + analyses_response: list[AnalysisOnPlate] = self._get_analyses_on_plate(plate) + user: UserOnPlate = self._get_plate_user(plate) return PlateResponse( created_at=plate.created_at, plate_id=plate.plate_id, @@ -118,13 +117,17 @@ def upload_plate(self, file: UploadFile) -> PlateResponse: refresh_sample_status(sample=analysis.sample, session=self.session) refresh_plate(session=self.session, plate=plate) - return self._get_plate_response(plate) + return self._create_plate_response(plate) def update_plate_sign_off( self, plate_id: int, user_email: EmailStr, method_document: str, method_version: str ) -> PlateResponse: plate: Plate = get_plate_by_id(session=self.session, plate_id=plate_id) + if not plate: + raise PlateNotFoundError user: User = get_user_by_email(session=self.session, email=user_email) + if not user: + raise UserNotFoundError plate_sign_off = PlateSignOff( user_id=user.id, signed_at=datetime.now(), @@ -132,19 +135,25 @@ def update_plate_sign_off( method_version=method_version, ) update_plate_sign_off(session=self.session, plate=plate, plate_sign_off=plate_sign_off) - return self._get_plate_response(plate) + return self._create_plate_response(plate) - def read_plate(self, plate_id: int) -> PlateResponse: + def get_plate(self, plate_id: int) -> PlateResponse: plate: Plate = get_plate_by_id(session=self.session, plate_id=plate_id) - return self._get_plate_response(plate) + if not plate: + raise PlateNotFoundError + return self._create_plate_response(plate) - def read_plates(self, order_params: PlateOrderParams) -> list[PlateResponse]: + def get_plates(self, order_params: PlateOrderParams) -> list[PlateResponse]: plates: list[Plate] = get_ordered_plates(session=self.session, order_params=order_params) - return [self._get_plate_response(plate) for plate in plates] + if not plates: + raise PlateNotFoundError + return [self._create_plate_response(plate) for plate in plates] def delete_plate(self, plate_id) -> list[int]: """Delete a plate with the given plate id and return associated analysis ids.""" plate = get_plate_by_id(session=self.session, plate_id=plate_id) + if not plate: + raise PlateNotFoundError analyses: list[Analysis] = get_analyses_from_plate(session=self.session, plate_id=plate_id) analysis_ids: list[int] = [analyse.id for analyse in analyses] for analysis in analyses: diff --git a/genotype_api/services/user_service/user_service.py b/genotype_api/services/user_service/user_service.py new file mode 100644 index 0000000..7c46afe --- /dev/null +++ b/genotype_api/services/user_service/user_service.py @@ -0,0 +1,75 @@ +"""Module to holds the user service.""" + +from pydantic import EmailStr +from sqlmodel import Session +from genotype_api.database.crud.create import create_user +from genotype_api.database.crud.delete import delete_user +from genotype_api.database.crud.read import ( + get_user_by_id, + get_users_with_skip_and_limit, + get_user_by_email, +) +from genotype_api.database.crud.update import update_user_email +from genotype_api.database.models import User +from genotype_api.dto.user import UserResponse, UserRequest, PlateOnUser +from genotype_api.exceptions import UserNotFoundError, UserArchiveError, UserExistsError + + +class UserService: + + def __init__(self, session: Session): + self.session: Session = session + + @staticmethod + def _get_plates_on_user(user: User) -> list[PlateOnUser] | None: + plates_response: list[PlateOnUser] = [] + if not user.plates: + return None + for plate in user.plates: + plate_response = PlateOnUser( + created_at=plate.created_at, + plate_id=plate.plate_id, + id=plate.id, + signed_by=plate.signed_by, + signed_at=plate.signed_at, + ) + plates_response.append(plate_response) + return plates_response + + def _create_user_response(self, user: User) -> UserResponse: + plates: list[PlateOnUser] = self._get_plates_on_user(user) + return UserResponse(email=user.email, name=user.name, id=user.id, plates=plates) + + def create_user(self, user: UserRequest): + existing_user: User = get_user_by_email(session=self.session, email=user.email) + if existing_user: + raise UserExistsError + new_user: User = create_user(session=self.session, user=user) + return self._create_user_response(new_user) + + def get_users(self, skip: int, limit: int) -> list[UserResponse]: + users: list[User] = get_users_with_skip_and_limit( + session=self.session, skip=skip, limit=limit + ) + return [self._create_user_response(user) for user in users] + + def get_user(self, user_id: int) -> UserResponse: + user: User = get_user_by_id(session=self.session, user_id=user_id) + if not user: + raise UserNotFoundError + return self._create_user_response(user) + + def delete_user(self, user_id: int): + user: User = get_user_by_id(session=self.session, user_id=user_id) + if not user: + raise UserNotFoundError + if user.plates: + raise UserArchiveError + delete_user(session=self.session, user=user) + + def update_user_email(self, user_id: int, email: EmailStr): + user: User = get_user_by_id(session=self.session, user_id=user_id) + if not user: + raise UserNotFoundError + user: User = update_user_email(session=self.session, user=user, email=email) + return self._create_user_response(user)