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

person cluster patient assignment endpoints #172

Merged
merged 12 commits into from
Feb 6, 2025
Merged
29 changes: 19 additions & 10 deletions src/recordlinker/database/mpi_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,19 +242,25 @@ def delete_blocking_values_for_patient(

:returns: None
"""
session.query(models.BlockingValue).filter(models.BlockingValue.patient_id == patient.id).delete()
session.query(models.BlockingValue).filter(
models.BlockingValue.patient_id == patient.id
).delete()
if commit:
session.commit()


def get_patient_by_reference_id(
session: orm.Session, reference_id: uuid.UUID
) -> models.Patient | None:
def get_patients_by_reference_ids(
session: orm.Session, *reference_ids: uuid.UUID
) -> list[models.Patient | None]:
"""
Retrieve the Patient by their reference id
Retrieve all the Patients by their reference ids. If a Patient is not found,
a None value will be returned in the list for that reference id.
"""
query = select(models.Patient).where(models.Patient.reference_id == reference_id)
return session.scalar(query)
query = select(models.Patient).where(models.Patient.reference_id.in_(reference_ids))
patients_by_id: dict[uuid.UUID, models.Patient] = {
patient.reference_id: patient for patient in session.execute(query).scalars().all()
}
return [patients_by_id.get(ref_id) for ref_id in reference_ids]


def get_person_by_reference_id(
Expand All @@ -269,19 +275,21 @@ def get_person_by_reference_id(

def update_person_cluster(
session: orm.Session,
patient: models.Patient,
patients: typing.Sequence[models.Patient],
person: models.Person | None = None,
commit: bool = True,
) -> models.Person:
"""
Update the cluster for a given patient.
"""
patient.person = person or models.Person()
person = person or models.Person()
for patient in patients:
patient.person = person
session.flush()

if commit:
session.commit()
return patient.person
return person


def reset_mpi(session: orm.Session, commit: bool = True):
Expand All @@ -294,6 +302,7 @@ def reset_mpi(session: orm.Session, commit: bool = True):
if commit:
session.commit()


def delete_patient(session: orm.Session, obj: models.Patient, commit: bool = False) -> None:
"""
Deletes an Patient from the database
Expand Down
6 changes: 4 additions & 2 deletions src/recordlinker/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from recordlinker.routes.algorithm_router import router as algorithm_router
from recordlinker.routes.link_router import router as link_router
from recordlinker.routes.patient_router import router as patient_router
from recordlinker.routes.person_router import router as person_router
from recordlinker.routes.seed_router import router as seed_router

app = fastapi.FastAPI(
Expand Down Expand Up @@ -78,5 +79,6 @@ async def health_check(

app.include_router(link_router, tags=["link"])
app.include_router(algorithm_router, prefix="/algorithm", tags=["algorithm"])
app.include_router(patient_router, prefix="/patient", tags=["patient"])
app.include_router(seed_router, prefix="/seed", tags=["seed"])
app.include_router(person_router, prefix="/person", tags=["mpi"])
app.include_router(patient_router, prefix="/patient", tags=["mpi"])
app.include_router(seed_router, prefix="/seed", tags=["mpi"])
22 changes: 16 additions & 6 deletions src/recordlinker/routes/patient_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,23 @@
"/{patient_reference_id}/person",
summary="Assign Patient to new Person",
status_code=fastapi.status.HTTP_201_CREATED,
deprecated=True,
)
def create_person(
patient_reference_id: uuid.UUID, session: orm.Session = fastapi.Depends(get_session)
) -> schemas.PatientPersonRef:
"""
**NOTE**: This endpoint is deprecated. Use the POST `/person` endpoint instead.

**NOTE**: This endpoint will be removed in v25.3.0.

Create a new Person in the MPI database and link the Patient to them.
"""
patient = service.get_patient_by_reference_id(session, patient_reference_id)
patient = service.get_patients_by_reference_ids(session, patient_reference_id)[0]
if patient is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)

person = service.update_person_cluster(session, patient, commit=False)
person = service.update_person_cluster(session, [patient], commit=False)
return schemas.PatientPersonRef(
patient_reference_id=patient.reference_id, person_reference_id=person.reference_id
)
Expand All @@ -44,24 +49,29 @@ def create_person(
"/{patient_reference_id}/person",
summary="Assign Patient to existing Person",
status_code=fastapi.status.HTTP_200_OK,
deprecated=True,
)
def update_person(
patient_reference_id: uuid.UUID,
data: schemas.PersonRef,
session: orm.Session = fastapi.Depends(get_session),
) -> schemas.PatientPersonRef:
"""
**NOTE**: This endpoint is deprecated. Use the PATCH `/person/{person_reference_id}` endpoint instead.

**NOTE**: This endpoint will be removed in v25.3.0.

Update the Person linked on the Patient.
"""
patient = service.get_patient_by_reference_id(session, patient_reference_id)
patient = service.get_patients_by_reference_ids(session, patient_reference_id)[0]
if patient is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)

person = service.get_person_by_reference_id(session, data.person_reference_id)
if person is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY)

person = service.update_person_cluster(session, patient, person, commit=False)
person = service.update_person_cluster(session, [patient], person, commit=False)
return schemas.PatientPersonRef(
patient_reference_id=patient.reference_id, person_reference_id=person.reference_id
)
Expand Down Expand Up @@ -118,7 +128,7 @@ def update_patient(
"""
Update an existing patient record in the MPI
"""
patient = service.get_patient_by_reference_id(session, patient_reference_id)
patient = service.get_patients_by_reference_ids(session, patient_reference_id)[0]
if patient is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)

Expand Down Expand Up @@ -162,7 +172,7 @@ def delete_patient(
"""
Delete a Patient from the mpi database.
"""
patient = service.get_patient_by_reference_id(session, patient_reference_id)
patient = service.get_patients_by_reference_ids(session, patient_reference_id)[0]

if patient is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)
Expand Down
77 changes: 77 additions & 0 deletions src/recordlinker/routes/person_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
recordlinker.routes.person_router
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This module implements the person router for the RecordLinker API. Exposing
the person API endpoints.
"""

import typing
import uuid

import fastapi
import sqlalchemy.orm as orm

from recordlinker import models
from recordlinker import schemas
from recordlinker.database import get_session
from recordlinker.database import mpi_service as service

router = fastapi.APIRouter()


def patients_by_id_or_422(
session: orm.Session, reference_ids: typing.Sequence[uuid.UUID]
) -> typing.Sequence[models.Patient]:
"""
Retrieve the Patients by their reference ids or raise a 422 error response.
"""
patients = service.get_patients_by_reference_ids(session, *reference_ids)
if None in patients:
raise fastapi.HTTPException(
status_code=fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[
{"loc": ["body", "patients"], "msg": "Invalid patient reference id", "type": "value_error"}
],
)
return patients # type: ignore


@router.post(
"",
summary="Create a new Person cluster",
status_code=fastapi.status.HTTP_201_CREATED,
)
def create_person(
data: typing.Annotated[schemas.PatientRefs, fastapi.Body()],
session: orm.Session = fastapi.Depends(get_session),
) -> schemas.PersonRef:
"""
Create a new Person in the MPI database and link the Patients to them.
"""
patients = patients_by_id_or_422(session, data.patients)

person = service.update_person_cluster(session, patients, commit=False)
return schemas.PersonRef(person_reference_id=person.reference_id)


@router.patch(
"/{person_reference_id}",
summary="Assign Patients to existing Person",
status_code=fastapi.status.HTTP_200_OK,
)
def update_person(
person_reference_id: uuid.UUID,
data: typing.Annotated[schemas.PatientRefs, fastapi.Body()],
session: orm.Session = fastapi.Depends(get_session),
) -> schemas.PersonRef:
"""
Assign the Patients to an existing Person cluster.
"""
person = service.get_person_by_reference_id(session, person_reference_id)
if person is None:
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)
patients = patients_by_id_or_422(session, data.patients)

person = service.update_person_cluster(session, patients, person, commit=False)
return schemas.PersonRef(person_reference_id=person.reference_id)
2 changes: 2 additions & 0 deletions src/recordlinker/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .mpi import PatientCreatePayload
from .mpi import PatientPersonRef
from .mpi import PatientRef
from .mpi import PatientRefs
from .mpi import PatientUpdatePayload
from .mpi import PersonRef
from .pii import Feature
Expand Down Expand Up @@ -40,6 +41,7 @@
"PersonRef",
"PatientRef",
"PatientPersonRef",
"PatientRefs",
"PatientCreatePayload",
"PatientUpdatePayload",
"Cluster",
Expand Down
4 changes: 4 additions & 0 deletions src/recordlinker/schemas/mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class PatientPersonRef(pydantic.BaseModel):
person_reference_id: uuid.UUID


class PatientRefs(pydantic.BaseModel):
patients: list[uuid.UUID] = pydantic.Field(..., min_length=1)


class PatientCreatePayload(pydantic.BaseModel):
person_reference_id: uuid.UUID
record: PIIRecord
Expand Down
Loading
Loading