---
{
  "title": "@atomic-ehr/codegen: US Core Profile-Klassen in Python",
  "description": "Generieren Sie typisierte US Core Profile-Klassen in Python aus dem FHIR IG mit @atomic-ehr/codegen — typisierte Factories, Extensions, Slices und profilbewusste Validierung.",
  "date": "2026-07-03",
  "author": "Mikhail Artemyev, Aleksandr Penskoi",
  "reading-time": "12 minutes",
  "tags": [
    "FHIR Tools",
    "FHIR Standard",
    "Code Generation",
    "Python",
    "Pydantic",
    "Aidbox"
  ]
}
---
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`](https://github.com/atomic-ehr/codegen) lässt diesen Boilerplate verschwinden. Verweisen Sie das Tool auf den [US Core IG](https://www.hl7.org/fhir/us/core/) 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](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) und [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html).

## Was Sie erstellen werden

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

1. Profil-Klassen für [US Core Patient](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-patient.html) und [US Core Blood Pressure](https://www.hl7.org/fhir/us/core/StructureDefinition-us-core-blood-pressure.html) 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](https://pypi.org/project/fhirpy/) 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](https://peps.python.org/pep-0604/) `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:

```bash
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`:

```typescript
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 `id`s 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:

```bash
$ 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)
```

<details><summary>Python-Virtualumgebung einrichten</summary>

```bash
python3.14 -m venv venv
source venv/bin/activate
```

</details>

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

```bash
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`](https://github.com/Aidbox/examples/tree/main/developer-experience/atomic-ehr-codegen-python-us-core-profiles) — `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](https://github.com/atomic-ehr/codegen/tree/main/examples/python-r4-us-core). 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](https://www.hl7.org/fhir/us/core/ValueSet-omb-race-category.html), die US Core erwartet:

```csv
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:

```python
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:

```python
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.)

```python
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")
```

```bash
$ 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:

```python
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)})")
```

```bash
$ 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](https://www.health-samurai.io/aidbox):

```bash
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:

```bash
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:

```bash
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:

```python
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:

```bash
$ 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`:

```bash
$ 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](https://mypy-lang.org/) 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:

```ini
[mypy]
strict = True
plugins = pydantic.mypy
```

Führen Sie es dann aus:

```bash
$ 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`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_patient.py), [`test_profile_bp.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_bp.py), [`test_profile_bodyweight.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/test_profile_bodyweight.py) und [`test_profile_typed_bundle.py`](https://github.com/atomic-ehr/codegen/blob/main/examples/python-r4-us-core/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](https://github.com/atomic-ehr/codegen) | [NPM](https://www.npmjs.com/package/@atomic-ehr/codegen) | [US Core IG](https://www.hl7.org/fhir/us/core/)