Skip to content

Commit e66bb7c

Browse files
authored
Merge pull request #33 from Aidbox/atomic-ehr-codegen-python-us-core-profiles-update
Atomic ehr codegen python us core profiles update
2 parents 71b3e51 + 3c6197f commit e66bb7c

8 files changed

Lines changed: 136 additions & 75 deletions

File tree

developer-experience/atomic-ehr-codegen-python-us-core-profiles/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://gith
55
The example:
66

77
1. generates profile classes for [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) and [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) plus base `Bundle` from `hl7.fhir.r4.core`,
8-
2. loads `patients.csv` (5 rows: MRN, name, demographics, race, one BP reading each),
8+
2. loads `patients.csv` (each row: MRN, name, demographics, race, one BP reading each),
99
3. converts each row into a validated `UscorePatientProfile` + `UscoreBloodPressureProfile`,
1010
4. packages them as a `Bundle[Patient | Observation]` transaction with `urn:uuid` cross-references,
1111
5. reads `bundle.json` back, selects the US Core BP observations, and prints the average BP.
@@ -55,6 +55,8 @@ Run [Aidbox](https://www.health-samurai.io/fhir-server) locally:
5555
curl -JO https://aidbox.app/runme && docker compose up -d
5656
```
5757

58+
Open `http://localhost:8080` and issue license if not already done.
59+
5860
Then POST `bundle.json` and read the stored observations back as typed resources:
5961

6062
```bash

developer-experience/atomic-ehr-codegen-python-us-core-profiles/avg.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
def is_us_core_bp(resource: dict) -> bool:
1010
# Python profiles have no `is()` type guard (unlike the TS API); select on
1111
# resourceType + meta.profile, then hand the survivors to from_resource().
12-
return (
13-
resource.get("resourceType") == "Observation"
14-
and UscoreBloodPressureProfile.canonical_url in (resource.get("meta", {}).get("profile") or [])
12+
return resource.get(
13+
"resourceType"
14+
) == "Observation" and UscoreBloodPressureProfile.canonical_url in (
15+
resource.get("meta", {}).get("profile") or []
1516
)
1617

1718

@@ -20,16 +21,24 @@ def main() -> None:
2021
bundle = json.load(f)
2122

2223
bps = [
23-
UscoreBloodPressureProfile.from_resource(Observation.model_validate(entry["resource"]))
24+
UscoreBloodPressureProfile.from_resource(
25+
Observation.model_validate(entry["resource"])
26+
)
2427
for entry in bundle.get("entry", [])
2528
if is_us_core_bp(entry["resource"])
2629
]
2730

2831
def avg(xs: list[float]) -> float:
2932
return sum(xs) / len(xs)
3033

31-
systolic = [bp.get_systolic()["value"] for bp in bps]
32-
diastolic = [bp.get_diastolic()["value"] for bp in bps]
34+
systolic = [
35+
systolic["value"] for bp in bps if (systolic := bp.get_systolic()) is not None
36+
]
37+
diastolic = [
38+
diastolic["value"]
39+
for bp in bps
40+
if (diastolic := bp.get_diastolic()) is not None
41+
]
3342

3443
print(f"Avg BP: {avg(systolic):.1f}/{avg(diastolic):.1f} mmHg (n={len(bps)})")
3544

developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/README.md

Lines changed: 48 additions & 48 deletions
Large diffs are not rendered by default.

developer-experience/atomic-ehr-codegen-python-us-core-profiles/generate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const main = async () => {
1313
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
1414
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {},
1515
},
16-
"hl7.fhir.r4.core": {
16+
"hl7.fhir.r4.core": { // hl7.fhir.us.core depends on hl7.fhir.r4.core
1717
"http://hl7.org/fhir/StructureDefinition/Bundle": {},
1818
},
1919
},

developer-experience/atomic-ehr-codegen-python-us-core-profiles/load.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
import uuid
66
import warnings
77

8-
from fhir_types.hl7_fhir_r4_core.base import Identifier, HumanName, Reference
9-
from fhir_types.hl7_fhir_r4_core.patient import Patient
10-
from fhir_types.hl7_fhir_r4_core.observation import Observation
11-
from fhir_types.hl7_fhir_r4_core.bundle import Bundle, BundleEntry, BundleEntryRequest
8+
from fhir_types.hl7_fhir_r4_core import (
9+
Identifier,
10+
HumanName,
11+
Reference,
12+
Patient,
13+
Observation,
14+
Bundle,
15+
BundleEntry,
16+
BundleEntryRequest,
17+
)
18+
1219
from fhir_types.hl7_fhir_us_core.profiles import (
1320
UscorePatientProfile,
1421
UscoreBloodPressureProfile,
@@ -22,16 +29,22 @@
2229
def row_to_patient(row: dict[str, str]) -> UscorePatientProfile:
2330
base_patient = Patient(
2431
resource_type="Patient",
25-
identifier=[Identifier(system="http://hospital.example.org/mrn", value=row["mrn"])],
32+
identifier=[
33+
Identifier(system="http://hospital.example.org/mrn", value=row["mrn"])
34+
],
2635
name=[HumanName(family=row["family"], given=[row["given"]])],
27-
gender=row["gender"], # Literal-typed; Pydantic validates the value
28-
birth_date=row["birthDate"], # snake_case attr; serializes back to "birthDate"
36+
gender=row["gender"], # Literal-typed; Pydantic validates the value
37+
birth_date=row["birthDate"], # snake_case attr; serializes back to "birthDate"
2938
)
3039

3140
patient = UscorePatientProfile.apply(base_patient)
3241
patient.set_race(
3342
{
34-
"ombCategory": {"system": "urn:oid:2.16.840.1.113883.6.238", "code": row["raceCode"], "display": row["raceDisplay"]},
43+
"ombCategory": {
44+
"system": "urn:oid:2.16.840.1.113883.6.238",
45+
"code": row["raceCode"],
46+
"display": row["raceDisplay"],
47+
},
3548
"text": row["raceDisplay"],
3649
}
3750
)
@@ -46,8 +59,22 @@ def row_to_bp(row: dict[str, str], patient_urn: str) -> UscoreBloodPressureProfi
4659

4760
(
4861
bp.set_effective_date_time(row["effectiveDateTime"])
49-
.set_systolic({"value": float(row["systolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
50-
.set_diastolic({"value": float(row["diastolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
62+
.set_systolic(
63+
{
64+
"value": float(row["systolic"]),
65+
"unit": "mmHg",
66+
"system": "http://unitsofmeasure.org",
67+
"code": "mm[Hg]",
68+
}
69+
)
70+
.set_diastolic(
71+
{
72+
"value": float(row["diastolic"]),
73+
"unit": "mmHg",
74+
"system": "http://unitsofmeasure.org",
75+
"code": "mm[Hg]",
76+
}
77+
)
5178
)
5279

5380
errors = bp.validate()["errors"]
@@ -62,10 +89,16 @@ def row_to_entries(row: dict[str, str]) -> list[BundleEntry[Patient | Observatio
6289
bp = row_to_bp(row, patient_urn)
6390

6491
return [
65-
BundleEntry(full_url=patient_urn, resource=patient.to_resource(),
66-
request=BundleEntryRequest(method="POST", url="Patient")),
67-
BundleEntry(full_url=f"urn:uuid:{uuid.uuid4()}", resource=bp.to_resource(),
68-
request=BundleEntryRequest(method="POST", url="Observation")),
92+
BundleEntry(
93+
full_url=patient_urn,
94+
resource=patient.to_resource(),
95+
request=BundleEntryRequest(method="POST", url="Patient"),
96+
),
97+
BundleEntry(
98+
full_url=f"urn:uuid:{uuid.uuid4()}",
99+
resource=bp.to_resource(),
100+
request=BundleEntryRequest(method="POST", url="Observation"),
101+
),
69102
]
70103

71104

@@ -82,7 +115,10 @@ def main() -> None:
82115

83116
with open("bundle.json", "w") as f:
84117
json.dump(bundle.model_dump(by_alias=True, exclude_none=True), f, indent=2)
85-
print(f"Wrote bundle with {len(bundle.entry)} entries")
118+
entries = bundle.entry
119+
if entries is None:
120+
raise ValueError("entries is None")
121+
print(f"Wrote bundle with {len(entries)} entries")
86122

87123

88124
if __name__ == "__main__":

developer-experience/atomic-ehr-codegen-python-us-core-profiles/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"generate": "tsx generate.ts"
99
},
1010
"devDependencies": {
11-
"@atomic-ehr/codegen": "^0.0.15",
11+
"@atomic-ehr/codegen": "^0.0.15-canary.20260616131237.0f30eea",
1212
"@types/node": "^25.6.0",
1313
"tsx": "^4.21.0",
1414
"typescript": "^5.9.3"

developer-experience/atomic-ehr-codegen-python-us-core-profiles/post.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@
2626

2727

2828
def make_client() -> AsyncFHIRClient:
29-
url = os.environ.get("FHIR_URL") or os.environ.get("AIDBOX_URL", "http://localhost:8080/fhir")
30-
dump = lambda r: r.model_dump(by_alias=True, exclude_none=True)
29+
url = (
30+
os.environ.get("FHIR_URL")
31+
or os.environ.get("AIDBOX_URL")
32+
or "http://localhost:8080/fhir"
33+
)
34+
35+
def dump(r):
36+
return r.model_dump(by_alias=True, exclude_none=True)
3137

3238
# Basic auth when a secret is provided (Aidbox); otherwise an open server.
3339
secret = os.environ.get("AIDBOX_SECRET")
@@ -50,7 +56,11 @@ async def main() -> None:
5056
print("Transaction committed:", response.get("type"))
5157

5258
# Read the stored US Core Blood Pressure observations back as typed resources.
53-
observations = await client.resources(Observation).search(code="http://loinc.org|85354-9").fetch()
59+
observations = (
60+
await client.resources(Observation)
61+
.search(code="http://loinc.org|85354-9")
62+
.fetch()
63+
)
5464
print(f"Stored BP observations: {len(observations)}")
5565
for obs in observations:
5666
assert isinstance(obs, Observation)

developer-experience/atomic-ehr-codegen-typescript-us-core-profiles/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ Run [Aidbox](https://www.health-samurai.io/fhir-server) locally and POST `bundle
4747
```bash
4848
curl -JO https://aidbox.app/runme && docker compose up -d
4949
SECRET=$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)
50+
```
51+
52+
Open `http://localhost:8080` and issue license if not already done.
5053

54+
```bash
5155
curl -u "root:$SECRET" -X POST -H "Content-Type: application/fhir+json" \
5256
-d @bundle.json http://localhost:8080/fhir
5357
```

0 commit comments

Comments
 (0)