|
6 Min. Lesezeit
|

@atomic-ehr/codegen: Python FHIR Types

Diesen Artikel zusammenfassen mit:
ChatGPTPerplexityClaudeGrok

FHIR-Entwicklung in Python bedeutet in der Regel, mit rohen Dicts zu kämpfen, Feldnamen aus einer umfangreichen Spezifikation zu erraten und Tippfehler erst zur Laufzeit zu entdecken. Wir haben einen Generator entwickelt, der all das überflüssig macht.

@atomic-ehr/codegen erzeugt stark typisierte Pydantic-Modelle aus beliebigen FHIR-Paketen – mit Validierung, IDE-Autovervollständigung und optionaler fhirpy-Integration. Es funktioniert mit jedem FHIR-Server, nicht nur mit Aidbox. Das Projekt ist Open Source (MIT), und wir begrüßen Beiträge.

Dieser Beitrag aktualisiert unseren früheren Python-SDK-Artikel. Seitdem wurde der Generator von Grund auf in einem einheitlichen TypeScript-Stack als @atomic-ehr/codegen neu geschrieben.

Was Sie erhalten:

  • Pydantic v2-Modelle für jede Ressource und jeden Datentyp
  • Value Sets – Literal-Enums und generisches CodeableConcept[T] mit Bindings im Typ
  • Primitive Erweiterungen – typisierte _birthDate-, _family-Felder
  • fhirpy Async-Client – direkte Einbindung in AsyncFHIRClient
  • Beliebige FHIR-Pakete – R4, US Core und benutzerdefinierte Pakete in einem Durchlauf; Pakete können on the fly gepatcht werden
  • Type Schema – Tree Shaking, Promotion logischer Modelle, Kollisionsauflösung

Generieren Sie Ihre FHIR-Typen

Installieren Sie den Generator (bun add und pnpm add funktionieren ebenfalls):

npm install @atomic-ehr/codegen

Erstellen Sie ein generate.ts-Skript:

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

const main = async () => {
  const builder = new APIBuilder()
    .fromPackage("hl7.fhir.r4.core", "4.0.1")
    .python({
      fieldFormat: "snake_case",
      allowExtraFields: false,
      primitiveTypeExtension: true,
    })
    .typeSchema({
      treeShake: {
        "hl7.fhir.r4.core": {
          "http://hl7.org/fhir/StructureDefinition/Patient": {},
          "http://hl7.org/fhir/StructureDefinition/Observation": {},
          "http://hl7.org/fhir/StructureDefinition/Bundle": {},
        },
      },
    })
    .outputTo("./fhir_types")
    .cleanOutput(true);

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

main();

Führen Sie es aus:

npx tsx generate.ts

Sie haben nun ein gebrauchsfertiges Python-Paket in ./fhir_types/. Tree Shaking zieht automatisch nur die aufgelisteten Ressourcen sowie deren Abhängigkeiten ein (DomainResource, Element, OperationOutcome usw.).

Was generiert wird

fhir_types/
├── __init__.py                  # Exports + model_rebuild() for forward refs
├── requirements.txt             # pydantic, requests, fhirpy, mypy, pytest
├── README.md
└── hl7_fhir_r4_core/
    ├── __init__.py
    ├── base.py                  # Complex types: CodeableConcept, HumanName, ...
    ├── patient.py               # Patient + nested types (PatientContact, ...)
    ├── observation.py
    ├── operation_outcome.py
    ├── bundle.py
    ├── domain_resource.py
    ├── resource.py
    └── resource_families.py     # Polymorphic validators for Bundle.entry etc.

Jede Ressource ist ein Pydantic-Modell mit korrekten Feldtypen, Aliases und Validierung:

# Simplified -- other fields also carry serialization_alias, omitted for brevity
class Patient(DomainResource):
    model_config = ConfigDict(
        validate_by_name=True,
        serialize_by_alias=True,
        extra="forbid"
    )
    resource_type: Literal['Patient'] = Field(
        default='Patient', alias='resourceType',
        serialization_alias='resourceType', frozen=True
    )
    birth_date: str | None = Field(None, alias="birthDate")
    birth_date_extension: Element | None = Field(None, alias="_birthDate")
    gender: Literal["male", "female", "other", "unknown"] | None = Field(None)
    marital_status: CodeableConcept[
        Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str
    ] | None = Field(None, alias="maritalStatus")
    name: PyList[HumanName] | None = Field(None)  # PyList = typing.List
    # ...

Die vollständig generierte Datei enthält das vollständige Patient-Modell.

Ressourcen erstellen und damit arbeiten

Python-Umgebung einrichten (Python 3.12+)
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r fhir_types/requirements.txt

Der Generator erzeugt Typen und Validierung – kein HTTP-Client ist enthalten. Für die Serverkommunikation verwenden Sie fhirpy oder eine beliebige HTTP-Bibliothek. Hier arbeiten wir direkt mit den Modellen:

from fhir_types.hl7_fhir_r4_core import Element, Extension, HumanName, Patient

patient = Patient(
    name=[HumanName(given=["John"], family="Doe")],
    gender="male",
    birth_date="1980-01-01",
    birth_date_extension=Element(
        extension=[
            Extension(
                url="http://hl7.org/fhir/StructureDefinition/patient-birthTime",
                value_date_time="1980-01-01T08:30:00-05:00",
            ),
        ],
    ),
)

# Serialize to FHIR JSON (camelCase keys, nulls excluded)
print(patient.to_json(indent=2))
{
  "resourceType": "Patient",
  "birthDate": "1980-01-01",
  "_birthDate": {
    "extension": [{
      "url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
      "valueDateTime": "1980-01-01T08:30:00-05:00"
    }]
  },
  "gender": "male",
  "name": [{ "family": "Doe", "given": ["John"] }]
}
# Round-trip: JSON -> Patient
restored = Patient.from_json(patient.to_json())
assert restored == patient

# Typed field access
patient.gender                                              # 'male'
assert patient.name is not None
patient.name[0].family                                      # 'Doe'
patient.birth_date                                          # '1980-01-01'
assert patient.birth_date_extension is not None
assert patient.birth_date_extension.extension is not None
patient.birth_date_extension.extension[0].value_date_time   # '1980-01-01T08:30:00-05:00'

Typprüfung und Validierung

Was passiert, wenn wir Fehler machen?

from fhir_types.hl7_fhir_r4_core import HumanName, Patient

Patient(
    name=[HumanName(family="Doe")],
    gender="FOO",            # wrong value
    some_data="1990-01-01",  # wrong field
)

Statische Analyse mit mypy. Fügen Sie das Pydantic-Plugin zu mypy.ini hinzu:

[mypy]
python_version = 3.12
plugins = pydantic.mypy

[pydantic-mypy]
init_typed = True

Dann ausführen:

$ mypy . --strict
main.py:10: error: Unexpected keyword argument "some_data" for "Patient"  [call-arg]
main.py:12: error: Argument "gender" to "Patient" has incompatible type "Literal['FOO']"; expected "Literal['male', 'female', 'other', 'unknown'] | None"  [arg-type]

Laufzeitvalidierung – führen Sie das Skript aus, und Pydantic erkennt beide Fehler bei der Instanziierung:

$ python main.py
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Patient
gender
  Input should be 'male', 'female', 'other' or 'unknown'
some_data
  Extra inputs are not permitted

Es ist nicht notwendig, einen Validator separat auszuführen – Modelle erzwingen Constraints bei der Konstruktion. Wenn Sie zusätzliche Felder akzeptieren möchten, generieren Sie mit allowExtraFields: true.

Anpassungsoptionen

OptionTypStandardAuswirkung
fieldFormat"snake_case" | "camelCase""camelCase"Feldbenennung: birth_date vs. birthDate
allowExtraFieldsbooleanfalseUnbekannte Felder in der JSON-Eingabe akzeptieren
primitiveTypeExtensionbooleanfalseTypisierte _birthDate-, _family-Begleitfelder generieren
clientfhirpy, nonefhirpyFhirpyBaseModel-kompatible Basisklasse

Diese Optionen lassen sich mit Funktionen auf APIBuilder-Ebene kombinieren – Tree Shaking, um nur die benötigten Ressourcen einzuschließen, Multi-Paket-Laden, um R4 Core mit IGs und benutzerdefinierten Profilen zu mischen, sowie Paket-Vorverarbeitung, um fehlerhafte Upstream-Pakete vor der Generierung zu patchen.

Als Nächstes: Profilunterstützung für Python

Unser TypeScript-Generator unterstützt bereits FHIR-Profilklassen – generierte Klassen, die feste Werte automatisch befüllen, typisierte Accessoren für Slices und Erweiterungen bereitstellen und clientseitige Validierung beinhalten. Wir arbeiten daran, dieselben Funktionen für Python verfügbar zu machen:

  • Slice-Accessoren mit automatisch angewendeten Diskriminatorwerten
  • Getter/Setter für Erweiterungen
  • Validierung mit strukturierten Fehlern und Warnungen

Wenn Sie daran interessiert sind – Feedback dazu, welche Profilmuster für Ihre Python-Workflows am wichtigsten sind, ist auf GitHub sehr willkommen.

Über Python hinaus: Was @atomic-ehr/codegen sonst noch bietet

Dieser Beitrag konzentrierte sich auf den Python-Generator, aber @atomic-ehr/codegen ist eine mehrsprachige Code-Generierungsplattform. Hier ein Überblick über weitere Funktionen:

  • Vier integrierte Generatoren – Python, TypeScript, C# und eine Mustache-Template-Engine für beliebige Sprachen
  • Paket-Vorverarbeitung – reale FHIR-Pakete werden häufig mit Fehlern ausgeliefert: fehlerhafte kanonische URLs, fehlende CodeSystem-Konzepte, nicht verfügbare externe ValueSets. Der preprocessPackage-Hook ermöglicht es, diese vor der Generierung zu patchen, sodass Sie das Paket weder forken noch Fehler nachgelagert umgehen müssen
  • Promotion logischer Modelle – logische StructureDefinitions in erstklassige Ressourcen für die Code-Generierung umwandeln
  • Schema-Kollisionsauflösung – wenn mehrere Pakete überlappende Bindings definieren, können Sie festlegen, welche Quelle Vorrang hat
  • Multi-Source-Laden – NPM-Pakete, lokale TGZ-Archive und einzelne StructureDefinition-JSON-Dateien in einem einzigen Generierungsdurchlauf kombinieren

Funktionierende Python-Beispiele:

@atomic-ehr/codegen ist Open Source unter der MIT-Lizenz. Wenn Sie mit FHIR in Python arbeiten – ob Sie Datenpipelines, ML-Features oder klinische Anwendungen entwickeln – probieren Sie es aus und teilen Sie uns Ihre Meinung mit. Issues, Feature-Requests und Pull Requests sind herzlich willkommen.

GitHub | NPM | Aleksandr Penskoi auf LinkedIn

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

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