Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add(plate service) #111

Merged
merged 33 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3e41587
add models
ChrOertlin Mar 20, 2024
5416e03
Merge branch 'main' into add-plate-service
ChrOertlin Mar 21, 2024
4184990
update signoff service
ChrOertlin Mar 22, 2024
61bed0a
fix plate service
ChrOertlin Mar 22, 2024
97b4b0e
cleanup
ChrOertlin Mar 22, 2024
3e637d3
remove depr. crud function
ChrOertlin Mar 22, 2024
1d81f27
cleanup service
ChrOertlin Mar 22, 2024
1fde28b
remove non existing model hint
ChrOertlin Mar 22, 2024
f0e69e0
fix import issue
ChrOertlin Mar 22, 2024
f21e017
fix statement
ChrOertlin Mar 22, 2024
55c8928
fix session import
ChrOertlin Mar 22, 2024
6c9d83c
fix depend
ChrOertlin Mar 22, 2024
efd1158
fix ref issue
ChrOertlin Mar 22, 2024
0530894
initialise empty model
ChrOertlin Mar 22, 2024
8467b2d
datetime typehints
ChrOertlin Mar 22, 2024
2160f54
fixes
ChrOertlin Mar 25, 2024
4467a34
conflicts
ChrOertlin Mar 25, 2024
fc19cd6
fix call
ChrOertlin Mar 25, 2024
77c3e87
fix plate analyses
ChrOertlin Mar 25, 2024
644e8e4
fix validator
ChrOertlin Mar 25, 2024
bdfd1dc
test
ChrOertlin Mar 25, 2024
e20fb43
test dependency injection
ChrOertlin Mar 25, 2024
2ded10d
test dependency injection
ChrOertlin Mar 25, 2024
e7c0c3a
test model
ChrOertlin Mar 25, 2024
641788c
fix user fetch
ChrOertlin Mar 25, 2024
041c3e5
fix response
ChrOertlin Mar 25, 2024
ccce05d
fix response
ChrOertlin Mar 25, 2024
be712be
fix signoff
ChrOertlin Mar 25, 2024
f595dfe
Update genotype_api/services/plate_service/plate_service.py
ChrOertlin Mar 25, 2024
8be17b4
fix typehint
ChrOertlin Mar 25, 2024
d62895d
fix typehint
ChrOertlin Mar 25, 2024
e61e645
Update genotype_api/dto/plate.py
ChrOertlin Mar 25, 2024
b567b78
review
ChrOertlin Mar 25, 2024
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
4 changes: 2 additions & 2 deletions genotype_api/api/endpoints/analyses.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
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

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),
Expand Down
131 changes: 41 additions & 90 deletions genotype_api/api/endpoints/plates.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,20 @@
"""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,
)
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.filter_models.plate_models import PlateOrderParams
from genotype_api.database.models import (
Analysis,
Plate,
User,
)
from genotype_api.dto.dto import (
PlateCreate,
PlateReadWithAnalyses,
PlateReadWithAnalysisDetail,
PlateReadWithAnalysisDetailSingle,
)
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()

Expand All @@ -53,64 +24,51 @@ def get_plate_id_from_file(file_name: Path) -> str:
return file_name.name.split("_", 1)[0]


@router.post("/plate", response_model=PlateReadWithAnalyses)
def get_plate_service() -> PlateService:
session: Session = get_session()
return PlateService(session)
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved


@router.post(
"/plate",
response_model=PlateResponse,
response_model_exclude={"detail"},
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved
)
def upload_plate(
file: UploadFile = File(...),
session: Session = Depends(get_session),
plate_service: PlateService = Depends(get_plate_service),
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved
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
"""

plate: Plate = get_plate(session=session, plate_id=plate_id)
plate_sign_off = PlateSignOff(
return plate_service.update_plate_sign_off(
plate_id=plate_id,
user_id=current_user.id,
signed_at=datetime.now(),
method_document=method_document,
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={
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved
"analyses": {
Expand All @@ -132,16 +90,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,
)
Expand All @@ -150,33 +109,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"""
sort_func = desc if sort_order == "descend" else asc
order_params = PlateOrderParams(order_by=order_by, skip=skip, limit=limit)
plates: Sequence[Plate] = get_ordered_plates(
session=session, order_params=order_params, sort_func=sort_func
)
return plates

return plate_service.read_plates(order_params=order_params, sort_func=sort_func)

@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,
)
2 changes: 1 addition & 1 deletion genotype_api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Sexes(str, Enum):
UNKNOWN = "unknown"


class STATUS(str, Enum):
class Status(str, Enum):
PASS = "pass"
FAIL = "fail"
CANCEL = "cancel"
Expand Down
9 changes: 1 addition & 8 deletions genotype_api/database/crud/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,13 @@ def get_analysis_by_type_and_sample_id(
).one()


def get_plate(session: Session, plate_id: int) -> Plate:
def get_plate_by_id(session: Session, plate_id: int) -> Plate:
"""Get plate"""
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved

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, sort_func: Callable
) -> Sequence[Plate]:
Expand Down
4 changes: 2 additions & 2 deletions genotype_api/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -59,7 +59,7 @@ def check_no_calls(self) -> dict[str, int]:


class SampleSlim(SQLModel):
status: STATUS | None
status: Status | None
comment: str | None


Expand Down
18 changes: 15 additions & 3 deletions genotype_api/dto/analysis.py
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -18,12 +19,23 @@ class AnalysisResponse(BaseModel):
id: int | None


class AnalysisWithGenotypeResponse(BaseModel):
class AnalysisGenotypeResponse(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
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
3 changes: 2 additions & 1 deletion genotype_api/dto/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion genotype_api/dto/genotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions genotype_api/dto/plate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Module for the plate DTOs."""

from collections import Counter
from pydantic import BaseModel, validator, Field
from genotype_api.dto.analysis import AnalysisSampleResponse
from genotype_api.dto.user import UserInfoResponse


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: str
plate_id: str
signed_by: int
signed_at: str
method_document: str
method_version: str
id: str
user: UserInfoResponse
analyses: list[AnalysisSampleResponse] = []
detail: PlateStatusCounts

@validator("detail")
def check_detail(self, 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
10 changes: 10 additions & 0 deletions genotype_api/dto/sample.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions genotype_api/dto/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Module for the plate DTOs."""

from pydantic import BaseModel, EmailStr


class UserInfoResponse(BaseModel):
ChrOertlin marked this conversation as resolved.
Show resolved Hide resolved
email: EmailStr
name: str | None = None
id: int
Loading
Loading