diff --git a/genotype_api/api/endpoints/samples.py b/genotype_api/api/endpoints/samples.py index c143661..73dcb20 100644 --- a/genotype_api/api/endpoints/samples.py +++ b/genotype_api/api/endpoints/samples.py @@ -1,78 +1,76 @@ from datetime import date +from http import HTTPStatus from typing import Literal from fastapi import APIRouter, Depends, Query from fastapi.responses import JSONResponse from sqlmodel import Session -from sqlmodel.sql.expression import Select, SelectOfScalar from starlette import status -from genotype_api.database.crud import create, delete from genotype_api.constants import Sexes, Types -from genotype_api.database.crud.read import ( - get_sample, - get_filtered_samples, - get_analyses_by_type_between_dates, - get_analysis_by_type_and_sample_id, -) -from genotype_api.database.crud.update import ( - refresh_sample_status, - update_sample_comment, - update_sample_status, - update_sample_sex, -) -from genotype_api.database.filter_models.sample_models import SampleFilterParams, SampleSexesUpdate +from genotype_api.database.filter_models.sample_models import SampleFilterParams from genotype_api.database.models import ( - Analysis, Sample, User, ) -from genotype_api.dto.dto import SampleRead, SampleReadWithAnalysisDeep from genotype_api.database.session_handler import get_session +from genotype_api.dto.sample import SampleResponse +from genotype_api.exceptions import SampleNotFoundError, SampleExistsError from genotype_api.models import MatchResult, SampleDetail from genotype_api.security import get_active_user -from genotype_api.services.match_genotype_service.match_genotype import MatchGenotypeService +from genotype_api.services.sample_service.sample_service import SampleService -SelectOfScalar.inherit_cache = True -Select.inherit_cache = True +router = APIRouter() -router = APIRouter() +def get_sample_service(session: Session = Depends(get_session)) -> SampleService: + return SampleService(session) @router.get( "/{sample_id}", - response_model=SampleReadWithAnalysisDeep, - response_model_by_alias=False, - response_model_exclude={ - "analyses": {"__all__": {"genotypes": True, "source": True, "created_at": True}}, - "detail": { - "sex": True, - "nocalls": True, - "snps": True, - "matches": True, - "mismatches": True, - "unknown": True, - }, - }, + response_model=SampleResponse, ) def read_sample( sample_id: str, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), + current_user: User = Depends(get_active_user), +): + try: + return sample_service.get_sample(sample_id) + except SampleNotFoundError: + return JSONResponse( + content=f"Sample with id: {sample_id} not found.", status_code=HTTPStatus.BAD_REQUEST + ) + + +@router.post( + "/", +) +def create_sample( + sample: Sample, + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), ): - sample: Sample = get_sample(session=session, sample_id=sample_id) - if len(sample.analyses) == 2 and not sample.status: - sample: Sample = refresh_sample_status(session=session, sample=sample) - return sample + try: + sample_service.create_sample(sample=sample) + new_sample: SampleResponse = sample_service.get_sample(sample_id=sample.id) + if not new_sample: + return JSONResponse( + content="Failed to create sample.", status_code=HTTPStatus.BAD_REQUEST + ) + return JSONResponse(f"Sample with id: {sample.id} was created.", status_code=HTTPStatus.OK) + except SampleExistsError: + return JSONResponse( + status_code=HTTPStatus.BAD_REQUEST, + content=f"Sample with id: {sample.id} already registered.", + ) @router.get( "/", - response_model=list[SampleReadWithAnalysisDeep], - response_model_by_alias=False, + response_model=list[SampleResponse], response_model_exclude={ - "analyses": {"__all__": {"genotypes": True, "source": True, "created_at": True}}, "detail": { "sex": True, "nocalls": True, @@ -91,9 +89,9 @@ def read_samples( incomplete: bool | None = False, commented: bool | None = False, status_missing: bool | None = False, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), -) -> list[Sample]: +): """Returns a list of samples matching the provided filters.""" filter_params = SampleFilterParams( sample_id=sample_id, @@ -104,56 +102,69 @@ def read_samples( skip=skip, limit=limit, ) - return get_filtered_samples(session=session, filter_params=filter_params) - -@router.post("/", response_model=SampleRead) -def create_sample( - sample: Sample, - session: Session = Depends(get_session), - current_user: User = Depends(get_active_user), -): - return create.create_sample(session=session, sample=sample) + return sample_service.get_samples(filter_params) -@router.put("/{sample_id}/sex", response_model=SampleRead) +@router.put("/{sample_id}/sex") def update_sex( sample_id: str, sex: Sexes = Query(...), genotype_sex: Sexes | None = None, sequence_sex: Sexes | None = None, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), ): """Updating sex field on sample and sample analyses.""" - sexes_update = SampleSexesUpdate( - sample_id=sample_id, sex=sex, genotype_sex=genotype_sex, sequence_sex=sequence_sex - ) - sample: Sample = update_sample_sex(session=session, sexes_update=sexes_update) - return sample - - -@router.put("/{sample_id}/comment", response_model=SampleRead) + try: + sample_service.set_sex( + sample_id=sample_id, sex=sex, genotype_sex=genotype_sex, sequence_sex=sequence_sex + ) + except SampleNotFoundError: + return JSONResponse( + content=f"Could not find sample with id: {sample_id}", + status_code=HTTPStatus.BAD_REQUEST, + ) + + +@router.put( + "/{sample_id}/comment", + response_model=SampleResponse, +) def update_comment( sample_id: str, comment: str = Query(...), - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), -) -> Sample: +): """Updating comment field on sample.""" - return update_sample_comment(session=session, sample_id=sample_id, comment=comment) - - -@router.put("/{sample_id}/status", response_model=SampleRead) + try: + return sample_service.set_sample_comment(sample_id=sample_id, comment=comment) + except SampleNotFoundError: + return JSONResponse( + content=f"Could not find sample with id: {sample_id}", + status_code=HTTPStatus.BAD_REQUEST, + ) + + +@router.put( + "/{sample_id}/status", + response_model=SampleResponse, +) def set_sample_status( sample_id: str, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), status: Literal["pass", "fail", "cancel"] | None = None, current_user: User = Depends(get_active_user), -) -> Sample: +): """Check sample analyses and update sample status accordingly.""" - - return update_sample_status(session=session, sample_id=sample_id, status=status) + try: + return sample_service.set_sample_status(sample_id=sample_id, status=status) + except SampleNotFoundError: + return JSONResponse( + content=f"Could not find sample with id: {sample_id}", + status_code=HTTPStatus.BAD_REQUEST, + ) @router.get("/{sample_id}/match", response_model=list[MatchResult]) @@ -163,20 +174,17 @@ def match( comparison_set: Types, date_min: date | None = date.min, date_max: date | None = date.max, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), ) -> list[MatchResult]: """Match sample genotype against all other genotypes.""" - analyses: list[Analysis] = get_analyses_by_type_between_dates( - session=session, analysis_type=comparison_set, date_max=date_max, date_min=date_min - ) - sample_analysis: Analysis = get_analysis_by_type_and_sample_id( - session=session, analysis_type=analysis_type, sample_id=sample_id - ) - matches: list[MatchResult] = MatchGenotypeService.get_matches( - analyses=analyses, sample_analysis=sample_analysis + return sample_service.get_match_results( + sample_id=sample_id, + analysis_type=analysis_type, + comparison_set=comparison_set, + date_max=date_max, + date_min=date_min, ) - return matches @router.get( @@ -187,25 +195,25 @@ def match( ) def get_status_detail( sample_id: str, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), ): - sample: Sample = get_sample(session=session, sample_id=sample_id) - if len(sample.analyses) != 2: - return SampleDetail() - return MatchGenotypeService.check_sample(sample=sample) + try: + return sample_service.get_status_detail(sample_id) + except SampleNotFoundError: + return JSONResponse( + content=f"Sample with id: {sample_id} not found.", status_code=HTTPStatus.BAD_REQUEST + ) -@router.delete("/{sample_id}", response_model=Sample) +@router.delete("/{sample_id}") def delete_sample( sample_id: str, - session: Session = Depends(get_session), + sample_service: SampleService = Depends(get_sample_service), current_user: User = Depends(get_active_user), ): """Delete sample and its Analyses.""" - - sample: Sample = get_sample(session=session, sample_id=sample_id) - for analysis in sample.analyses: - delete.delete_analysis(session=session, analysis=analysis) - delete.delete_sample(session=session, sample=sample) - return JSONResponse("Deleted", status_code=status.HTTP_200_OK) + sample_service.delete_sample(sample_id) + return JSONResponse( + content=f"Deleted sample with id: {sample_id}", status_code=status.HTTP_200_OK + ) diff --git a/genotype_api/database/crud/create.py b/genotype_api/database/crud/create.py index e4eb8af..1943ef3 100644 --- a/genotype_api/database/crud/create.py +++ b/genotype_api/database/crud/create.py @@ -5,8 +5,9 @@ from sqlmodel.sql.expression import Select, SelectOfScalar from genotype_api.database.models import Analysis, Plate, Sample, User, SNP -from genotype_api.dto.dto import UserCreate, PlateCreate +from genotype_api.dto.dto import PlateCreate from genotype_api.dto.user import UserRequest +from genotype_api.exceptions import SampleExistsError SelectOfScalar.inherit_cache = True Select.inherit_cache = True @@ -36,7 +37,7 @@ def create_sample(session: Session, sample: Sample) -> Sample: sample_in_db = session.get(Sample, sample.id) if sample_in_db: - raise HTTPException(status_code=409, detail="Sample already registered") + raise SampleExistsError session.add(sample) session.commit() session.refresh(sample) diff --git a/genotype_api/database/crud/read.py b/genotype_api/database/crud/read.py index fa6c913..9b643a4 100644 --- a/genotype_api/database/crud/read.py +++ b/genotype_api/database/crud/read.py @@ -1,6 +1,5 @@ import logging from datetime import timedelta, date -from typing import Callable, Sequence from sqlalchemy import func, desc, asc from sqlalchemy.orm import Query @@ -17,7 +16,6 @@ User, SNP, ) -from genotype_api.dto.dto import PlateReadWithAnalysisDetailSingle SelectOfScalar.inherit_cache = True Select.inherit_cache = True diff --git a/genotype_api/database/crud/update.py b/genotype_api/database/crud/update.py index fe5aa94..e52c8d4 100644 --- a/genotype_api/database/crud/update.py +++ b/genotype_api/database/crud/update.py @@ -10,6 +10,7 @@ from genotype_api.database.models import Sample, Plate, User from sqlmodel.sql.expression import Select, SelectOfScalar +from genotype_api.exceptions import SampleNotFoundError from genotype_api.services.match_genotype_service.match_genotype import MatchGenotypeService SelectOfScalar.inherit_cache = True @@ -31,6 +32,8 @@ def refresh_sample_status(sample: Sample, session: Session) -> Sample: def update_sample_comment(session: Session, sample_id: str, comment: str) -> Sample: sample: Sample = get_sample(session=session, sample_id=sample_id) + if not sample: + raise SampleNotFoundError sample.comment = comment session.add(sample) session.commit() @@ -40,6 +43,8 @@ def update_sample_comment(session: Session, sample_id: str, comment: str) -> Sam def update_sample_status(session: Session, sample_id: str, status: str | None) -> Sample: sample: Sample = get_sample(session=session, sample_id=sample_id) + if not sample: + raise SampleNotFoundError sample.status = status session.add(sample) session.commit() @@ -63,6 +68,8 @@ def update_plate_sign_off(session: Session, plate: Plate, plate_sign_off: PlateS def update_sample_sex(session: Session, sexes_update: SampleSexesUpdate) -> Sample: sample: Sample = get_sample(session=session, sample_id=sexes_update.sample_id) + if not sample: + raise SampleNotFoundError sample.sex = sexes_update.sex for analysis in sample.analyses: if sexes_update.genotype_sex and analysis.type == Types.GENOTYPE: diff --git a/genotype_api/dto/dto.py b/genotype_api/dto/dto.py index 1a889b0..86c8b49 100644 --- a/genotype_api/dto/dto.py +++ b/genotype_api/dto/dto.py @@ -1,136 +1,5 @@ -from collections import Counter - -from pydantic import constr, validator - -import genotype_api.database.models from genotype_api.database import models -from genotype_api.models import SampleDetail -from genotype_api.dto.plate import PlateStatusCounts -from genotype_api.services.match_genotype_service.utils import check_snps, check_sex - - -class GenotypeRead(models.GenotypeBase): - id: int - - -class GenotypeCreate(models.GenotypeBase): - pass - - -class AnalysisRead(models.AnalysisBase): - id: int - - -class AnalysisCreate(models.AnalysisBase): - pass - - -class SampleRead(genotype_api.database.models.SampleBase): - id: constr(max_length=32) - - -class SampleCreate(genotype_api.database.models.SampleBase): - pass - - -class SNPRead(models.SNPBase): - id: constr(max_length=32) - - -class UserRead(models.UserBase): - id: int - - -class UserCreate(models.UserBase): - pass - - -class PlateRead(models.PlateBase): - id: str - user: UserRead | None class PlateCreate(models.PlateBase): analyses: list[models.Analysis] | None = [] - - -class UserReadWithPlates(UserRead): - plates: list[models.Plate] | None = [] - - -class SampleReadWithAnalysis(SampleRead): - analyses: list[AnalysisRead] | None = [] - - -class AnalysisReadWithGenotype(AnalysisRead): - genotypes: list[models.Genotype] | None = [] - - -class SampleReadWithAnalysisDeep(SampleRead): - analyses: list[AnalysisReadWithGenotype] | None = [] - detail: SampleDetail | None - - @validator("detail") - def get_detail(cls, value, values) -> SampleDetail: - analyses = values.get("analyses") - if len(analyses) != 2: - return SampleDetail() - genotype_analysis = [analysis for analysis in analyses if analysis.type == "genotype"][0] - sequence_analysis = [analysis for analysis in analyses if analysis.type == "sequence"][0] - status = check_snps( - genotype_analysis=genotype_analysis, sequence_analysis=sequence_analysis - ) - sex = check_sex( - sample_sex=values.get("sex"), - genotype_analysis=genotype_analysis, - sequence_analysis=sequence_analysis, - ) - - return SampleDetail(**status, sex=sex) - - class Config: - validate_all = True - - -class AnalysisReadWithSample(AnalysisRead): - sample: models.SampleSlim | None - - -class AnalysisReadWithSampleDeep(AnalysisRead): - sample: SampleReadWithAnalysisDeep | None - - -class PlateReadWithAnalyses(PlateRead): - analyses: list[AnalysisReadWithSample] | None = [] - - -class PlateReadWithAnalysisDetail(PlateRead): - analyses: list[AnalysisReadWithSample] | None = [] - detail: PlateStatusCounts | None - - @validator("detail") - def check_detail(cls, value, values): - analyses = values.get("analyses") - statuses = [str(analysis.sample.status) for analysis in analyses] - commented = sum(1 for analysis in analyses if analysis.sample.comment) - status_counts = Counter(statuses) - return PlateStatusCounts(**status_counts, total=len(analyses), commented=commented) - - class Config: - validate_all = True - - -class PlateReadWithAnalysisDetailSingle(PlateRead): - analyses: list[AnalysisReadWithSample] | None = [] - detail: PlateStatusCounts | None - - @validator("detail") - def check_detail(cls, value, values): - analyses = values.get("analyses") - statuses = [str(analysis.sample.status) for analysis in analyses] - commented = sum(1 for analysis in analyses if analysis.sample.comment) - status_counts = Counter(statuses) - return PlateStatusCounts(**status_counts, total=len(analyses), commented=commented) - - class Config: - validate_all = True diff --git a/genotype_api/dto/genotype.py b/genotype_api/dto/genotype.py index ede217a..73b0e56 100644 --- a/genotype_api/dto/genotype.py +++ b/genotype_api/dto/genotype.py @@ -8,3 +8,7 @@ class GenotypeResponse(BaseModel): analysis_id: int allele_1: str = Field(max_length=1) allele_2: str = Field(max_length=1) + + @property + def alleles(self): + return sorted([self.allele_1, self.allele_2]) diff --git a/genotype_api/dto/plate.py b/genotype_api/dto/plate.py index eadac09..485402f 100644 --- a/genotype_api/dto/plate.py +++ b/genotype_api/dto/plate.py @@ -5,9 +5,7 @@ from pydantic import BaseModel, validator, Field, EmailStr -from genotype_api.constants import Types, Sexes - -from genotype_api.dto.sample import SampleStatusResponse +from genotype_api.constants import Types, Sexes, Status class PlateStatusCounts(BaseModel): @@ -28,6 +26,11 @@ class UserOnPlate(BaseModel): id: int | None = None +class SampleStatus(BaseModel): + status: Status | None = None + comment: str | None = None + + class AnalysisOnPlate(BaseModel): type: Types | None source: str | None @@ -36,7 +39,7 @@ class AnalysisOnPlate(BaseModel): sample_id: str | None plate_id: str | None id: int | None - sample: SampleStatusResponse | None = None + sample: SampleStatus | None = None class PlateResponse(BaseModel): diff --git a/genotype_api/dto/sample.py b/genotype_api/dto/sample.py index c05b8b1..98e4254 100644 --- a/genotype_api/dto/sample.py +++ b/genotype_api/dto/sample.py @@ -1,10 +1,52 @@ """Module for the sample DTOs.""" -from pydantic import BaseModel +from datetime import datetime +from pydantic import BaseModel, validator +from genotype_api.constants import Sexes, Status, Types +from genotype_api.dto.genotype import GenotypeResponse -from genotype_api.constants import Status +from genotype_api.models import SampleDetail +from genotype_api.services.match_genotype_service.utils import check_snps, check_sex -class SampleStatusResponse(BaseModel): - status: Status | None = None - comment: str | None = None +class AnalysisOnSample(BaseModel): + type: Types | None = None + sex: Sexes | None = None + sample_id: str | None = None + plate_id: str | None = None + id: int | None = None + genotypes: list[GenotypeResponse] + + +class SampleResponse(BaseModel): + id: str | None = None + status: Status | None + comment: str | None + sex: Sexes | None + created_at: datetime | None = datetime.now() + analyses: list[AnalysisOnSample] | None + detail: SampleDetail | None + + @validator("detail") + def get_detail(cls, value, values) -> SampleDetail | None: + analyses = values.get("analyses") + if analyses: + if len(analyses) != 2: + return SampleDetail() + genotype_analysis: list[AnalysisOnSample] = [ + analysis for analysis in analyses if analysis.type == "genotype" + ][0] + sequence_analysis: list[AnalysisOnSample] = [ + analysis for analysis in analyses if analysis.type == "sequence" + ][0] + status: dict = check_snps( + genotype_analysis=genotype_analysis, sequence_analysis=sequence_analysis + ) + sex: str = check_sex( + sample_sex=values.get("sex"), + genotype_analysis=genotype_analysis, + sequence_analysis=sequence_analysis, + ) + + return SampleDetail(**status, sex=sex) + return None diff --git a/genotype_api/exceptions.py b/genotype_api/exceptions.py index 2076dd4..711135e 100644 --- a/genotype_api/exceptions.py +++ b/genotype_api/exceptions.py @@ -37,5 +37,13 @@ class AnalysisNotFoundError(Exception): pass +class SampleNotFoundError(Exception): + pass + + +class SampleExistsError(Exception): + pass + + class SNPExistsError(Exception): pass diff --git a/genotype_api/models.py b/genotype_api/models.py index 5d13d6b..fbcfbda 100644 --- a/genotype_api/models.py +++ b/genotype_api/models.py @@ -2,28 +2,28 @@ class SampleDetailStats(BaseModel): - matches: int | None - mismatches: int | None - unknown: int | None + matches: int | None = None + mismatches: int | None = None + unknown: int | None = None class SampleDetailStatus(BaseModel): - sex: str | None - snps: str | None - nocalls: str | None + sex: str | None = None + snps: str | None = None + nocalls: str | None = None class SampleDetail(BaseModel): - sex: str | None - snps: str | None - nocalls: str | None - matches: int | None - mismatches: int | None - unknown: int | None - failed_snps: list[str] | None + sex: str | None = None + snps: str | None = None + nocalls: str | None = None + matches: int | None = None + mismatches: int | None = None + unknown: int | None = None + failed_snps: list[str] | None = None - stats: SampleDetailStats | None - status: SampleDetailStatus | None + stats: SampleDetailStats | None = None + status: SampleDetailStatus | None = None @validator("stats") def validate_stats(cls, value, values) -> SampleDetailStats: @@ -51,4 +51,4 @@ class MatchCounts(BaseModel): class MatchResult(BaseModel): sample_id: str - match_results: MatchCounts + match_results: MatchCounts | None = None diff --git a/genotype_api/services/plate_service/plate_service.py b/genotype_api/services/plate_service/plate_service.py index bd557f2..53c04dc 100644 --- a/genotype_api/services/plate_service/plate_service.py +++ b/genotype_api/services/plate_service/plate_service.py @@ -31,8 +31,7 @@ from genotype_api.database.filter_models.plate_models import PlateSignOff, PlateOrderParams from genotype_api.database.models import Plate, Analysis, User from genotype_api.dto.dto import PlateCreate -from genotype_api.dto.plate import PlateResponse, UserOnPlate, AnalysisOnPlate -from genotype_api.dto.sample import SampleStatusResponse +from genotype_api.dto.plate import PlateResponse, UserOnPlate, AnalysisOnPlate, SampleStatus from genotype_api.exceptions import PlateNotFoundError, UserNotFoundError from genotype_api.file_parsing.excel import GenotypeAnalysis from genotype_api.file_parsing.files import check_file @@ -48,7 +47,7 @@ def _get_analyses_on_plate(plate: Plate) -> list[AnalysisOnPlate] | None: analyses_response: list[AnalysisOnPlate] = [] for analysis in plate.analyses: if analysis: - sample_status = SampleStatusResponse( + sample_status = SampleStatus( status=analysis.sample.status, comment=analysis.sample.comment ) analysis_response = AnalysisOnPlate( diff --git a/genotype_api/services/sample_service/sample_service.py b/genotype_api/services/sample_service/sample_service.py new file mode 100644 index 0000000..02adf42 --- /dev/null +++ b/genotype_api/services/sample_service/sample_service.py @@ -0,0 +1,147 @@ +"""Module for the sample service.""" + +from datetime import date +from typing import Literal + +from sqlmodel import Session + +from genotype_api.constants import Types, Sexes +from genotype_api.database.crud.create import create_sample +from genotype_api.database.crud.delete import delete_analysis, delete_sample +from genotype_api.database.crud.read import ( + get_sample, + get_filtered_samples, + get_analysis_by_type_and_sample_id, + get_analyses_by_type_between_dates, +) +from genotype_api.database.crud.update import ( + refresh_sample_status, + update_sample_status, + update_sample_comment, + update_sample_sex, +) +from genotype_api.database.filter_models.sample_models import SampleFilterParams, SampleSexesUpdate +from genotype_api.database.models import Sample, Analysis +from genotype_api.dto.genotype import GenotypeResponse +from genotype_api.dto.sample import AnalysisOnSample, SampleResponse +from genotype_api.exceptions import SampleNotFoundError +from genotype_api.models import SampleDetail, MatchResult +from genotype_api.services.match_genotype_service.match_genotype import MatchGenotypeService + + +class SampleService: + + def __init__(self, session: Session): + self.session = session + + @staticmethod + def _get_genotype_on_analysis(analysis: Analysis) -> list[GenotypeResponse] | None: + genotypes: list[GenotypeResponse] = [] + if not analysis.genotypes: + return None + for genotype_on_analysis in analysis.genotypes: + genotype = GenotypeResponse( + rsnumber=genotype_on_analysis.rsnumber, + analysis_id=genotype_on_analysis.analysis_id, + allele_1=genotype_on_analysis.allele_1, + allele_2=genotype_on_analysis.allele_1, + ) + genotypes.append(genotype) + return genotypes + + def _get_analyses_on_sample(self, sample: Sample) -> list[AnalysisOnSample] | None: + analyses: list[AnalysisOnSample] = [] + if not sample.analyses: + return None + for analysis in sample.analyses: + genotypes: list[GenotypeResponse] = self._get_genotype_on_analysis(analysis) + analysis_on_sample = AnalysisOnSample( + type=analysis.type, + sex=analysis.sex, + sample_id=analysis.sample_id, + plate_id=analysis.plate_id, + id=analysis.id, + genotypes=genotypes, + ) + analyses.append(analysis_on_sample) + return analyses + + def _get_sample_response(self, sample: Sample) -> SampleResponse: + analyses: list[AnalysisOnSample] = self._get_analyses_on_sample(sample=sample) + return SampleResponse( + id=sample.id, + status=sample.status, + comment=sample.comment, + sex=sample.sex, + created_at=sample.created_at, + analyses=analyses, + ) + + def get_sample(self, sample_id: str) -> SampleResponse: + sample: Sample = get_sample(session=self.session, sample_id=sample_id) + if not sample: + raise SampleNotFoundError + if len(sample.analyses) == 2 and not sample.status: + sample: Sample = refresh_sample_status(session=self.session, sample=sample) + return self._get_sample_response(sample) + + def get_samples(self, filter_params: SampleFilterParams) -> list[SampleResponse]: + samples: list[Sample] = get_filtered_samples( + session=self.session, filter_params=filter_params + ) + return [self._get_sample_response(sample) for sample in samples] + + def create_sample(self, sample: Sample) -> None: + create_sample(session=self.session, sample=sample) + + def delete_sample(self, sample_id: str) -> None: + sample: Sample = get_sample(session=self.session, sample_id=sample_id) + for analysis in sample.analyses: + delete_analysis(session=self.session, analysis=analysis) + delete_sample(session=self.session, sample=sample) + + def get_status_detail(self, sample_id: str) -> SampleDetail: + sample: Sample = get_sample(session=self.session, sample_id=sample_id) + if len(sample.analyses) != 2: + return SampleDetail() + return MatchGenotypeService.check_sample(sample=sample) + + def get_match_results( + self, + sample_id: str, + analysis_type: Types, + comparison_set: Types, + date_min: date, + date_max: date, + ) -> list[MatchResult]: + """Get the match results for an analysis type and the comparison type in a given time frame.""" + analyses: list[Analysis] = get_analyses_by_type_between_dates( + session=self.session, analysis_type=comparison_set, date_max=date_max, date_min=date_min + ) + sample_analysis: Analysis = get_analysis_by_type_and_sample_id( + session=self.session, analysis_type=analysis_type, sample_id=sample_id + ) + matches: list[MatchResult] = MatchGenotypeService.get_matches( + analyses=analyses, sample_analysis=sample_analysis + ) + return matches + + def set_sample_status( + self, sample_id: str, status: Literal["pass", "fail", "cancel"] | None + ) -> SampleResponse: + sample: Sample = update_sample_status( + session=self.session, sample_id=sample_id, status=status + ) + return self._get_sample_response(sample) + + def set_sample_comment(self, sample_id: str, comment: str) -> SampleResponse: + sample: Sample = update_sample_comment( + session=self.session, sample_id=sample_id, comment=comment + ) + return self._get_sample_response(sample) + + def set_sex(self, sample_id: str, sex: Sexes, genotype_sex: Sexes, sequence_sex: Sexes) -> None: + sexes_update = SampleSexesUpdate( + sample_id=sample_id, sex=sex, genotype_sex=genotype_sex, sequence_sex=sequence_sex + ) + update_sample_sex(session=self.session, sexes_update=sexes_update)