Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .github/workflows/atomic-ehr-codegen-python-us-core-profiles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: atomic-ehr-codegen-python-us-core-profiles

on:
push:
branches:
- main
paths:
- 'developer-experience/atomic-ehr-codegen-python-us-core-profiles/**'
- '.github/workflows/atomic-ehr-codegen-python-us-core-profiles.yaml'
pull_request:
paths:
- 'developer-experience/atomic-ehr-codegen-python-us-core-profiles/**'
- '.github/workflows/atomic-ehr-codegen-python-us-core-profiles.yaml'
workflow_dispatch:

defaults:
run:
working-directory: developer-experience/atomic-ehr-codegen-python-us-core-profiles

jobs:
test:
runs-on: ubuntu-latest

services:
aidbox_db:
image: healthsamurai/aidboxdb:17
env:
POSTGRES_USER: aidbox
POSTGRES_PASSWORD: aidboxpwd
POSTGRES_DB: aidbox
options: >-
--health-cmd "pg_isready -U aidbox"
--health-interval 10s
--health-timeout 5s
--health-retries 10

aidbox:
image: healthsamurai/aidboxone:edge
ports:
- 8080:8080
env:
AIDBOX_LICENSE: ${{ secrets.AIDBOX_LICENSE }}
AIDBOX_CLIENT_ID: root
AIDBOX_CLIENT_SECRET: secret
AIDBOX_ADMIN_PASSWORD: secret
AIDBOX_PORT: 8080
AIDBOX_FHIR_PACKAGES: hl7.fhir.r4.core#4.0.1
AIDBOX_FHIR_SCHEMA_VALIDATION: true
BOX_SETTINGS_MODE: read-write
PGHOST: aidbox_db
PGPORT: 5432
PGUSER: aidbox
PGPASSWORD: aidboxpwd
PGDATABASE: aidbox

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.12.0'

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install Node dependencies
run: npm install

- name: Install @atomic-ehr/codegen 0.0.17
run: npm install --save-dev @atomic-ehr/codegen@0.0.17

- name: Regenerate fhir_types
run: npx tsx generate.ts

- name: Check generated fhir_types are up to date
run: |
if [ -n "$(git status --porcelain -- fhir_types)" ]; then
echo "::error::fhir_types/ changed after regeneration. Run 'npx tsx generate.ts' and commit the result."
git status --porcelain -- fhir_types
git diff -- fhir_types
exit 1
fi

- name: Install Python dependencies
run: pip install -r fhir_types/requirements.txt

- name: Typecheck
run: mypy .

- name: Build bundle
run: |
out=$(python load.py)
echo "$out"
echo "$out" | grep -qF 'Wrote bundle with 10 entries'

- name: Compute average BP and check output
run: |
out=$(python avg.py)
echo "$out"
echo "$out" | grep -qF 'Avg BP: 125.2/82.0 mmHg (n=5)'

- name: Wait for Aidbox
run: |
for i in $(seq 1 60); do
if curl -fsS http://localhost:8080/health >/dev/null 2>&1; then
echo "Aidbox is up"; exit 0
fi
echo "waiting for Aidbox ($i)"; sleep 3
done
echo "Aidbox did not become healthy in time"; exit 1

- name: POST bundle to Aidbox with fhirpy and read it back
env:
AIDBOX_URL: http://localhost:8080/fhir
AIDBOX_SECRET: secret
run: |
out=$(python post.py)
echo "$out"
echo "$out" | grep -qF 'Stored BP observations: 5'
118 changes: 118 additions & 0 deletions .github/workflows/atomic-ehr-codegen-typescript-us-core-profiles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: atomic-ehr-codegen-typescript-us-core-profiles

on:
push:
branches:
- main
paths:
- 'developer-experience/atomic-ehr-codegen-typescript-us-core-profiles/**'
- '.github/workflows/atomic-ehr-codegen-typescript-us-core-profiles.yaml'
pull_request:
paths:
- 'developer-experience/atomic-ehr-codegen-typescript-us-core-profiles/**'
- '.github/workflows/atomic-ehr-codegen-typescript-us-core-profiles.yaml'
workflow_dispatch:

defaults:
run:
working-directory: developer-experience/atomic-ehr-codegen-typescript-us-core-profiles

jobs:
test:
runs-on: ubuntu-latest

services:
aidbox_db:
image: healthsamurai/aidboxdb:17
env:
POSTGRES_USER: aidbox
POSTGRES_PASSWORD: aidboxpwd
POSTGRES_DB: aidbox
options: >-
--health-cmd "pg_isready -U aidbox"
--health-interval 10s
--health-timeout 5s
--health-retries 10

aidbox:
image: healthsamurai/aidboxone:edge
ports:
- 8080:8080
env:
AIDBOX_LICENSE: ${{ secrets.AIDBOX_LICENSE }}
AIDBOX_CLIENT_ID: root
AIDBOX_CLIENT_SECRET: secret
AIDBOX_ADMIN_PASSWORD: secret
AIDBOX_PORT: 8080
AIDBOX_FHIR_PACKAGES: hl7.fhir.r4.core#4.0.1
AIDBOX_FHIR_SCHEMA_VALIDATION: true
BOX_SETTINGS_MODE: read-write
PGHOST: aidbox_db
PGPORT: 5432
PGUSER: aidbox
PGPASSWORD: aidboxpwd
PGDATABASE: aidbox

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.12.0'

- name: Install dependencies
run: npm install

- name: Install @atomic-ehr/codegen 0.0.17
run: npm install --save-dev @atomic-ehr/codegen@0.0.17

- name: Regenerate fhir-types
run: npx tsx generate.ts

- name: Check generated fhir-types are up to date
run: |
if [ -n "$(git status --porcelain -- fhir-types)" ]; then
echo "::error::fhir-types/ changed after regeneration. Run 'npx tsx generate.ts' and commit the result."
git status --porcelain -- fhir-types
git diff -- fhir-types
exit 1
fi

- name: Typecheck (including generated types)
run: npx tsc --noEmit

- name: Build bundle
run: |
out=$(npx tsx load.ts)
echo "$out"
echo "$out" | grep -qF 'Wrote bundle with 10 entries'

- name: Compute average BP and check output
run: |
out=$(npx tsx avg.ts)
echo "$out"
echo "$out" | grep -qF 'Avg BP: 125.2/82.0 mmHg (n=5)'

- name: Wait for Aidbox
run: |
for i in $(seq 1 60); do
if curl -fsS http://localhost:8080/health >/dev/null 2>&1; then
echo "Aidbox is up"; exit 0
fi
echo "waiting for Aidbox ($i)"; sleep 3
done
echo "Aidbox did not become healthy in time"; exit 1

- name: POST bundle to Aidbox and read it back
run: |
curl -fsS -u "root:secret" -X POST \
-H "Content-Type: application/fhir+json" \
--data @bundle.json http://localhost:8080/fhir > tx-response.json
echo "Transaction type: $(node -e "console.log(require('./tx-response.json').type)")"

curl -fsS -u "root:secret" \
"http://localhost:8080/fhir/Observation?code=http://loinc.org|85354-9" > obs.json
total=$(node -e "console.log(require('./obs.json').total)")
echo "Stored BP observations: $total"
test "$total" = "5"
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# US Core Profiles in Python with @atomic-ehr/codegen

A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) profile class generation for US Core. The Python counterpart of [atomic-ehr-codegen-typescript-us-core-profiles](../atomic-ehr-codegen-typescript-us-core-profiles), generated for **Pydantic** with the **[fhirpy](https://github.com/beda-software/fhirpy)** async client enabled (`fhirpyClient: true`).
A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) profile class generation for US Core. The Python counterpart of [atomic-ehr-codegen-typescript-us-core-profiles](../atomic-ehr-codegen-typescript-us-core-profiles), generated for **Pydantic** with the **[fhirpy](https://github.com/beda-software/fhirpy)** async client enabled (`client: "fhirpy"`).

The example:

Expand Down Expand Up @@ -73,7 +73,7 @@ python post.py
## Notes on the Code

- **The generator is a Node tool; the output is Python.** `generate.ts` runs once to emit `fhir_types/`. After that you only need Python + Pydantic (and fhirpy for `post.py`).
- **`fhirpyClient: true`** makes the generated resources extend `FhirpyBaseModel`: they expose `resourceType` at class level and serialize via `model_dump`, which is everything fhirpy's typed client needs to `create` / `search` / `fetch` them.
- **`client: "fhirpy"`** makes the generated resources extend `FhirpyBaseModel`: they expose `resourceType` at class level and serialize via `model_dump`, which is everything fhirpy's typed client needs to `create` / `search` / `fetch` them.
- **camelCase attributes (`fieldFormat: "camelCase"`).** Resource fields use the FHIR wire names (`resourceType`, `birthDate`, `effectiveDateTime`), so attribute names match the JSON. This is what lets `client.resources(Observation).search(...).fetch()` be statically typed: fhirpy's `ResourceProtocol` looks for a `resourceType` attribute, which only exists as a real attribute under camelCase. (The generator also supports `snake_case`, but then `resourceType` is injected only at runtime and fhirpy's typed client can't see it statically.) Profile **method** names stay snake_case (`set_systolic`, `set_race`, `from_resource`).
- **Must-support base fields** (`gender`, `birthDate`) aren't profiled further by US Core, so the profile class emits no `.set_gender()`-style setters. `load.py` sets them on the base `Patient`, then calls `UscorePatientProfile.apply()`. `validate()` warns if a must-support field is missing.
- **No `is()` type guard.** Unlike the TypeScript API, the Python classes don't ship a `.filter()`-style guard. `avg.py` selects BP observations by `resourceType` + `meta.profile`, then calls `from_resource()`.
Loading
Loading