|
12 Min. Lesezeit
|

@atomic-ehr/codegen: US Core Profile-Klassen in Python

Diesen Artikel zusammenfassen mit:
ChatGPTPerplexityClaudeGrok

US Core-Ressourcen von Hand zu erstellen ist mühsam. Sie stempeln meta.profile, schlagen LOINC-Codes nach, schreiben die verschachtelte us-core-race-Extension manuell — jedes Feld ist ein Tippfehler, der darauf wartet zu passieren, jedes Profil ist eine eigene Variante derselben Zeremonie.

@atomic-ehr/codegen lässt diesen Boilerplate verschwinden. Verweisen Sie das Tool auf den US Core IG und Sie erhalten ein Pydantic-Modell pro Basistyp sowie eine einfache Python-Wrapper-Klasse pro Profil — mit typisierten Accessoren für feste Werte, Extensions und Slices sowie einem validate(), das weiß, was das Profil erfordert.

Dieses Tutorial führt Sie von Anfang bis Ende durch zwei US Core Profile: US Core Patient und US Core Blood Pressure.

Was Sie erstellen werden

Einen CSV-zu-FHIR-Konverter, Schritt für Schritt:

  1. Profil-Klassen für US Core Patient und US Core Blood Pressure aus hl7.fhir.us.core@8.0.1 generieren,
  2. jede Zeile in einen US Core Patient umwandeln — typisierte Extension-Setter und apply(),
  3. jede Zeile in einen US Core Blood Pressure umwandeln — typisierte Slices, fester LOINC und validate(),
  4. diese als Bundle paketieren,
  5. das Bundle mit typisierten Gettern zurücklesen, um einen durchschnittlichen Blutdruckwert zu berechnen,
  6. das Bundle über den fhirpy-Client an einen lokalen Aidbox-Server senden.

Voraussetzungen

  • Node.js 20+ (oder Bun) — der Generator selbst ist das Node-Paket @atomic-ehr/codegen. Sie führen es einmalig aus, um Python-Code zu erzeugen; danach benötigen Sie Node nicht mehr. Das Generierungsskript umfasst wenige Zeilen TypeScript (siehe unten).
  • Python 3.12+ — der generierte Code zielt auf modernes Python (PEP 604 X | None-Unions, generische Modelle via typing_extensions).
  • Pydantic v2 (pydantic>=2.11) und fhirpy — generierte Modelle sind Pydantic v2; mit dem Standard-fhirpy-Client lassen sie sich auch in fhirpys asynchronen Client einbinden (Schritt 6). Beide sind in der generierten requirements.txt festgelegt (siehe Schritt 1); übergeben Sie client: "none" für reines Pydantic ohne Client-Code.
  • Grundkenntnisse in FHIR und US Core (es genügt zu wissen, was „Profil" und „Slice" bedeuten).

Schritt 1 — Profil-Klassen generieren

Die Code-Generierung läuft über das Node-Tool; richten Sie daher ein kleines Generator-Projekt neben Ihrer Python-Anwendung ein:

mkdir py-us-core-tutorial && cd py-us-core-tutorial
npm init -y
npm install --save-dev @atomic-ehr/codegen tsx typescript

Erstellen Sie generate.ts:

import { APIBuilder, mkCodegenLogger, prettyReport } from "@atomic-ehr/codegen";

const main = async () => {
  const logger = mkCodegenLogger({
    suppressTags: ["#fieldTypeNotFound", "#duplicateSchema", "#duplicateCanonical", "#largeValueSet"],
  });

  const builder = new APIBuilder({ logger })
    .fromPackage("hl7.fhir.us.core", "8.0.1")
    .typeSchema({
      treeShake: {
        "hl7.fhir.us.core": {
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient": {},
          "http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure": {},
        },
        "hl7.fhir.r4.core": {
          "http://hl7.org/fhir/StructureDefinition/Bundle": {},
        },
      },
    })
    .python({
      generateProfile: true,
      allowExtraFields: false,
      primitiveTypeExtension: true,
    })
    .outputTo("./fhir_types")
    .cleanOutput(true);

  const report = await builder.generate();
  console.log(prettyReport(report));
  if (!report.success) process.exit(1);
};

main();

Die relevanten Optionen:

  • generateProfile: true — erzeugt eine Wrapper-Klasse pro Profil mit typisierten Accessoren für Extensions, Slices und feste Werte. Ohne diese Option erhalten Sie nur die Basis-R4-Pydantic-Modelle.
  • allowExtraFields: false — generierte Modelle verwenden Pydantics extra="forbid", sodass ein unbekanntes Feld beim Parsen einen Fehler auslöst, anstatt still ignoriert zu werden.
  • primitiveTypeExtension: true — generiert zusätzlich die FHIR-Primitive-Extension-Geschwister (die _field-Begleiter, z. B. birthDateExtension), damit Sie Extensions und ids an primitive Werte anhängen können.
  • treeShake: { ... } — nur die aufgeführten Canonicals und ihre transitiven Abhängigkeiten werden generiert (~20 Dateien statt Hunderte).

Führen Sie es aus. prettyReport(report) gibt eine gruppierte Zusammenfassung aus, damit Sie sehen, was erzeugt wurde, ohne das Ausgabeverzeichnis zu durchsuchen:

$ npx tsx generate.ts
# generation logs omitted; this is the prettyReport summary
Generated files (24 files, 12 kloc):
  python (23 files, 3.6 kloc):
    - fhir_types/ (4 files, 622 loc)
    - fhir_types/hl7_fhir_r4_core/ (8 files, 1.1 kloc)
    - fhir_types/hl7_fhir_r4_core/profiles/ (2 files, 164 loc)
    - fhir_types/hl7_fhir_us_core/profiles/ (9 files, 1.8 kloc)
  ir-report (1 files, 8.2 kloc):
    - fhir_types/README.md (8223 loc)
Duration: 8097ms
Status: 🟩 Success

Die Verzeichnisstruktur sieht folgendermaßen aus:

fhir_types/
├── hl7_fhir_r4_core/                  # Base R4 Pydantic models
│   ├── base.py                        # Element, Coding, CodeableConcept, Quantity, ...
│   ├── resource.py                    # Resource, DomainResource, Meta
│   ├── patient.py
│   ├── observation.py
│   ├── bundle.py
│   ├── profiles/                      # base R4 profiles US Core builds on
│   │   └── observation_observation_vitalsigns.py   # vital-signs base (BP derives from it)
│   └── ...
├── hl7_fhir_us_core/
│   └── profiles/
│       ├── __init__.py                # re-exports the profile classes
│       ├── patient_uscore_patient_profile.py
│       ├── observation_uscore_blood_pressure_profile.py
│       ├── extension_uscore_race_extension.py
│       └── ...
├── fhirpy_base_model.py               # fhirpy client base model (default fhirpy client)
├── profile_helpers.py                 # Runtime helpers shared by all profile classes
├── README.md                          # IR report — human-readable dump of the generated types
└── requirements.txt                   # pydantic, fhirpy (+ pytest, requests for tests/Step 6)
Python-Virtualumgebung einrichten
python3.14 -m venv venv
source venv/bin/activate

Verweisen Sie Ihre Python-Anwendung auf das erzeugte fhir_types/-Verzeichnis und installieren Sie die Abhängigkeiten:

pip install -r fhir_types/requirements.txt

Die generierte requirements.txt legt Pydantic und fhirpy sowie pytest und requests für Tests und Beispiele fest.

Der vollständige Tutorial-Code befindet sich in Aidbox/examples — generate.ts, load.py, avg.py, post.py, die CSV sowie das committete fhir_types/-Verzeichnis, damit Sie den generierten Code einsehen können, ohne den Generator ausführen zu müssen. Zur umfassenderen Erkundung der Profil-API enthält das Codegen-Repository auch ein python-r4-us-core-Testbeispiel. Beide verwenden standardmäßig camelCase-Attributnamen, genau wie die Code-Schnipsel hier. (Übergeben Sie fieldFormat: "snake_case", wenn Sie Attribute lieber als birth_date, effective_date_time schreiben möchten; die Serialisierung gibt in jedem Fall FHIR-konformes camelCase-JSON aus.)

Schritt 2 — Zeile zu einem US Core Patient

Die Eingabe ist patients.csv — grundlegende demografische Daten plus eine Blutdruckmessung pro Patient. Die Ethnizität verwendet die OMB-Kategorie-Codes, die US Core erwartet:

mrn,family,given,birthDate,gender,raceCode,raceDisplay,effectiveDateTime,systolic,diastolic
MRN-001,Lovelace,Ada,1815-12-10,female,2106-3,White,2026-04-15,120,80
MRN-002,Turing,Alan,1912-06-23,male,2106-3,White,2026-04-15,118,76
MRN-003,Curie,Marie,1867-11-07,female,2106-3,White,2026-04-16,125,82
MRN-004,Carver,George,1864-01-01,male,2054-5,Black or African American,2026-04-16,135,88
MRN-005,Ochoa,Ellen,1958-05-10,female,2054-5,Black or African American,2026-04-17,128,84

csv.DictReader übergibt jede Zeile als einfaches dict[str, str]; die numerische Umwandlung erfolgt später, wenn Werte an typisierte Profil-Setter übergeben werden.

Das US Core Patient-Profil fügt einige Extensions hinzu und macht identifier und name zu Pflichtfeldern. Die generierte Klasse verfügt über einen typisierten Setter für jedes davon:

from fhir_types.hl7_fhir_r4_core.base import Identifier, HumanName, Coding
from fhir_types.hl7_fhir_r4_core.patient import Patient
from fhir_types.hl7_fhir_us_core.profiles import UscorePatientProfile

def row_to_patient(row: dict[str, str]) -> UscorePatientProfile:
    base_patient = Patient(
        resourceType="Patient",
        identifier=[Identifier(system="http://hospital.example.org/mrn", value=row["mrn"])],
        name=[HumanName(family=row["family"], given=[row["given"]])],
        gender=row["gender"],          # gender is a Literal type — Pydantic validates the value
        birthDate=row["birthDate"],    # default camelCase attrs match the FHIR wire names
    )

    patient = UscorePatientProfile.apply(base_patient)

    patient.set_race({
        "ombCategory": {"system": "urn:oid:2.16.840.1.113883.6.238", "code": row["raceCode"], "display": row["raceDisplay"]},
        "text": row["raceDisplay"],
    })

    return patient

Zwei Phasen:

  1. Den einfachen Patient aufbauen — profilpflichtige (identifier, name) und must-support-Felder (gender, birthDate) als typisiertes R4-Pydantic-Modell. Konstruieren Sie mit den standardmäßigen camelCase-Attributnamen (birthDate, die FHIR-Wire-Namen); Werte werden sofort validiert (z. B. ist gender ein Literal["male", "female", "other", "unknown"]).
  2. Dann stampelt UscorePatientProfile.apply(base_patient) meta.profile und gibt eine Profil-Instanz mit typisierten Accessoren für die US Core Extensions zurück. apply() umschließt die Ressource an Ort und Stelle — das Profil mutiert dasselbe Patient-Objekt.

Drei Hinweise dazu, was die Profil-API für Sie erledigt:

  • Drei Setter-Formen für Extensions. set_race({ "ombCategory": ..., "text": ... }) akzeptiert flache Eingabe — beachten Sie, dass die Sub-Extension-Schlüssel (ombCategory, detailed, text) die camelCase-Slice-Namen sind — und erzeugt die verschachtelte extension[]-Struktur. Derselbe Setter akzeptiert auch eine typisierte Extension-Profil-Instanz (UscoreRaceExtension) oder eine rohe Extension und löst einen Fehler aus, wenn die url einer rohen Extension nicht übereinstimmt.
  • Einwertige Extensions erhalten den Wert direkt. us-core-individual-sex trägt ein einzelnes valueCoding, daher nimmt set_sex(Coding(code="female")) eine Coding-Instanz (oder eine rohe Extension) entgegen.
  • Keine Setter für must-support-Basisfelder. gender, birthDate und address werden von US Core nicht weiter profiliert, daher erzeugt die Profil-Klasse keine .set_gender()-artigen Wrapper — befüllen Sie diese als normale Patient-Felder. validate() gibt dennoch eine Warnung aus, wenn ein must-support-Feld fehlt.

Pydantic gibt eine UserWarning aus, wenn eine extension[]-Liste einfache Dicts statt Extension-Instanzen enthält — dies ist beim aktuellen Flat-Dict-Mechanismus zu erwarten. Unterdrücken Sie sie mit warnings.filterwarnings("ignore", category=UserWarning, module="pydantic").

Schritt 3 — Zeile zu einem US Core Blood Pressure

Das BP-Profil ist der Bereich, in dem Codegen seinen größten Nutzen entfaltet. Das US Core Blood Pressure-Profil:

  • legt code auf LOINC 85354-9 fest („Blood pressure panel"),
  • legt einen vital-signs-Kategorie-Slice fest,
  • definiert component[systolic]- und component[diastolic]-Slices mit spezifischen LOINC-Diskriminatoren (8480-6 und 8462-4),
  • erfordert ein effectiveDateTime oder effectivePeriod,
  • erfordert valueQuantity innerhalb jedes Slices.

Das manuell pro Zeile zu schreiben ist genau die Art von Aufgabe, die Codegen eliminiert. Die generierte Klasse reduziert es auf drei Setter:

from fhir_types.hl7_fhir_r4_core.base import Reference
from fhir_types.hl7_fhir_us_core.profiles import UscoreBloodPressureProfile

def row_to_bp(row: dict[str, str], patient_urn: str) -> UscoreBloodPressureProfile:
    bp = UscoreBloodPressureProfile.create(
        status="final",
        subject=Reference(reference=patient_urn),
    )

    (
        bp.set_effective_date_time(row["effectiveDateTime"])
          .set_systolic({"value": float(row["systolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
          .set_diastolic({"value": float(row["diastolic"]), "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"})
    )

    errors = bp.validate()["errors"]
    if errors:
        raise ValueError(f"{row['mrn']}: {'; '.join(errors)}")

    return bp

Was im Hintergrund geschieht:

  • create() erledigt die Zeremonie. Es stempelt meta.profile, füllt den festen code (LOINC 85354-9), hängt den vital-signs-Kategorie-Slice an und fügt leere component[systolic]- / component[diastolic]-Stubs mit bereits gesetzten Diskriminator-Codes hinzu. create() nimmt nur Schlüsselwort-Argumente entgegen; create_resource() ist identisch, gibt aber ein einfaches Observation-Objekt statt eines Profil-Wrappers zurück.
  • set_systolic({ "value": ..., "unit": ... }) füllt die valueQuantity im systolischen Slice. Der Diskriminator-code für diese Komponente ist bereits durch create() gesetzt — Sie liefern nur den Messwert.
  • validate() gibt {"errors": [...], "warnings": [...]} zurück. Fehler blockieren (Pflichtfelder, ausgeschlossene Felder, unzulässige Choice-Varianten, Slice-Kardinalität). Warnungen weisen auf must-support-Probleme hin. Eine fehlerhafte Zeile schlägt sofort mit der MRN fehl — Sie entdecken es nicht erst beim POST.

Sie haben die Diskriminator-Codes nicht eingegeben. Sie mussten sich 85354-9 nicht merken. Die Setter lassen sich fließend verketten (jeder gibt das Profil zurück), genau wie die TypeScript-API.

Schritt 4 — Das Bundle zusammenstellen

Jede Zeile erzeugt einen Patient und eine BP-Observation, die über den urn:uuid-Platzhalter des Patienten verknüpft sind. Paketieren Sie diese als Transaktions-Einträge. Die generierte Bundle- und BundleEntry-Klasse ist generisch über die enthaltene Ressource, sodass ein Bundle[Patient | Observation] entry[].resource auf diese Union typisiert hält. (row_to_patient und row_to_bp sind die Funktionen aus Schritt 2 und 3; im Beispiel befinden sie sich alle in einer load.py und sind daher bereits im Geltungsbereich.)

import json
import csv
import uuid

from fhir_types.hl7_fhir_r4_core.patient import Patient
from fhir_types.hl7_fhir_r4_core.observation import Observation
from fhir_types.hl7_fhir_r4_core.bundle import Bundle, BundleEntry, BundleEntryRequest

def row_to_entries(row: dict[str, str]) -> list[BundleEntry[Patient | Observation]]:
    patient_urn = f"urn:uuid:{uuid.uuid4()}"
    patient = row_to_patient(row)
    bp = row_to_bp(row, patient_urn)

    return [
        BundleEntry(fullUrl=patient_urn, resource=patient.to_resource(),
                    request=BundleEntryRequest(method="POST", url="Patient")),
        BundleEntry(fullUrl=f"urn:uuid:{uuid.uuid4()}", resource=bp.to_resource(),
                    request=BundleEntryRequest(method="POST", url="Observation")),
    ]

rows = list(csv.DictReader(open("patients.csv")))
print(f"Loaded {len(rows)} rows")

entries = [entry for row in rows for entry in row_to_entries(row)]

bundle = Bundle[Patient | Observation](
    resourceType="Bundle",
    type="transaction",
    entry=entries,
)

with open("bundle.json", "w") as f:
    json.dump(bundle.model_dump(by_alias=True, exclude_none=True), f, indent=2)
print(f"Wrote bundle with {len(entries)} entries")
$ python load.py
Loaded 5 rows
Wrote bundle with 10 entries

Wichtige Hinweise:

  • to_resource() liefert das einfache Modell — die zugrunde liegende Pydantic-Ressource ohne Wrapper, bereit zur Verwendung in einem BundleEntry.
  • model_dump(by_alias=True, exclude_none=True) erzeugt FHIR-JSON — by_alias serialisiert über die FHIR-Wire-Aliase (sodass ein snake_case-Build dennoch effectiveDateTime ausgibt) und exclude_none entfernt None-wertige Felder. Dies ist der eine Serialisierungsaufruf, den Sie überall verwenden werden.
  • urn:uuid-Referenzen. Die fullUrl des Patienten und die subject.reference der Observation teilen sich eine UUID; der Server löst diese beim Commit in eine echte ID auf.

Schritt 5 — Rücklesen: Durchschnittlicher Blutdruck aus dem Bundle

Lesen Sie es nun zurück. Parsen Sie bundle.json und berechnen Sie den durchschnittlichen systolischen/diastolischen Wert, um die Lese-API zu erproben:

import json
from typing import Any

from fhir_types.hl7_fhir_r4_core.observation import Observation
from fhir_types.hl7_fhir_us_core.profiles import UscoreBloodPressureProfile

bundle = json.load(open("bundle.json"))

def is_us_core_bp(resource: dict[str, Any]) -> bool:
    return (
        resource.get("resourceType") == "Observation"
        and UscoreBloodPressureProfile.canonical_url in (resource.get("meta", {}).get("profile") or [])
    )

bps = [
    UscoreBloodPressureProfile.from_resource(Observation.model_validate(entry["resource"]))
    for entry in bundle.get("entry", [])
    if is_us_core_bp(entry["resource"])
]

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

# get_systolic()/get_diastolic() are Optional, so guard with a walrus before indexing.
systolic = [s["value"] for bp in bps if (s := bp.get_systolic()) is not None]
diastolic = [d["value"] for bp in bps if (d := bp.get_diastolic()) is not None]

print(f"Avg BP: {avg(systolic):.1f}/{avg(diastolic):.1f} mmHg (n={len(bps)})")
$ python avg.py
Avg BP: 125.2/82.0 mmHg (n=5)

Drei Dinge, die das Profil hier leistet:

  • from_resource(obs) validiert beim Umschließen. Es prüft, ob meta.profile die kanonische URL enthält, und gibt eine Profil-Instanz zurück; ist eine Ressource, die das Profil beansprucht, fehlerhaft, wird eine Ausnahme ausgelöst — ein defektes Bundle schlägt also beim Lesen fehl, nicht erst beim nächsten Feldzugriff.
  • Kein eingebauter Type-Guard. Anders als das TypeScript-API-Prädikat is() liefern die Python-Klassen keinen .filter()-artigen Guard. Sie wählen Kandidaten selbst aus — prüfen Sie resourceType und canonical_url in meta.profile (siehe oben) oder umhüllen Sie from_resource() mit try/except ValueError. In beiden Fällen ist canonical_url als Klassenattribut genau für diesen Zweck verfügbar.
  • get_systolic() / get_diastolic() geben den flachen Slice-Wert zurück. Kein manuelles Traversieren von component[].code.coding[].code, um LOINC-Codes abzugleichen — das Profil weiß bereits, welcher Slice welcher ist, und liefert Ihnen die Quantity-Daten als einfaches Dict.

Das ist der vollständige Kreislauf: CSV → typisierte Profile → validiertes Bundle → typisiertes Rücklesen mit profilbewussten Gettern. Dieselben wenigen Zeilen würden BPs verarbeiten, die von einem FHIR-Server abgerufen, aus einer Datei geladen oder über eine Subscription empfangen wurden — das typisierte Profil ist die gemeinsame Form, unabhängig von der Quelle.

Schritt 6 — Das Bundle auf einem FHIR-Server ablegen

Die typisierte Pipeline ist nur die halbe Geschichte. Um die Transaktion tatsächlich zu committen — Patienten-IDs vergeben, urn:uuid-Referenzen umgeschrieben, Ressourcen gespeichert und durchsuchbar —, benötigen Sie einen FHIR-Server. Starten Sie Aidbox:

curl -JO https://aidbox.app/runme && docker compose up -d

Öffnen Sie http://localhost:8080 in Ihrem Browser, um eine kostenlose Entwicklerlizenz zu erhalten, und lesen Sie dann das Root-Client-Secret aus docker-compose.yaml in eine Umgebungsvariable — diese wird von den curl-Aufrufen und dem Python-Skript unten wiederverwendet:

export BOX_ROOT_CLIENT_SECRET=$(awk '/BOX_ROOT_CLIENT_SECRET:/{print $2}' docker-compose.yaml)

Prüfen Sie, ob der FHIR-Endpunkt verfügbar ist:

curl -u "root:$BOX_ROOT_CLIENT_SECRET" http://localhost:8080/fhir/metadata

Sie sollten ein JSON-CapabilityStatement erhalten.

Senden Sie die soeben erstellte bundle.json mit dem asynchronen fhirpy-Client:

import asyncio
import base64
import json
import os

from fhirpy import AsyncFHIRClient
from fhir_types.hl7_fhir_r4_core import Bundle

secret = os.environ["BOX_ROOT_CLIENT_SECRET"]  # exported above
auth = base64.b64encode(f"root:{secret}".encode()).decode()


async def main() -> None:
    client = AsyncFHIRClient("http://localhost:8080/fhir", authorization=f"Basic {auth}")
    bundle = json.load(open("bundle.json"))
    resp: Bundle = await client.execute("/", method="post", data=bundle)
    if resp.entry is None:
        return

    for entry in resp.entry:
        if entry.response is None:
            continue
        print(entry.response.status, entry.response.location)


asyncio.run(main())

Aidbox gibt ein transaction-response-Bundle zurück — einen Eintrag pro Eingabe, jeder mit 201 Created und einem location, der auf die gespeicherte Ressource verweist:

$ python post.py
201 Created Patient/<id>/_history/1
201 Created Observation/<id>/_history/1
...

Fragen Sie eine Observation zurück und betrachten Sie deren subject:

$ curl -u "root:$BOX_ROOT_CLIENT_SECRET" \
  "http://localhost:8080/fhir/Observation?code=http://loinc.org|85354-9" \
  | jq '.entry[].resource.subject.reference'
"Patient/01J..."
"Patient/01J..."

Kein urn:uuid — Aidbox hat die Platzhalter beim Commit atomisch umgeschrieben.

Die Pipeline typprüfen

Die generierten Modelle sind Pydantic v2, sodass der Konverter mit mypy typgeprüft werden kann — bereits in der generierten requirements.txt festgelegt. Eine Anforderung: Aktivieren Sie das Pydantic mypy-Plugin (es wird mit Pydantic mitgeliefert, keine zusätzliche Installation nötig), sonst kann mypy nicht erkennen, dass ein Field(None, ...)-Standard ein Feld optional macht, und überschwemmt Sie bei jedem konstruierten Modell mit falschen „missing argument"-Fehlern.

Legen Sie eine mypy.ini neben Ihren Code:

[mypy]
strict = True
plugins = pydantic.mypy

Führen Sie es dann aus:

$ mypy .
Success: no issues found in 35 source files

Sowohl Ihre Konverter-Module — load.py, avg.py, post.py — als auch das generierte fhir_types/-Paket werden ohne Beanstandungen geprüft. Die typisierten Factories, to_resource() und das generische Bundle[Patient | Observation] bestehen allesamt die Prüfung, sodass ein falscher Feldtyp oder ein fehlendes Pflichtargument erkannt wird, bevor Sie den Server jemals erreichen. Die generierte Profil-Schicht besteht die Typprüfung auch unter vollem --strict — der eigentliche Grund, warum mypy.ini aus nur diesen zwei Zeilen besteht: kein disable_error_code, kein strict_optional = False, nichts manuell innerhalb von fhir_types/ bearbeitet. Das Einzige, was Ihr eigener Code beiträgt, ist eine gewöhnliche None-Prüfung beim Lesen eines optionalen Felds — das Walrus-Operator-Muster in avg.py oben oder if entries is None: ... vor dem Indexieren einer Liste. Das ist normales strict-mode-Python, keine Eigenheit des Generators.

Wie es weitergeht

  • Mehr zur Profil-API. Weitere Factories, Getter und Slice-/Extension-Formen werden in den Codegen-Beispieltests demonstriert: test_profile_patient.py, test_profile_bp.py, test_profile_bodyweight.py und test_profile_typed_bundle.py.
  • Typisierte Bundles mit benannten Eintrags-Slices. Ein profiliertes Bundle generiert Setter/Getter pro Slice (set_patient_entry, get_organization_entry), wobei einfache und unbegrenzte Kardinalität (max: *) automatisch berücksichtigt wird — siehe den oben erwähnten typed-bundle-Test.
  • Die Ausgabe an Ihre Codebasis anpassen. fieldFormat (snake_case/camelCase), client ("fhirpy"/"none"), allowExtraFields und primitiveTypeExtension sind allesamt Optionen für .python({ ... }).
  • Profile aus mehreren Paketen kombinieren. APIBuilder.fromPackage() lässt sich verketten — US Core zusammen mit Ihrem eigenen IG oder einem regionalen Basispaket. localStructureDefinitions() lädt Profile direkt aus einem Ordner mit StructureDefinition-JSON-Dateien.

Fazit

Der Generator erzeugt sowohl die Basis-R4-Pydantic-Modelle als auch eine schlanke Profil-Klassen-Schicht darüber — keine Runtime-DSL, kein ORM, kein Framework. to_resource() liefert immer eine einfache Pydantic-Ressource, und model_dump(by_alias=True, exclude_none=True) liefert immer einfaches FHIR-JSON, das Sie an jeden Server senden können.

@atomic-ehr/codegen ist MIT-lizenziert; Issues und Pull Requests sind willkommen.

GitHub | NPM | US Core IG

Diesen Artikel teilen
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

Get the latest articles on FHIR, interoperability, and healthcare IT.