diff --git a/genotype_api/api/endpoints/analyses.py b/genotype_api/api/endpoints/analyses.py index 8215456..7ca1d78 100644 --- a/genotype_api/api/endpoints/analyses.py +++ b/genotype_api/api/endpoints/analyses.py @@ -9,7 +9,7 @@ get_analysis_by_id, ) from genotype_api.database.models import Analysis, User -from genotype_api.dto.analysis import AnalysisWithGenotypeResponse, AnalysisResponse +from genotype_api.dto.analysis import AnalysisGenotypeResponse, AnalysisResponse from genotype_api.database.session_handler import get_session from genotype_api.security import get_active_user from genotype_api.services.analysis_service.analysis_service import AnalysisService @@ -17,7 +17,7 @@ router = APIRouter() -@router.get("/{analysis_id}", response_model=AnalysisWithGenotypeResponse) +@router.get("/{analysis_id}", response_model=AnalysisGenotypeResponse) def read_analysis( analysis_id: int, session: Session = Depends(get_session), diff --git a/genotype_api/api/endpoints/plates.py b/genotype_api/api/endpoints/plates.py index 95583d1..e417435 100644 --- a/genotype_api/api/endpoints/plates.py +++ b/genotype_api/api/endpoints/plates.py @@ -1,117 +1,65 @@ """Routes for plates""" -from datetime import datetime -from io import BytesIO -from pathlib import Path -from typing import Literal, Sequence - -from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status +from typing import Literal +from fastapi import APIRouter, Depends, File, Query, UploadFile, status from fastapi.responses import JSONResponse -from sqlalchemy import asc, desc from sqlmodel import Session -from sqlmodel.sql.expression import Select, SelectOfScalar - -from genotype_api.database.crud.create import create_analyses_samples, create_plate -from genotype_api.database.crud.delete import delete_analysis -from genotype_api.database.crud.read import ( - check_analyses_objects, - get_analyses_from_plate, - get_plate, - get_plate_read_analysis_single, - get_ordered_plates, - get_user_by_email, -) -from genotype_api.database.crud.update import ( - refresh_sample_status, - refresh_plate, - update_plate_sign_off, -) -from genotype_api.database.filter_models.plate_models import PlateSignOff, PlateOrderParams -from genotype_api.database.models import ( - Analysis, - Plate, - User, -) -from genotype_api.dto.dto import ( - PlateCreate, - PlateReadWithAnalyses, - PlateReadWithAnalysisDetail, - PlateReadWithAnalysisDetailSingle, -) +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.file_parsing.excel import GenotypeAnalysis -from genotype_api.file_parsing.files import check_file +from genotype_api.dto.plate import PlateResponse from genotype_api.security import get_active_user +from genotype_api.services.plate_service.plate_service import PlateService -SelectOfScalar.inherit_cache = True -Select.inherit_cache = True router = APIRouter() -def get_plate_id_from_file(file_name: Path) -> str: - # Get the plate id from the standardized name of the plate - return file_name.name.split("_", 1)[0] +def get_plate_service(session: Session = Depends(get_session)) -> PlateService: + return PlateService(session) -@router.post("/plate", response_model=PlateReadWithAnalyses) +@router.post( + "/plate", + response_model=PlateResponse, + response_model_exclude={"detail"}, +) def upload_plate( file: UploadFile = File(...), - session: Session = Depends(get_session), + plate_service: PlateService = Depends(get_plate_service), current_user: User = Depends(get_active_user), ): - file_name: Path = check_file(file_path=file.filename, extension=".xlsx") - plate_id: str = get_plate_id_from_file(file_name) - db_plate = session.get(Plate, plate_id) - if db_plate: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Plate with id {db_plate.id} already exists", - ) - - excel_parser = GenotypeAnalysis( - excel_file=BytesIO(file.file.read()), - file_name=str(file_name), - include_key="-CG-", - ) - analyses: list[Analysis] = list(excel_parser.generate_analyses()) - check_analyses_objects(session=session, analyses=analyses, analysis_type="genotype") - create_analyses_samples(session=session, analyses=analyses) - plate_obj = PlateCreate(plate_id=plate_id) - plate_obj.analyses = analyses - plate: Plate = create_plate(session=session, plate=plate_obj) - for analysis in plate.analyses: - refresh_sample_status(sample=analysis.sample, session=session) - refresh_plate(session=session, plate=plate) - return plate - - -@router.patch("/{plate_id}/sign-off", response_model=Plate) + return plate_service.upload_plate(file) + + +@router.patch( + "/{plate_id}/sign-off", + response_model=PlateResponse, + response_model_exclude={"analyses", "user", "detail"}, +) def sign_off_plate( plate_id: int, method_document: str = Query(...), method_version: str = Query(...), - session: Session = Depends(get_session), + plate_service: PlateService = Depends(get_plate_service), current_user: User = Depends(get_active_user), ): """Sign off a plate. This means that current User sign off that the plate is checked Add Depends with current user """ - user: User = get_user_by_email(session=session, email=current_user.email) - plate: Plate = get_plate(session=session, plate_id=plate_id) - plate_sign_off = PlateSignOff( - user_id=user.id, - signed_at=datetime.now(), - method_document=method_document, + + return plate_service.update_plate_sign_off( + plate_id=plate_id, + user_email=current_user.email, method_version=method_version, + method_document=method_document, ) - return update_plate_sign_off(session=session, plate=plate, plate_sign_off=plate_sign_off) @router.get( "/{plate_id}", - response_model=PlateReadWithAnalysisDetailSingle, + response_model=PlateResponse, response_model_by_alias=False, response_model_exclude={ "analyses": { @@ -133,16 +81,17 @@ def sign_off_plate( ) def read_plate( plate_id: int, - session: Session = Depends(get_session), + plate_service: PlateService = Depends(get_plate_service), current_user: User = Depends(get_active_user), ): """Display information about a plate.""" - return get_plate_read_analysis_single(session=session, plate_id=plate_id) + + return plate_service.read_plate(plate_id=plate_id) @router.get( "/", - response_model=list[PlateReadWithAnalysisDetail], + response_model=list[PlateResponse], response_model_exclude={"analyses"}, response_model_by_alias=False, ) @@ -151,33 +100,25 @@ async def read_plates( sort_order: Literal["ascend", "descend"] | None = "descend", skip: int | None = 0, limit: int | None = 10, - session: Session = Depends(get_session), + plate_service: PlateService = Depends(get_plate_service), current_user: User = Depends(get_active_user), -) -> Sequence[Plate]: +): """Display all plates""" - order_params = PlateOrderParams( order_by=order_by, skip=skip, limit=limit, sort_order=sort_order ) - plates: Sequence[Plate] = get_ordered_plates(session=session, order_params=order_params) - return plates + return plate_service.read_plates(order_params=order_params) -@router.delete("/{plate_id}", response_model=Plate) +@router.delete("/{plate_id}") def delete_plate( plate_id: int, - session: Session = Depends(get_session), + plate_service: PlateService = Depends(get_plate_service), current_user: User = Depends(get_active_user), ): """Delete plate.""" - plate = session.get(Plate, plate_id) - analyses: list[Analysis] = get_analyses_from_plate(session=session, plate_id=plate_id) - analyse_ids = [analyse.id for analyse in analyses] - for analysis in analyses: - delete_analysis(session=session, analysis=analysis) - delete_plate(session=session, plate=plate) - + analysis_ids = plate_service.delete_plate(plate_id) return JSONResponse( - f"Deleted plate: {plate_id} and analyses: {analyse_ids}", + f"Deleted plate: {plate_id} and analyses: {analysis_ids}", status_code=status.HTTP_200_OK, ) diff --git a/genotype_api/api/endpoints/users.py b/genotype_api/api/endpoints/users.py index 081c06f..48db70d 100644 --- a/genotype_api/api/endpoints/users.py +++ b/genotype_api/api/endpoints/users.py @@ -8,7 +8,7 @@ from starlette.responses import JSONResponse from genotype_api.database.crud.read import ( - get_user, + get_user_by_id, get_user_by_email, get_users_with_skip_and_limit, ) @@ -32,7 +32,7 @@ def read_user( session: Session = Depends(get_session), current_user: User = Depends(get_active_user), ) -> User: - return get_user(session=session, user_id=user_id) + return get_user_by_id(session=session, user_id=user_id) @router.delete("/{user_id}") @@ -41,7 +41,7 @@ def delete_user( session: Session = Depends(get_session), current_user: User = Depends(get_active_user), ) -> JSONResponse: - user: User = get_user(session=session, user_id=user_id) + 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") if user.plates: @@ -60,7 +60,7 @@ def change_user_email( session: Session = Depends(get_session), current_user: User = Depends(get_active_user), ) -> User: - user: User = get_user(session=session, user_id=user_id) + 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) diff --git a/genotype_api/constants.py b/genotype_api/constants.py index 78d1325..4eb1578 100644 --- a/genotype_api/constants.py +++ b/genotype_api/constants.py @@ -19,7 +19,7 @@ class Sexes(str, Enum): UNKNOWN = "unknown" -class STATUS(str, Enum): +class Status(str, Enum): PASS = "pass" FAIL = "fail" CANCEL = "cancel" diff --git a/genotype_api/database/crud/read.py b/genotype_api/database/crud/read.py index 61fb944..fa6c913 100644 --- a/genotype_api/database/crud/read.py +++ b/genotype_api/database/crud/read.py @@ -76,33 +76,23 @@ def get_analysis_by_type_and_sample_id( ).one() -def get_plate(session: Session, plate_id: int) -> Plate: - """Get plate""" +def get_plate_by_id(session: Session, plate_id: int) -> Plate: statement = select(Plate).where(Plate.id == plate_id) return session.exec(statement).one() -def get_plate_read_analysis_single( - session: Session, plate_id: int -) -> PlateReadWithAnalysisDetailSingle: - plate: Plate = get_plate(session=session, plate_id=plate_id) - return PlateReadWithAnalysisDetailSingle.from_orm(plate) - - def get_ordered_plates(session: Session, order_params: PlateOrderParams) -> list[Plate]: sort_func = desc if order_params.sort_order == "descend" else asc - plates: list[Plate] = session.exec( + return session.exec( select(Plate) .order_by(sort_func(order_params.order_by)) .offset(order_params.skip) .limit(order_params.limit) ).all() - return plates def get_incomplete_samples(statement: SelectOfScalar) -> SelectOfScalar: """Returning sample query statement for samples with less than two analyses.""" - return ( statement.group_by(Analysis.sample_id) .order_by(Analysis.created_at) @@ -162,7 +152,7 @@ def get_samples(statement: SelectOfScalar, sample_id: str) -> SelectOfScalar: return statement.where(Sample.id.contains(sample_id)) -def get_user(session: Session, user_id: int): +def get_user_by_id(session: Session, user_id: int): statement = select(User).where(User.id == user_id) return session.exec(statement).one() diff --git a/genotype_api/database/models.py b/genotype_api/database/models.py index 5af2280..426b7b2 100644 --- a/genotype_api/database/models.py +++ b/genotype_api/database/models.py @@ -5,7 +5,7 @@ from sqlalchemy import Index from sqlmodel import Field, Relationship, SQLModel -from genotype_api.constants import Sexes, STATUS, Types +from genotype_api.constants import Sexes, Status, Types class GenotypeBase(SQLModel): @@ -59,7 +59,7 @@ def check_no_calls(self) -> dict[str, int]: class SampleSlim(SQLModel): - status: STATUS | None + status: Status | None comment: str | None diff --git a/genotype_api/dto/analysis.py b/genotype_api/dto/analysis.py index ce8e284..1a51715 100644 --- a/genotype_api/dto/analysis.py +++ b/genotype_api/dto/analysis.py @@ -5,7 +5,8 @@ from pydantic import BaseModel from genotype_api.constants import Sexes, Types -from genotype_api.dto.genotype import GenotypeBase +from genotype_api.dto.genotype import GenotypeResponse +from genotype_api.dto.sample import SampleStatusResponse class AnalysisResponse(BaseModel): @@ -18,7 +19,7 @@ class AnalysisResponse(BaseModel): id: int | None -class AnalysisWithGenotypeResponse(BaseModel): +class AnalysisGenotypeResponse(BaseModel): type: Types | None source: str | None sex: Sexes | None @@ -26,4 +27,15 @@ class AnalysisWithGenotypeResponse(BaseModel): sample_id: str | None plate_id: str | None id: int | None - genotypes: list[GenotypeBase] | 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/dto.py b/genotype_api/dto/dto.py index 2ee7919..1a889b0 100644 --- a/genotype_api/dto/dto.py +++ b/genotype_api/dto/dto.py @@ -4,7 +4,8 @@ import genotype_api.database.models from genotype_api.database import models -from genotype_api.models import SampleDetail, PlateStatusCounts +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 diff --git a/genotype_api/dto/genotype.py b/genotype_api/dto/genotype.py index 306ab30..ede217a 100644 --- a/genotype_api/dto/genotype.py +++ b/genotype_api/dto/genotype.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field -class GenotypeBase(BaseModel): +class GenotypeResponse(BaseModel): rsnumber: str = Field(max_length=10) analysis_id: int allele_1: str = Field(max_length=1) diff --git a/genotype_api/dto/plate.py b/genotype_api/dto/plate.py new file mode 100644 index 0000000..c3b309b --- /dev/null +++ b/genotype_api/dto/plate.py @@ -0,0 +1,46 @@ +"""Module for the plate DTOs.""" + +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 + + +class PlateStatusCounts(BaseModel): + total: int = Field(0, nullable=True) + failed: int = Field(0, alias="STATUS.FAIL", nullable=True) + passed: int = Field(0, alias="STATUS.PASS", nullable=True) + cancelled: int = Field(0, alias="STATUS.CANCEL", nullable=True) + unknown: int = Field(0, alias="None", nullable=True) + commented: int = Field(0, nullable=True) + + class Config: + allow_population_by_field_name = True + + +class PlateResponse(BaseModel): + created_at: datetime | None = None + plate_id: str | None = None + signed_by: int | None = None + signed_at: datetime | None = None + method_document: str | None = None + method_version: str | None = None + id: str | None = None + user: UserResponse | None = None + analyses: list[AnalysisSampleResponse] | None = None + plate_status_counts: PlateStatusCounts | None = None + + @validator("detail") + def check_detail(cls, value, values): + analyses = values.get("analyses") + if not analyses: + return None + 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/sample.py b/genotype_api/dto/sample.py new file mode 100644 index 0000000..c05b8b1 --- /dev/null +++ b/genotype_api/dto/sample.py @@ -0,0 +1,10 @@ +"""Module for the sample DTOs.""" + +from pydantic import BaseModel + +from genotype_api.constants import Status + + +class SampleStatusResponse(BaseModel): + status: Status | None = None + comment: str | None = None diff --git a/genotype_api/dto/user.py b/genotype_api/dto/user.py new file mode 100644 index 0000000..1556680 --- /dev/null +++ b/genotype_api/dto/user.py @@ -0,0 +1,9 @@ +"""Module for the plate DTOs.""" + +from pydantic import BaseModel, EmailStr + + +class UserResponse(BaseModel): + email: EmailStr | None = None + name: str | None = None + id: int | None = None diff --git a/genotype_api/models.py b/genotype_api/models.py index 93a9e4c..5d13d6b 100644 --- a/genotype_api/models.py +++ b/genotype_api/models.py @@ -1,16 +1,4 @@ -from pydantic import BaseModel, validator, Field - - -class PlateStatusCounts(BaseModel): - total: int = Field(0, nullable=True) - failed: int = Field(0, alias="STATUS.FAIL", nullable=True) - passed: int = Field(0, alias="STATUS.PASS", nullable=True) - cancelled: int = Field(0, alias="STATUS.CANCEL", nullable=True) - unknown: int = Field(0, alias="None", nullable=True) - commented: int = Field(0, nullable=True) - - class Config: - allow_population_by_field_name = True +from pydantic import BaseModel, validator class SampleDetailStats(BaseModel): diff --git a/genotype_api/services/analysis_service/analysis_service.py b/genotype_api/services/analysis_service/analysis_service.py index 41753ed..9b49f84 100644 --- a/genotype_api/services/analysis_service/analysis_service.py +++ b/genotype_api/services/analysis_service/analysis_service.py @@ -14,7 +14,7 @@ ) from genotype_api.database.crud.update import refresh_sample_status from genotype_api.database.models import Analysis -from genotype_api.dto.analysis import AnalysisWithGenotypeResponse, AnalysisResponse +from genotype_api.dto.analysis import AnalysisGenotypeResponse, AnalysisResponse from genotype_api.file_parsing.files import check_file from genotype_api.file_parsing.vcf import SequenceAnalysis @@ -25,9 +25,9 @@ class AnalysisService: def __init__(self, session: Session): self.session: Session = session - def get_analysis_with_genotype(self, analysis_id: int) -> AnalysisWithGenotypeResponse: + def get_analysis_with_genotype(self, analysis_id: int) -> AnalysisGenotypeResponse: analysis: Analysis = get_analysis_by_id(session=self.session, analysis_id=analysis_id) - return AnalysisWithGenotypeResponse( + return AnalysisGenotypeResponse( type=analysis.type, source=analysis.source, sex=analysis.sex, diff --git a/genotype_api/services/plate_service/plate_service.py b/genotype_api/services/plate_service/plate_service.py new file mode 100644 index 0000000..b88cef0 --- /dev/null +++ b/genotype_api/services/plate_service/plate_service.py @@ -0,0 +1,153 @@ +"""Module to holds the plate service.""" + +from datetime import datetime +from http.client import HTTPException +from io import BytesIO +from pathlib import Path + + +from fastapi import UploadFile +from pydantic import EmailStr +from sqlmodel import Session +from starlette import status + + +from genotype_api.constants import Types +from genotype_api.database.crud.create import create_analyses_samples, create_plate +from genotype_api.database.crud.delete import delete_analysis, delete_plate +from genotype_api.database.crud.read import ( + check_analyses_objects, + get_plate_by_id, + get_ordered_plates, + get_analyses_from_plate, + get_user_by_id, + get_user_by_email, +) +from genotype_api.database.crud.update import ( + refresh_sample_status, + refresh_plate, + update_plate_sign_off, +) +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.sample import SampleStatusResponse +from genotype_api.dto.user import UserResponse +from genotype_api.file_parsing.excel import GenotypeAnalysis +from genotype_api.file_parsing.files import check_file + + +class PlateService: + + def __init__(self, session: Session): + self.session: Session = session + + @staticmethod + def _get_analyses_on_plate(plate: Plate) -> list[AnalysisSampleResponse] | None: + analyses_response: list[AnalysisSampleResponse] = [] + for analysis in plate.analyses: + if analysis: + sample_status = SampleStatusResponse( + status=analysis.sample.status, comment=analysis.sample.comment + ) + analysis_response = AnalysisSampleResponse( + 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, + sample=sample_status, + ) + analyses_response.append(analysis_response) + return analyses_response if analyses_response else None + + def _get_plate_user(self, plate: Plate) -> UserResponse | 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 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) + return PlateResponse( + created_at=plate.created_at, + plate_id=plate.plate_id, + signed_by=plate.signed_by, + signed_at=plate.signed_at, + method_document=plate.method_document, + method_version=plate.method_version, + id=plate.id, + user=user, + analyses=analyses_response, + ) + + @staticmethod + def _get_plate_id_from_file(file_name: Path) -> str: + # Get the plate id from the standardized name of the plate + return file_name.name.split("_", 1)[0] + + def upload_plate(self, file: UploadFile) -> PlateResponse: + file_name: Path = check_file(file_path=file.filename, extension=".xlsx") + plate_id: str = self._get_plate_id_from_file(file_name) + db_plate = self.session.get(Plate, plate_id) + if db_plate: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"Plate with id {db_plate.id} already exists", + ) + + excel_parser = GenotypeAnalysis( + excel_file=BytesIO(file.file.read()), + file_name=str(file_name), + include_key="-CG-", + ) + analyses: list[Analysis] = list(excel_parser.generate_analyses()) + check_analyses_objects( + session=self.session, analyses=analyses, analysis_type=Types.GENOTYPE + ) + create_analyses_samples(session=self.session, analyses=analyses) + plate_obj = PlateCreate(plate_id=plate_id) + plate_obj.analyses = analyses + plate: Plate = create_plate(session=self.session, plate=plate_obj) + for analysis in plate.analyses: + refresh_sample_status(sample=analysis.sample, session=self.session) + refresh_plate(session=self.session, plate=plate) + + return self._get_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) + user: User = get_user_by_email(session=self.session, email=user_email) + plate_sign_off = PlateSignOff( + user_id=user.id, + signed_at=datetime.now(), + method_document=method_document, + 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) + + def read_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) + + def read_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] + + 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) + 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: + delete_analysis(session=self.session, analysis=analysis) + delete_plate(session=self.session, plate=plate) + return analysis_ids diff --git a/tests/test_excel.py b/tests/test_excel.py deleted file mode 100644 index def5b02..0000000 --- a/tests/test_excel.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Tests fpr the excel module""" - -""" -def test_read_excel(excel_file: Path, include_key: str): - # GIVEN an excel file and a include key - - # WHEN parsing an excel file - parser = GenotypeAnalysis(excel_file=excel_file, include_key=include_key) - - # THEN assert that the sample is in the header - assert "SAMPLE" in parser.header_row - - -def test_parse_analyses(excel_file: Path, include_key: str, sample_name: str): - # GIVEN a parsed excel file - parser = GenotypeAnalysis(excel_file=excel_file, include_key=include_key) - - # WHEN parsing the analyses - analyses = [analysis for analysis in parser.generate_analyses()] - - # THEN assert the number of analyses is 3 - assert len(analyses) == 3 -"""