Skip to content

Commit e89fdda

Browse files
authored
endpoints to update person cluster on patient (#101)
## Description Adding two new endpoints to support updating the Person cluster a Patient belongs to. One is for assigning the Patient to a specific cluster, and another is for assigning them to a new cluster. ## Related Issues closes #96 ## Additional Notes - `POST /patient/<ref-id>/person` takes an empty payload, creates a new Person cluster, assigns the Patient to that cluster and returns back reference ids for both the Patient and Person. - `PATCH /patient/<ref-id>/person` takes a person ref id payload, assigned the Patient to that existing Person and returns back reference ids for both the Patient and Person.
1 parent 2533adb commit e89fdda

File tree

9 files changed

+588
-40
lines changed

9 files changed

+588
-40
lines changed

docs/mpi-design.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,22 @@ The following diagram illustrates the relationships between the `Person`, `Patie
2525
```mermaid
2626
erDiagram
2727
Person {
28-
bigint id PK "Primary Key"
29-
uuid internal_id "Internal UUID"
28+
bigint id PK "Primary Key (auto-generated)"
29+
uuid reference_id "Reference UUID (auto-generated)"
3030
}
3131
3232
Patient {
33-
bigint id PK "Primary Key"
33+
bigint id PK "Primary Key (auto-generated)"
3434
bigint person_id FK "Foreign Key to Person"
35-
json data "Patient Data"
35+
json data "Patient Data JSON Object"
36+
uuid reference_id "Reference UUID (auto-generated)"
3637
string external_patient_id "External Patient ID"
3738
string external_person_id "External Person ID"
3839
string external_person_source "External Person Source"
3940
}
4041
4142
BlockingValue {
42-
bigint id PK "Primary Key"
43+
bigint id PK "Primary Key (auto-generated)"
4344
bigint patient_id FK "Foreign Key to Patient"
4445
smallint blockingkey "Blocking Key Type"
4546
string value "Blocking Value"

src/recordlinker/linking/mpi_service.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
"""
77

88
import typing
9+
import uuid
910

1011
from sqlalchemy import orm
12+
from sqlalchemy import select
1113
from sqlalchemy.sql import expression
1214

1315
from recordlinker import models
@@ -29,7 +31,7 @@ def get_block_data(
2931
# has a matching Blocking Value for all the Blocking Keys, then it
3032
# is considered a match.
3133
for idx, key_id in enumerate(algorithm_pass.blocking_keys):
32-
#get the BlockingKey obj from the id
34+
# get the BlockingKey obj from the id
3335
if not hasattr(models.BlockingKey, key_id):
3436
raise ValueError(f"No BlockingKey with id {id} found.")
3537
key = getattr(models.BlockingKey, key_id)
@@ -109,3 +111,40 @@ def insert_blocking_keys(
109111
if commit:
110112
session.commit()
111113
return values
114+
115+
116+
def get_patient_by_reference_id(
117+
session: orm.Session, reference_id: uuid.UUID
118+
) -> models.Patient | None:
119+
"""
120+
Retrieve the Patient by their reference id
121+
"""
122+
query = select(models.Patient).where(models.Patient.reference_id == reference_id)
123+
return session.scalar(query)
124+
125+
126+
def get_person_by_reference_id(
127+
session: orm.Session, reference_id: uuid.UUID
128+
) -> models.Person | None:
129+
"""
130+
Retrieve the Person by their reference id
131+
"""
132+
query = select(models.Person).where(models.Person.reference_id == reference_id)
133+
return session.scalar(query)
134+
135+
136+
def update_person_cluster(
137+
session: orm.Session,
138+
patient: models.Patient,
139+
person: models.Person | None = None,
140+
commit: bool = True,
141+
) -> models.Person:
142+
"""
143+
Update the cluster for a given patient.
144+
"""
145+
patient.person = person or models.Person()
146+
session.flush()
147+
148+
if commit:
149+
session.commit()
150+
return patient.person

src/recordlinker/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from recordlinker.linking import algorithm_service
2424
from recordlinker.linking import link
2525
from recordlinker.routes.algorithm_router import router as algorithm_router
26+
from recordlinker.routes.patient_router import router as patient_router
2627

2728
# Instantiate FastAPI via DIBBs' BaseService class
2829
app = BaseService(
@@ -35,6 +36,7 @@
3536
app.add_middleware(middleware.CorrelationIdMiddleware)
3637
app.add_middleware(middleware.AccessLogMiddleware)
3738
app.include_router(algorithm_router, prefix="/algorithm", tags=["algorithm"])
39+
app.include_router(patient_router, prefix="/patient", tags=["patient"])
3840

3941

4042
# Request and response models

src/recordlinker/models/mpi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Person(Base):
1919
__tablename__ = "mpi_person"
2020

2121
id: orm.Mapped[int] = orm.mapped_column(get_bigint_pk(), autoincrement=True, primary_key=True)
22-
reference_id: orm.Mapped[uuid.UUID] = orm.mapped_column(default=uuid.uuid4)
22+
reference_id: orm.Mapped[uuid.UUID] = orm.mapped_column(default=uuid.uuid4, unique=True, index=True)
2323
patients: orm.Mapped[list["Patient"]] = orm.relationship(back_populates="person")
2424

2525
def __hash__(self):
@@ -50,7 +50,7 @@ class Patient(Base):
5050
external_person_id: orm.Mapped[str] = orm.mapped_column(sqltypes.String(255), nullable=True)
5151
external_person_source: orm.Mapped[str] = orm.mapped_column(sqltypes.String(100), nullable=True)
5252
blocking_values: orm.Mapped[list["BlockingValue"]] = orm.relationship(back_populates="patient")
53-
reference_id: orm.Mapped[uuid.UUID] = orm.mapped_column(default=uuid.uuid4)
53+
reference_id: orm.Mapped[uuid.UUID] = orm.mapped_column(default=uuid.uuid4, unique=True, index=True)
5454

5555
@classmethod
5656
def _scrub_empty(cls, data: dict) -> dict:
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
recordlinker.routes.patient_router
3+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4+
5+
This module implements the patient router for the RecordLinker API. Exposing
6+
the patient API endpoints.
7+
"""
8+
9+
import uuid
10+
11+
import fastapi
12+
import sqlalchemy.orm as orm
13+
14+
from recordlinker import schemas
15+
from recordlinker.database import get_session
16+
from recordlinker.linking import mpi_service as service
17+
18+
router = fastapi.APIRouter()
19+
20+
21+
@router.post(
22+
"/{patient_reference_id}/person",
23+
summary="Assign Patient to new Person",
24+
status_code=fastapi.status.HTTP_201_CREATED,
25+
)
26+
def create_person(
27+
patient_reference_id: uuid.UUID, session: orm.Session = fastapi.Depends(get_session)
28+
) -> schemas.PatientPersonRef:
29+
"""
30+
Create a new Person in the MPI database and link the Patient to them.
31+
"""
32+
patient = service.get_patient_by_reference_id(session, patient_reference_id)
33+
if patient is None:
34+
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)
35+
36+
person = service.update_person_cluster(session, patient, commit=False)
37+
return schemas.PatientPersonRef(
38+
patient_reference_id=patient.reference_id, person_reference_id=person.reference_id
39+
)
40+
41+
42+
@router.patch(
43+
"/{patient_reference_id}/person",
44+
summary="Assign Patient to existing Person",
45+
status_code=fastapi.status.HTTP_200_OK,
46+
)
47+
def update_person(
48+
patient_reference_id: uuid.UUID,
49+
data: schemas.PersonRef,
50+
session: orm.Session = fastapi.Depends(get_session),
51+
) -> schemas.PatientPersonRef:
52+
"""
53+
Update the Person linked on the Patient.
54+
"""
55+
patient = service.get_patient_by_reference_id(session, patient_reference_id)
56+
if patient is None:
57+
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_404_NOT_FOUND)
58+
59+
person = service.get_person_by_reference_id(session, data.person_reference_id)
60+
if person is None:
61+
raise fastapi.HTTPException(status_code=fastapi.status.HTTP_400_BAD_REQUEST)
62+
63+
person = service.update_person_cluster(session, patient, person, commit=False)
64+
return schemas.PatientPersonRef(
65+
patient_reference_id=patient.reference_id, person_reference_id=person.reference_id
66+
)

src/recordlinker/schemas/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from .algorithm import Algorithm
22
from .algorithm import AlgorithmPass
33
from .algorithm import AlgorithmSummary
4+
from .mpi import PatientPersonRef
5+
from .mpi import PersonRef
46
from .pii import Feature
57
from .pii import PIIRecord
68

@@ -10,4 +12,6 @@
1012
"AlgorithmSummary",
1113
"Feature",
1214
"PIIRecord",
15+
"PersonRef",
16+
"PatientPersonRef",
1317
]

src/recordlinker/schemas/mpi.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import uuid
2+
3+
import pydantic
4+
5+
6+
class PersonRef(pydantic.BaseModel):
7+
person_reference_id: uuid.UUID
8+
9+
10+
class PatientPersonRef(pydantic.BaseModel):
11+
patient_reference_id: uuid.UUID
12+
person_reference_id: uuid.UUID

0 commit comments

Comments
 (0)