From de9435834d7465c068c6177063bfc775597ffa06 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Mon, 27 Nov 2023 17:05:21 +0100 Subject: [PATCH 1/7] feat: caseinfo schemas, models and apis --- backend/app/api/api_v1/api.py | 3 +- .../app/api/api_v1/endpoints/acmgseqvar.py | 170 ++++++++++++++++++ backend/app/api/api_v1/endpoints/caseinfo.py | 42 ++--- backend/app/crud/__init__.py | 3 + backend/app/crud/acmgseqvar.py | 26 +++ backend/app/models/__init__.py | 1 + backend/app/models/acmgseqvar.py | 38 ++++ backend/app/schemas/__init__.py | 1 + backend/app/schemas/acmgseqvar.py | 86 +++++++++ 9 files changed, 348 insertions(+), 22 deletions(-) create mode 100644 backend/app/api/api_v1/endpoints/acmgseqvar.py create mode 100644 backend/app/crud/acmgseqvar.py create mode 100644 backend/app/models/acmgseqvar.py create mode 100644 backend/app/schemas/acmgseqvar.py diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index ff0ac97a..84d7339e 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -4,12 +4,13 @@ from httpx_oauth.clients.openid import OpenID from httpx_oauth.errors import GetIdEmailError -from app.api.api_v1.endpoints import adminmsgs, auth, bookmarks, caseinfo, utils +from app.api.api_v1.endpoints import acmgseqvar, adminmsgs, auth, bookmarks, caseinfo, utils from app.core.auth import auth_backend_bearer, auth_backend_cookie, fastapi_users from app.core.config import settings from app.schemas.user import UserRead, UserUpdate api_router = APIRouter() +api_router.include_router(acmgseqvar.router, prefix="/acmgseqvar", tags=["acmgseqvar"]) api_router.include_router(adminmsgs.router, prefix="/adminmsgs", tags=["adminmsgs"]) api_router.include_router(bookmarks.router, prefix="/bookmarks", tags=["bookmarks"]) api_router.include_router(utils.router, prefix="/utils", tags=["utils"]) diff --git a/backend/app/api/api_v1/endpoints/acmgseqvar.py b/backend/app/api/api_v1/endpoints/acmgseqvar.py new file mode 100644 index 00000000..459f44ed --- /dev/null +++ b/backend/app/api/api_v1/endpoints/acmgseqvar.py @@ -0,0 +1,170 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app import crud, schemas +from app.api import deps +from app.api.deps import current_active_superuser, current_active_user +from app.models.user import User + +router = APIRouter() + + +@router.post("/create", response_model=schemas.AcmgSeqVarCreate) +async def create_acmgseqvar( + acmgseqvar: schemas.AcmgSeqVarCreate, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Create a new ACMG Sequence Variant. + + :param acmgseqvar: ACMG Sequence Variant to create + :type acmgseqvar: dict or :class:`.schemas.AcmgSeqVarCreate` + :return: ACMG Sequence Variant + :rtype: dict + """ + acmgseqvar.user = user.id + return await crud.acmgseqvar.create(db, obj_in=acmgseqvar) + + +@router.get( + "/list-all", + dependencies=[Depends(current_active_superuser)], + response_model=list[schemas.AcmgSeqVarRead], +) +async def list_acmgseqvars( + skip: int = 0, limit: int = 100, db: AsyncSession = Depends(deps.get_db) +): + """ + List all ACMG Sequence Variants. Available only for superusers. + + :param skip: number of ACMG Sequence Variants to skip + :type skip: int + :param limit: maximum number of ACMG Sequence Variants to return + :type limit: int + :return: list of ACMG Sequence Variants + :rtype: list + """ + return await crud.acmgseqvar.get_multi(db, skip=skip, limit=limit) + + +@router.get( + "/get-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.AcmgSeqVarRead, +) +async def get_acmgseqvar(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Get a ACMG Sequence Variant by id. Available only for superusers. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get(db, id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return response + + +@router.get("/list", response_model=list[schemas.AcmgSeqVarRead]) +async def list_acmgseqvars_by_user( + skip: int = 0, + limit: int = 100, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + List ACMG Sequence Variants by user. + + :return: list of ACMG Sequence Variants + :rtype: list + """ + return await crud.acmgseqvar.get_multi_by_user(db, user_id=user.id, skip=skip, limit=limit) + + +@router.get("/get", response_model=schemas.AcmgSeqVarRead) +async def get_acmgseqvar_by_user( + id: str, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Get a ACMG Sequence Variant by id. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return response + + +@router.put("/update", response_model=schemas.AcmgSeqVarRead) +@router.patch("/update", response_model=schemas.AcmgSeqVarRead) +async def update_acmgseqvar( + acmgseqvar: schemas.AcmgSeqVarUpdate, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Update a ACMG Sequence Variant. + + :param acmgseqvar: ACMG Sequence Variant to update + :type acmgseqvar: dict or :class:`.schemas.AcmgSeqVarUpdate` + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=acmgseqvar.id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.update(db, db_obj=response, obj_in=acmgseqvar) + + +@router.delete( + "/delete-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.AcmgSeqVarRead, +) +async def delete_acmgseqvar(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Delete a ACMG Sequence Variant by id. Available only for superusers. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get(db, id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.remove(db, id=id) + + +@router.delete("/delete", response_model=schemas.AcmgSeqVarRead) +async def delete_acmgseqvar_by_user( + id: str, + db: AsyncSession = Depends(deps.get_db), + user: User = Depends(current_active_user), +): + """ + Delete a ACMG Sequence Variant by id. + + :param id: ACMG Sequence Variant id + :type id: uuid + :return: ACMG Sequence Variant + :rtype: dict + """ + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=id) + if not response: + raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") + else: + return await crud.acmgseqvar.remove(db, id=id) diff --git a/backend/app/api/api_v1/endpoints/caseinfo.py b/backend/app/api/api_v1/endpoints/caseinfo.py index e909b96e..c837a7c9 100644 --- a/backend/app/api/api_v1/endpoints/caseinfo.py +++ b/backend/app/api/api_v1/endpoints/caseinfo.py @@ -67,27 +67,6 @@ async def get_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): return response -@router.delete( - "/delete-by-id", - dependencies=[Depends(current_active_superuser)], - response_model=schemas.CaseInfoRead, -) -async def delete_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): - """ - Delete a Case Information by id. Available only for superusers. - - :param id: Case Information id - :type id: uuid - :return: Case Information - :rtype: dict - """ - response = await crud.caseinfo.remove(db, id=id) - if not response: - raise HTTPException(status_code=404, detail="Case Information not found") - else: - return response - - @router.get("/list", response_model=list[schemas.CaseInfoRead]) async def list_caseinfos_for_user( db: AsyncSession = Depends(deps.get_db), @@ -140,6 +119,27 @@ async def update_caseinfo_for_user( return await crud.caseinfo.update(db, db_obj=caseinfo, obj_in=caseinfoupdate) +@router.delete( + "/delete-by-id", + dependencies=[Depends(current_active_superuser)], + response_model=schemas.CaseInfoRead, +) +async def delete_caseinfo(id: str, db: AsyncSession = Depends(deps.get_db)): + """ + Delete a Case Information by id. Available only for superusers. + + :param id: Case Information id + :type id: uuid + :return: Case Information + :rtype: dict + """ + response = await crud.caseinfo.remove(db, id=id) + if not response: + raise HTTPException(status_code=404, detail="Case Information not found") + else: + return response + + @router.delete("/delete", response_model=schemas.CaseInfoRead) async def delete_caseinfo_for_user( db: AsyncSession = Depends(deps.get_db), user: User = Depends(current_active_user) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py index e7dfc7a8..506c6f2a 100644 --- a/backend/app/crud/__init__.py +++ b/backend/app/crud/__init__.py @@ -1,6 +1,8 @@ +from app.crud.acmgseqvar import CrudAcmgSeqVar from app.crud.base import CrudBase from app.crud.bookmarks import CrudBookmark from app.crud.caseinfo import CrudCaseInfo +from app.models.acmgseqvar import AcmgSeqVar from app.models.adminmsg import AdminMessage from app.models.bookmark import Bookmark from app.models.caseinfo import CaseInfo @@ -9,3 +11,4 @@ adminmessage = CrudBase[AdminMessage, AdminMessageCreate, AdminMessageUpdate](AdminMessage) bookmark = CrudBookmark(Bookmark) caseinfo = CrudCaseInfo(CaseInfo) +acmgseqvar = CrudAcmgSeqVar(AcmgSeqVar) diff --git a/backend/app/crud/acmgseqvar.py b/backend/app/crud/acmgseqvar.py new file mode 100644 index 00000000..61b47a0a --- /dev/null +++ b/backend/app/crud/acmgseqvar.py @@ -0,0 +1,26 @@ +from typing import Any, Sequence + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +from app.crud.base import CrudBase +from app.models.acmgseqvar import AcmgSeqVar +from app.schemas.acmgseqvar import AcmgSeqVarCreate, AcmgSeqVarUpdate + + +class CrudAcmgSeqVar(CrudBase[AcmgSeqVar, AcmgSeqVarCreate, AcmgSeqVarUpdate]): + async def get_multi_by_user( + self, session: AsyncSession, *, user_id: Any + ) -> Sequence[AcmgSeqVar]: + query = select(self.model).filter(self.model.user == user_id) + result = await session.execute(query) + return result.scalars().all() + + async def get_by_user( + self, session: AsyncSession, *, user_id: Any, seqvar_id: Any + ) -> AcmgSeqVar | None: + query = select(self.model).filter( + self.model.user == user_id, self.model.seqvar_id == seqvar_id + ) + result = await session.execute(query) + return result.scalars().first() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a779ebe4..7302c49a 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,3 +1,4 @@ +from app.models.acmgseqvar import AcmgSeqVar # noqa from app.models.adminmsg import AdminMessage # noqa from app.models.bookmark import Bookmark # noqa from app.models.caseinfo import CaseInfo # noqa diff --git a/backend/app/models/acmgseqvar.py b/backend/app/models/acmgseqvar.py new file mode 100644 index 00000000..a8265a46 --- /dev/null +++ b/backend/app/models/acmgseqvar.py @@ -0,0 +1,38 @@ +"""Models for ACMG sequence variant.""" + +import uuid as uuid_module +from typing import TYPE_CHECKING + +from fastapi_users_db_sqlalchemy.generics import GUID # noqa +from sqlalchemy import JSON, Column, ForeignKey, String, UniqueConstraint, Uuid +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.session import Base +from app.schemas.acmgseqvar import AcmgSeqVar + +UUID_ID = uuid_module.UUID + + +class AcmgSeqVar(Base): + """ACMG sequence variant.""" + + __tablename__ = "acmgseqvar" + + __table_args__ = (UniqueConstraint("user", "seqvar_id", name="uq_acmgseqvar"),) + + if TYPE_CHECKING: # pragma: no cover + id: UUID_ID + user: UUID_ID + seqvar_id: str + criteria: list[AcmgSeqVar] + else: + #: UUID of the ACMG sequence variant. + id: Mapped[UUID_ID] = mapped_column( + GUID, primary_key=True, index=True, default=uuid_module.uuid4 + ) + #: User who created the ACMG sequence variant. + user = Column(Uuid, ForeignKey("user.id", ondelete="CASCADE"), nullable=False) + #: Sequence variant ID. + seqvar_id = Column(String(255), nullable=False) + #: ACMG criteria. + criteria = Column(JSON, nullable=True) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 1fe27737..bce0f66e 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -1,3 +1,4 @@ +from app.schemas.acmgseqvar import AcmgSeqVarCreate, AcmgSeqVarRead, AcmgSeqVarUpdate # noqa from app.schemas.adminmsg import AdminMessageCreate, AdminMessageRead, AdminMessageUpdate # noqa from app.schemas.auth import OAuth2ProviderConfig, OAuth2ProviderPublic # noqa from app.schemas.bookmark import BookmarkCreate, BookmarkRead, BookmarkUpdate # noqa diff --git a/backend/app/schemas/acmgseqvar.py b/backend/app/schemas/acmgseqvar.py new file mode 100644 index 00000000..a977933e --- /dev/null +++ b/backend/app/schemas/acmgseqvar.py @@ -0,0 +1,86 @@ +from enum import Enum +from uuid import UUID + +from pydantic import BaseModel, ConfigDict + + +class Presence(Enum): + Present = "present" + Absent = "absent" + Unknown = "unknown" + + +class Evidence(Enum): + PathogenicVeryStrong = ("Pathogenic Very Strong",) + PathogenicStrong = ("Pathogenic Strong",) + PathogenicModerate = ("Pathogenic Moderate",) + PathogenicSupporting = ("Pathogenic Supporting",) + BenignStandalone = ("Benign Standalone",) + BenignStrong = ("Benign Strong",) + BenignSupporting = ("Benign Supporting",) + NotSet = "Not Set" + + +class Criteria(Enum): + Pvs1 = ("Pvs1",) + Ps1 = ("Ps1",) + Ps2 = ("Ps2",) + Ps3 = ("Ps3",) + Ps4 = ("Ps4",) + Pm1 = ("Pm1",) + Pm2 = ("Pm2",) + Pm3 = ("Pm3",) + Pm4 = ("Pm4",) + Pm5 = ("Pm5",) + Pm6 = ("Pm6",) + Pp1 = ("Pp1",) + Pp2 = ("Pp2",) + Pp3 = ("Pp3",) + Pp4 = ("Pp4",) + Pp5 = ("Pp5",) + Ba1 = ("Ba1",) + Bs1 = ("Bs1",) + Bs2 = ("Bs2",) + Bs3 = ("Bs3",) + Bs4 = ("Bs4",) + Bp1 = ("Bp1",) + Bp2 = ("Bp2",) + Bp3 = ("Bp3",) + Bp4 = ("Bp4",) + Bp5 = ("Bp5",) + Bp6 = ("Bp6",) + Bp7 = "Bp7" + + +class SeqVarCriteria(BaseModel): + criteria: Criteria + presence: Presence + evidence: Evidence + + +class AcmgSeqVar(BaseModel): + user: UUID | None = None + seqvar_id: str | None = None + criteria: list[SeqVarCriteria] | None = None + + +class AcmgSeqVarCreate(AcmgSeqVar): + pass + + +class AcmgSeqVarUpdate(AcmgSeqVar): + pass + + +class AcmgSeqVarInDbBase(AcmgSeqVar): + model_config = ConfigDict(from_attributes=True) + + id: UUID + + +class AcmgSeqVarRead(AcmgSeqVarInDbBase): + pass + + +class AcmgSeqVarInDb(AcmgSeqVarInDbBase): + pass From 572cb09d890535bcc730927477b7cb9b3d65fd91 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Wed, 29 Nov 2023 11:31:14 +0100 Subject: [PATCH 2/7] new apis --- .../versions/44fcdd33bf03_init_acmgseqvar.py | 29 ++++++ .../versions/d10fec1c88fc_init_acmgseqvar.py | 39 ++++++++ .../app/api/api_v1/endpoints/acmgseqvar.py | 15 +-- backend/app/api/api_v1/endpoints/utils.py | 13 +++ backend/app/crud/acmgseqvar.py | 11 ++- backend/app/models/acmgseqvar.py | 12 +-- backend/app/schemas/acmgseqvar.py | 93 ++++++++++--------- 7 files changed, 151 insertions(+), 61 deletions(-) create mode 100644 backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py create mode 100644 backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py diff --git a/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py b/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py new file mode 100644 index 00000000..c03bd07d --- /dev/null +++ b/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py @@ -0,0 +1,29 @@ +"""init acmgseqvar + +Revision ID: 44fcdd33bf03 +Revises: d10fec1c88fc +Create Date: 2023-11-29 11:21:01.073814+01:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "44fcdd33bf03" +down_revision = "d10fec1c88fc" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py b/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py new file mode 100644 index 00000000..085d6af8 --- /dev/null +++ b/backend/alembic/versions/d10fec1c88fc_init_acmgseqvar.py @@ -0,0 +1,39 @@ +"""init acmgseqvar +Revision ID: d10fec1c88fc +Revises: 4f3b20f156c1 +Create Date: 2023-11-29 11:16:25.636296+01:00 + +""" +import fastapi_users_db_sqlalchemy.generics # noqa +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "d10fec1c88fc" +down_revision = "4f3b20f156c1" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "acmgseqvar", + sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False), + sa.Column("user", sa.Uuid(), nullable=False), + sa.Column("seqvar_name", sa.String(length=255), nullable=False), + sa.Column("acmg_rank", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(["user"], ["user.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user", "seqvar_name", name="uq_acmgseqvar"), + ) + op.create_index(op.f("ix_acmgseqvar_id"), "acmgseqvar", ["id"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_acmgseqvar_id"), table_name="acmgseqvar") + op.drop_table("acmgseqvar") + # ### end Alembic commands ### diff --git a/backend/app/api/api_v1/endpoints/acmgseqvar.py b/backend/app/api/api_v1/endpoints/acmgseqvar.py index 459f44ed..c85e8d4d 100644 --- a/backend/app/api/api_v1/endpoints/acmgseqvar.py +++ b/backend/app/api/api_v1/endpoints/acmgseqvar.py @@ -87,7 +87,7 @@ async def list_acmgseqvars_by_user( @router.get("/get", response_model=schemas.AcmgSeqVarRead) async def get_acmgseqvar_by_user( - id: str, + seqvar: str, db: AsyncSession = Depends(deps.get_db), user: User = Depends(current_active_user), ): @@ -99,7 +99,7 @@ async def get_acmgseqvar_by_user( :return: ACMG Sequence Variant :rtype: dict """ - response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=id) + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_name=seqvar) if not response: raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") else: @@ -121,7 +121,10 @@ async def update_acmgseqvar( :return: ACMG Sequence Variant :rtype: dict """ - response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=acmgseqvar.id) + acmgseqvar.user = user.id + response = await crud.acmgseqvar.get_by_user( + db, user_id=user.id, seqvar_name=acmgseqvar.seqvar_name + ) if not response: raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") else: @@ -151,7 +154,7 @@ async def delete_acmgseqvar(id: str, db: AsyncSession = Depends(deps.get_db)): @router.delete("/delete", response_model=schemas.AcmgSeqVarRead) async def delete_acmgseqvar_by_user( - id: str, + seqvar: str, db: AsyncSession = Depends(deps.get_db), user: User = Depends(current_active_user), ): @@ -163,8 +166,8 @@ async def delete_acmgseqvar_by_user( :return: ACMG Sequence Variant :rtype: dict """ - response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_id=id) + response = await crud.acmgseqvar.get_by_user(db, user_id=user.id, seqvar_name=seqvar) if not response: raise HTTPException(status_code=404, detail="ACMG Sequence Variant not found") else: - return await crud.acmgseqvar.remove(db, id=id) + return await crud.acmgseqvar.remove(db, id=response.id) diff --git a/backend/app/api/api_v1/endpoints/utils.py b/backend/app/api/api_v1/endpoints/utils.py index 2a14da9f..22716716 100644 --- a/backend/app/api/api_v1/endpoints/utils.py +++ b/backend/app/api/api_v1/endpoints/utils.py @@ -5,11 +5,24 @@ from app import schemas from app.api.deps import current_active_superuser +from app.db.init_db import init_db from app.etc.utils import send_test_email router = APIRouter() +@router.get("/init-db/", response_model=schemas.Msg, status_code=201) +async def init_db_command() -> Any: + """ + Initialize the database. + + :return: message + :rtype: dict + """ + await init_db() + return {"msg": "Database initialized"} + + @router.post( "/test-email/", response_model=schemas.Msg, diff --git a/backend/app/crud/acmgseqvar.py b/backend/app/crud/acmgseqvar.py index 61b47a0a..f3647651 100644 --- a/backend/app/crud/acmgseqvar.py +++ b/backend/app/crud/acmgseqvar.py @@ -10,17 +10,18 @@ class CrudAcmgSeqVar(CrudBase[AcmgSeqVar, AcmgSeqVarCreate, AcmgSeqVarUpdate]): async def get_multi_by_user( - self, session: AsyncSession, *, user_id: Any + self, session: AsyncSession, *, skip: int = 0, limit: int = 100, user_id: Any ) -> Sequence[AcmgSeqVar]: - query = select(self.model).filter(self.model.user == user_id) + query = select(self.model).filter(self.model.user == user_id).offset(skip).limit(limit) result = await session.execute(query) - return result.scalars().all() + all_scalars: Sequence[AcmgSeqVar] = result.scalars().all() # type: ignore[assignment] + return all_scalars async def get_by_user( - self, session: AsyncSession, *, user_id: Any, seqvar_id: Any + self, session: AsyncSession, *, user_id: Any, seqvar_name: Any ) -> AcmgSeqVar | None: query = select(self.model).filter( - self.model.user == user_id, self.model.seqvar_id == seqvar_id + self.model.user == user_id, self.model.seqvar_name == seqvar_name ) result = await session.execute(query) return result.scalars().first() diff --git a/backend/app/models/acmgseqvar.py b/backend/app/models/acmgseqvar.py index a8265a46..a715b803 100644 --- a/backend/app/models/acmgseqvar.py +++ b/backend/app/models/acmgseqvar.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column from app.db.session import Base -from app.schemas.acmgseqvar import AcmgSeqVar +from app.schemas.acmgseqvar import AcmgRank UUID_ID = uuid_module.UUID @@ -18,13 +18,13 @@ class AcmgSeqVar(Base): __tablename__ = "acmgseqvar" - __table_args__ = (UniqueConstraint("user", "seqvar_id", name="uq_acmgseqvar"),) + __table_args__ = (UniqueConstraint("user", "seqvar_name", name="uq_acmgseqvar"),) if TYPE_CHECKING: # pragma: no cover id: UUID_ID user: UUID_ID - seqvar_id: str - criteria: list[AcmgSeqVar] + seqvar_name: str + acmg_rank: AcmgRank else: #: UUID of the ACMG sequence variant. id: Mapped[UUID_ID] = mapped_column( @@ -33,6 +33,6 @@ class AcmgSeqVar(Base): #: User who created the ACMG sequence variant. user = Column(Uuid, ForeignKey("user.id", ondelete="CASCADE"), nullable=False) #: Sequence variant ID. - seqvar_id = Column(String(255), nullable=False) + seqvar_name = Column(String(255), nullable=False) #: ACMG criteria. - criteria = Column(JSON, nullable=True) + acmg_rank = Column(JSON, nullable=True) diff --git a/backend/app/schemas/acmgseqvar.py b/backend/app/schemas/acmgseqvar.py index a977933e..931f1e84 100644 --- a/backend/app/schemas/acmgseqvar.py +++ b/backend/app/schemas/acmgseqvar.py @@ -4,51 +4,51 @@ from pydantic import BaseModel, ConfigDict -class Presence(Enum): - Present = "present" - Absent = "absent" - Unknown = "unknown" - - -class Evidence(Enum): - PathogenicVeryStrong = ("Pathogenic Very Strong",) - PathogenicStrong = ("Pathogenic Strong",) - PathogenicModerate = ("Pathogenic Moderate",) - PathogenicSupporting = ("Pathogenic Supporting",) - BenignStandalone = ("Benign Standalone",) - BenignStrong = ("Benign Strong",) - BenignSupporting = ("Benign Supporting",) +class Presence(str, Enum): + Present = "Present" + Absent = "Absent" + Unknown = "Unknown" + + +class Evidence(str, Enum): + PathogenicVeryStrong = "Pathogenic Very Strong" + PathogenicStrong = "Pathogenic Strong" + PathogenicModerate = "Pathogenic Moderate" + PathogenicSupporting = "Pathogenic Supporting" + BenignStandalone = "Benign Standalone" + BenignStrong = "Benign Strong" + BenignSupporting = "Benign Supporting" NotSet = "Not Set" -class Criteria(Enum): - Pvs1 = ("Pvs1",) - Ps1 = ("Ps1",) - Ps2 = ("Ps2",) - Ps3 = ("Ps3",) - Ps4 = ("Ps4",) - Pm1 = ("Pm1",) - Pm2 = ("Pm2",) - Pm3 = ("Pm3",) - Pm4 = ("Pm4",) - Pm5 = ("Pm5",) - Pm6 = ("Pm6",) - Pp1 = ("Pp1",) - Pp2 = ("Pp2",) - Pp3 = ("Pp3",) - Pp4 = ("Pp4",) - Pp5 = ("Pp5",) - Ba1 = ("Ba1",) - Bs1 = ("Bs1",) - Bs2 = ("Bs2",) - Bs3 = ("Bs3",) - Bs4 = ("Bs4",) - Bp1 = ("Bp1",) - Bp2 = ("Bp2",) - Bp3 = ("Bp3",) - Bp4 = ("Bp4",) - Bp5 = ("Bp5",) - Bp6 = ("Bp6",) +class Criteria(str, Enum): + Pvs1 = "Pvs1" + Ps1 = "Ps1" + Ps2 = "Ps2" + Ps3 = "Ps3" + Ps4 = "Ps4" + Pm1 = "Pm1" + Pm2 = "Pm2" + Pm3 = "Pm3" + Pm4 = "Pm4" + Pm5 = "Pm5" + Pm6 = "Pm6" + Pp1 = "Pp1" + Pp2 = "Pp2" + Pp3 = "Pp3" + Pp4 = "Pp4" + Pp5 = "Pp5" + Ba1 = "Ba1" + Bs1 = "Bs1" + Bs2 = "Bs2" + Bs3 = "Bs3" + Bs4 = "Bs4" + Bp1 = "Bp1" + Bp2 = "Bp2" + Bp3 = "Bp3" + Bp4 = "Bp4" + Bp5 = "Bp5" + Bp6 = "Bp6" Bp7 = "Bp7" @@ -58,10 +58,15 @@ class SeqVarCriteria(BaseModel): evidence: Evidence +class AcmgRank(BaseModel): + criterias: list[SeqVarCriteria] + comment: str + + class AcmgSeqVar(BaseModel): user: UUID | None = None - seqvar_id: str | None = None - criteria: list[SeqVarCriteria] | None = None + seqvar_name: str | None = None + acmg_rank: AcmgRank | None = None class AcmgSeqVarCreate(AcmgSeqVar): From d3c8eb391ac914a2fd2316bec638abc9011dd595 Mon Sep 17 00:00:00 2001 From: gromdimon Date: Wed, 29 Nov 2023 11:36:08 +0100 Subject: [PATCH 3/7] cleanup --- .../versions/44fcdd33bf03_init_acmgseqvar.py | 29 ------------------- backend/app/api/api_v1/endpoints/utils.py | 13 --------- 2 files changed, 42 deletions(-) delete mode 100644 backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py diff --git a/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py b/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py deleted file mode 100644 index c03bd07d..00000000 --- a/backend/alembic/versions/44fcdd33bf03_init_acmgseqvar.py +++ /dev/null @@ -1,29 +0,0 @@ -"""init acmgseqvar - -Revision ID: 44fcdd33bf03 -Revises: d10fec1c88fc -Create Date: 2023-11-29 11:21:01.073814+01:00 - -""" -import fastapi_users_db_sqlalchemy.generics # noqa -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "44fcdd33bf03" -down_revision = "d10fec1c88fc" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git a/backend/app/api/api_v1/endpoints/utils.py b/backend/app/api/api_v1/endpoints/utils.py index 22716716..2a14da9f 100644 --- a/backend/app/api/api_v1/endpoints/utils.py +++ b/backend/app/api/api_v1/endpoints/utils.py @@ -5,24 +5,11 @@ from app import schemas from app.api.deps import current_active_superuser -from app.db.init_db import init_db from app.etc.utils import send_test_email router = APIRouter() -@router.get("/init-db/", response_model=schemas.Msg, status_code=201) -async def init_db_command() -> Any: - """ - Initialize the database. - - :return: message - :rtype: dict - """ - await init_db() - return {"msg": "Database initialized"} - - @router.post( "/test-email/", response_model=schemas.Msg, From e103ef17aeaab13b50e9d960d0440a122e1acdec Mon Sep 17 00:00:00 2001 From: gromdimon Date: Fri, 1 Dec 2023 13:27:47 +0100 Subject: [PATCH 4/7] frontend part --- frontend/src/api/acmgseqvar.ts | 93 ++++++++++ .../components/VariantDetails/AcmgRating.vue | 76 ++++++--- frontend/src/lib/acmgSeqVar.ts | 17 +- frontend/src/stores/variantAcmgRating.ts | 161 +++++++++++++++++- 4 files changed, 321 insertions(+), 26 deletions(-) create mode 100644 frontend/src/api/acmgseqvar.ts diff --git a/frontend/src/api/acmgseqvar.ts b/frontend/src/api/acmgseqvar.ts new file mode 100644 index 00000000..b6acbb46 --- /dev/null +++ b/frontend/src/api/acmgseqvar.ts @@ -0,0 +1,93 @@ +import { API_V1_BASE_PREFIX } from '@/api/common' +import { type AcmgRatingBackend } from '@/stores/variantAcmgRating' + +/** + * Access to the seqvar part of the API. + */ +export class AcmgSeqVarClient { + private apiBaseUrl: string + private csrfToken: string | null + private currentUserId: string | null + + constructor(apiBaseUrl?: string, csrfToken?: string) { + this.apiBaseUrl = apiBaseUrl ?? API_V1_BASE_PREFIX + this.csrfToken = csrfToken ?? null + this.currentUserId = null + } + + /** + * Obtains the ACMG rating for a variant. + * + * @param variantName The variant to retrieve the ACMG rating for. + * @returns The ACMG rating for the variant. + */ + async fetchAcmgRating(variantName: string): Promise { + const url = `${this.apiBaseUrl}acmgseqvar/get?seqvar=${variantName}` + const response = await fetch(url, { + method: 'GET', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } + + /** + * Save the ACMG rating for a variant. + */ + async saveAcmgRating( + variantName: string, + acmgRating: AcmgRatingBackend + ): Promise { + const postData = `{ + "seqvar_name": "${variantName}", + "acmg_rank": ${JSON.stringify(acmgRating)} + }` + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/create`, { + method: 'POST', + mode: 'cors', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: postData + }) + return await response.json() + } + + /** + * Update the ACMG rating for a variant. + */ + async updateAcmgRating( + variantName: string, + acmgRating: AcmgRatingBackend + ): Promise { + const postData = `{ + "seqvar_name": "${variantName}", + "acmg_rank": ${JSON.stringify(acmgRating)} + }` + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/update`, { + method: 'PUT', + mode: 'cors', + credentials: 'include', + headers: { + accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: postData + }) + return await response.json() + } + + /** + * Delete the ACMG rating for a variant. + */ + async deleteAcmgRating(variantName: string): Promise { + const response = await fetch(`${this.apiBaseUrl}acmgseqvar/delete?seqvar=${variantName}`, { + method: 'DELETE', + mode: 'cors', + credentials: 'include' + }) + return await response.json() + } +} diff --git a/frontend/src/components/VariantDetails/AcmgRating.vue b/frontend/src/components/VariantDetails/AcmgRating.vue index 96de65ea..dab22c93 100644 --- a/frontend/src/components/VariantDetails/AcmgRating.vue +++ b/frontend/src/components/VariantDetails/AcmgRating.vue @@ -29,10 +29,22 @@ const unsetAcmgRating = () => { acmgRatingStore.acmgRating.setUserPresenceAbsent() } -const resetAcmgRating = () => { +const resetAcmgRatingInterVar = () => { acmgRatingStore.acmgRating.setUserPresenceInterVar() } +const resetAcmgRatingServer = () => { + acmgRatingStore.acmgRating.setUserPresenceServer() +} + +const saveAcmgRating = () => { + acmgRatingStore.saveAcmgRating() +} + +const deleteAcmgRating = () => { + acmgRatingStore.deleteAcmgRating() +} + const updateAcmgConflicting = (isConflicting: boolean) => { acmgRatingConflicting.value = isConflicting } @@ -54,7 +66,11 @@ watch( async () => { if (props.smallVariant && acmgRatingStore.storeState === StoreState.Active) { await acmgRatingStore.setAcmgRating(props.smallVariant) - resetAcmgRating() + if (acmgRatingStore.acmgRatingStatus === false) { + resetAcmgRatingInterVar() + } else { + resetAcmgRatingServer() + } } } ) @@ -70,9 +86,8 @@ onMounted(async () => { ACMG Rating - - - + +

@@ -86,10 +101,6 @@ onMounted(async () => { Further documentation mdi-open-in-new

-
- Clear - Reset -
@@ -108,16 +119,44 @@ onMounted(async () => {
- + + + + With the buttons below you can twick the presented ACMG rating: +
+ Clear + + Reset to InterVar + +
+ + Reset to server + +
+
+ Reset to server +
+
+ With this buttons below you can + {{ acmgRatingStore.acmgRatingStatus === false ? 'save' : 'update' }} ACMG rating on the + server: +
+ + {{ acmgRatingStore.acmgRatingStatus === false ? 'Save' : 'Update' }} + + Delete +
+
+
- + {{ showSwitches ? 'Hide' : 'Show' }} summary view - +

Pathogenic:

@@ -160,7 +199,7 @@ onMounted(async () => {
- + {{ showFailed ? 'Hide' : 'Show' }} failed criteria @@ -203,17 +242,6 @@ onMounted(async () => {