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 Endpoint for getting orphaned patients #222

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
24 changes: 24 additions & 0 deletions src/recordlinker/database/mpi_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,27 @@ def check_person_for_patients(session: orm.Session, person: models.Person) -> bo
"""
query = select(literal(1)).filter(models.Patient.person_id == person.id).limit(1)
return True if session.execute(query).scalar() is not None else False


def get_orphaned_patients(
session: orm.Session,
limit: int | None = 50,
cursor: str | None = None,
) -> typing.Sequence[models.Patient]:
"""
Retrieve orphaned Patients in the MPI database, up to the provided limit. If a
cursor (in the form of a patient reference_id) is provided, only retrieve Patients
with a reference_id greater than the cursor.
"""
query = (
select(models.Patient)
.where(models.Patient.person_id.is_(None))
.order_by(models.Patient.reference_id)
.limit(limit)
)

# Apply cursor if provided
if cursor:
query = query.where(models.Patient.reference_id > cursor)

return session.execute(query).scalars().all()
30 changes: 30 additions & 0 deletions src/recordlinker/routes/patient_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ def create_patient(
)


@router.get(
"/orphaned", summary="Retrieve orphaned patients", status_code=fastapi.status.HTTP_200_OK
)
def get_orphaned_patients(
request: fastapi.Request,
session: orm.Session = fastapi.Depends(get_session),
limit: int | None = fastapi.Query(50, alias="limit", ge=1, le=100),
cursor: str | None = fastapi.Query(None, alias="cursor"),
) -> schemas.PaginatedPatientRefs:
"""
Retrieve patient_reference_id(s) for all Patients that are not linked to a Person.
"""
patients = service.get_orphaned_patients(session, limit, cursor)

if not patients:
return schemas.PaginatedPatientRefs(patients=[], meta=None)

# Prepare the meta data
next_cursor = patients[-1].reference_id if len(patients) == limit else None
base_url = str(request.url).split("?")[0]
next_url = f"{base_url}?limit={limit}&cursor={next_cursor}" if next_cursor else None

return schemas.PaginatedPatientRefs(
patients=schemas.PatientRefs(
patients=[p.reference_id for p in patients if p.reference_id is not None]
),
meta=schemas.PaginatedMetaData(next_cursor=str(next_cursor), next=next_url),
)


@router.get(
"/{patient_reference_id}",
summary="Retrieve a patient record",
Expand Down
4 changes: 4 additions & 0 deletions src/recordlinker/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from .link import Prediction
from .mpi import ErrorDetail
from .mpi import ErrorResponse
from .mpi import PaginatedMetaData
from .mpi import PaginatedPatientRefs
from .mpi import PatientCreatePayload
from .mpi import PatientInfo
from .mpi import PatientPersonRef
Expand Down Expand Up @@ -58,4 +60,6 @@
"PersonRefs",
"ErrorDetail",
"ErrorResponse",
"PaginatedMetaData",
"PaginatedPatientRefs",
]
10 changes: 10 additions & 0 deletions src/recordlinker/schemas/mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,13 @@ class ErrorResponse(pydantic.BaseModel):
"""

detail: list[ErrorDetail]


class PaginatedMetaData(pydantic.BaseModel):
next_cursor: str | None = None
next: str | None = None


class PaginatedPatientRefs(pydantic.BaseModel):
patients: PatientRefs
meta: PaginatedMetaData | None
56 changes: 56 additions & 0 deletions tests/unit/database/test_mpi_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,3 +892,59 @@ def test_check_person_for_patients(self, session):
assert session.query(models.Person).count() == 2
assert mpi_service.check_person_for_patients(session, person1)
assert not mpi_service.check_person_for_patients(session, person2)


class TestGetOrphanedPatients:
def test_get_orphaned_patients_success(self, session):
patient = models.Patient(person=None, data={"reference_id": str(uuid.uuid4())})
patient2 = models.Patient(person=models.Person(), data={})
session.add_all([patient, patient2])
session.flush()
assert session.query(models.Patient).count() == 2
assert session.query(models.Person).count() == 1
assert mpi_service.get_orphaned_patients(session) == [patient]

def test_get_orphaned_patients_no_patients(self, session):
person = models.Person()
patient = models.Patient(person=person, data={})
session.add(patient)
session.flush()
assert mpi_service.get_orphaned_patients(session) == []

def test_get_orphaned_patients_limit(self, session):
# Checks that limit is correctly applied
patient1 = models.Patient(person=None, data={"id": 1, "reference_id": str(uuid.uuid4())})
patient2 = models.Patient(person=None, data={"id": 2, "reference_id": str(uuid.uuid4())})
patient3 = models.Patient(person=models.Person(), data={})
session.add_all([patient1, patient2, patient3])
session.flush()

assert len(mpi_service.get_orphaned_patients(session, limit=1)) == 1
assert len(mpi_service.get_orphaned_patients(session, limit=2)) == 2
assert len(mpi_service.get_orphaned_patients(session, limit=3)) == 2

def test_get_orphaned_patients_cursor(self, session):
ordered_uuids = [uuid.uuid4() for _ in range(4)]
ordered_uuids.sort()

patient1 = models.Patient(person=None, reference_id=ordered_uuids[0])
patient2 = models.Patient(person=None, reference_id=ordered_uuids[1])
patient3 = models.Patient(person=None, reference_id=ordered_uuids[2])
patient4 = models.Patient(person=models.Person(), reference_id=ordered_uuids[3])
session.add_all([patient1, patient2, patient3, patient4])
session.flush()

# Checks that cursor is correctly applied
assert mpi_service.get_orphaned_patients(
session, limit=1, cursor=patient1.reference_id
) == [patient2]

assert mpi_service.get_orphaned_patients(
session, limit=1, cursor=patient2.reference_id
) == [patient3]
assert mpi_service.get_orphaned_patients(
session, limit=2, cursor=patient2.reference_id
) == [patient3]
assert mpi_service.get_orphaned_patients(
session, limit=2, cursor=patient1.reference_id
) == [patient2, patient3]
19 changes: 19 additions & 0 deletions tests/unit/routes/test_patient_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,22 @@ def test_get_patient(self, client):
"external_patient_id": "123",
"external_person_id": "456",
}


class TestGetOrphanedPatients:
def test_get_orphaned_patients(self, client):
patient1 = models.Patient()
person2 = models.Person()
patient2 = models.Patient(person=person2)
client.session.add_all([patient1, person2, patient2])
client.session.flush()
response = client.get("/patient/orphaned")
assert response.status_code == 200
assert response.json() == {
"patients": [str(patient1.reference_id)],
}

def test_no_orphaned_patients(self, client):
response = client.get("/patient/orphaned")
assert response.status_code == 200
assert response.json() is None
Loading