Skip to content

Commit

Permalink
person cluster patient assignment endpoints (#172)
Browse files Browse the repository at this point in the history
## Description
Two new person API endpoints for assigning patients to different person
clusters.

## Related Issues
closes #158

## Additional Notes
- Deprecating existing create_person and update_person routes in the
patient router
- Created create_person and update_person in the person router to
replace ones in patient router
- Updated two mpi_service functions to accept a list of patients rather
than just 1
- regrouping all patient, person and seeding api endpoints into "mpi"
tag in api docs
  • Loading branch information
ericbuckley authored Feb 6, 2025
1 parent 04578b2 commit 09294ac
Show file tree
Hide file tree
Showing 8 changed files with 317 additions and 109 deletions.
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

0 comments on commit 09294ac

Please sign in to comment.