|
6 min de lecture
|

@atomic-ehr/codegen: Python FHIR Types

Résumer cet article avec :
ChatGPTPerplexityClaudeGrok

Le développement FHIR en Python signifie généralement se débattre avec des dictionnaires bruts, deviner les noms de champs à partir d'une spécification volumineuse et découvrir des fautes de frappe seulement à l'exécution. Nous avons conçu un générateur qui élimine tout cela.

@atomic-ehr/codegen produit des modèles Pydantic fortement typés à partir de n'importe quel paquet FHIR — avec validation, autocomplétion IDE et intégration optionnelle de fhirpy. Il fonctionne avec n'importe quel serveur FHIR, pas seulement Aidbox. Le projet est à code source ouvert (MIT) et nous accueillons les contributions avec plaisir.

Cet article met à jour notre article précédent sur le SDK Python. Depuis lors, le générateur a été réécrit de zéro dans une pile TypeScript unifiée sous le nom @atomic-ehr/codegen.

Ce que vous obtenez :

  • Modèles Pydantic v2 pour chaque ressource et type de données
  • Ensembles de valeurs — énumérations Literal et CodeableConcept[T] générique avec liaisons dans le type
  • Extensions de types primitifs — champs _birthDate, _family typés
  • Client asynchrone fhirpy — branchement direct sur AsyncFHIRClient
  • N'importe quel paquet FHIR — R4, US Core et profils personnalisés en une seule passe ; application de correctifs aux paquets à la volée
  • Type Schema — élagage d'arbre, promotion de modèles logiques, résolution de collisions

Générez vos types FHIR

Installez le générateur (bun add et pnpm add fonctionnent aussi) :

npm install @atomic-ehr/codegen

Créez un script generate.ts :

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();

Exécutez-le :

npx tsx generate.ts

Vous disposez maintenant d'un paquet Python prêt à l'emploi dans ./fhir_types/. L'élagage d'arbre n'inclut que les ressources que vous avez listées ainsi que leurs dépendances (DomainResource, Element, OperationOutcome, etc.) automatiquement.

Ce qui est généré

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.

Chaque ressource est un modèle Pydantic avec des types de champs appropriés, des alias et une validation :

# 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
    # ...

Consultez le fichier généré complet pour voir le modèle Patient dans son intégralité.

Créer et manipuler des ressources

Configurer l'environnement Python (Python 3.12+)
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r fhir_types/requirements.txt

Le générateur produit des types et une validation — aucun client HTTP n'est intégré. Pour la communication avec le serveur, utilisez fhirpy ou n'importe quelle bibliothèque HTTP. Ici, nous travaillons directement avec les modèles :

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'

Vérification de types et validation

Que se passe-t-il lorsque nous faisons des erreurs ?

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
)

Analyse statique avec mypy. Ajoutez le greffon Pydantic à mypy.ini :

[mypy]
python_version = 3.12
plugins = pydantic.mypy

[pydantic-mypy]
init_typed = True

Puis exécutez :

$ 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]

Validation à l'exécution — exécutez le script et Pydantic intercepte les deux erreurs à l'instanciation :

$ 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

Nul besoin d'exécuter un validateur séparément — les modèles appliquent les contraintes à la construction. Si vous devez accepter des champs supplémentaires, générez avec allowExtraFields: true.

Options de personnalisation

OptionTypeDéfautEffet
fieldFormat"snake_case" | "camelCase""camelCase"Nommage des champs : birth_date ou birthDate
allowExtraFieldsbooleanfalseAccepter les champs inconnus dans les données JSON en entrée
primitiveTypeExtensionbooleanfalseGénérer des champs complémentaires typés _birthDate, _family
clientfhirpy, nonefhirpyClasse de base compatible FhirpyBaseModel

Ces options se combinent avec les fonctionnalités au niveau de l'APIBuilder — élagage d'arbre pour n'inclure que les ressources dont vous avez besoin, chargement multi-paquets pour combiner le noyau R4 avec des IG et des profils personnalisés, et prétraitement des paquets pour corriger les paquets amont défectueux avant la génération.

Prochainement : prise en charge des profils pour Python

Notre générateur TypeScript prend déjà en charge les classes de profils FHIR — des classes générées qui peuplent automatiquement les valeurs fixes, fournissent des accesseurs typés pour les tranches et les extensions, et incluent une validation côté client. Nous travaillons à apporter les mêmes capacités à Python :

  • Accesseurs de tranches avec valeurs de discriminateur appliquées automatiquement
  • Accesseurs/modificateurs d'extensions
  • Validation retournant des erreurs et avertissements structurés

Si cela vous intéresse — vos commentaires sur les schémas de profils les plus importants pour vos flux de travail Python sont les bienvenus sur GitHub.

Au-delà de Python : ce que fait d'autre @atomic-ehr/codegen

Cet article s'est concentré sur le générateur Python, mais @atomic-ehr/codegen est une plateforme de génération de code multilangage. Voici ce qu'il offre également :

  • Quatre générateurs intégrés — Python, TypeScript, C# et un moteur de gabarits Mustache pour tout langage
  • Prétraitement des paquets — les paquets FHIR du monde réel sont souvent livrés avec des bogues : URL canoniques malformées, concepts CodeSystem manquants, ValueSets externes indisponibles. Le point d'ancrage preprocessPackage vous permet de les corriger avant la génération, sans avoir à créer une bifurcation du paquet ni à contourner les erreurs en aval
  • Promotion de modèles logiques — convertir les StructureDefinitions logiques en ressources de première classe pour la génération de code
  • Résolution des collisions de schémas — lorsque plusieurs paquets définissent des liaisons qui se chevauchent, vous pouvez spécifier quelle source l'emporte
  • Chargement multi-sources — combiner des paquets NPM, des archives TGZ locales et des fichiers JSON StructureDefinition isolés en une seule passe de génération

Exemples Python fonctionnels :

@atomic-ehr/codegen est à code source ouvert sous licence MIT. Que vous travailliez avec FHIR en Python pour créer des pipelines de données, des fonctionnalités d'apprentissage automatique ou des applications cliniques — essayez-le et dites-nous ce que vous en pensez. Les signalements de bogues, demandes de fonctionnalités et requêtes de fusion sont tous les bienvenus.

GitHub | NPM | Aleksandr Penskoi sur LinkedIn

Partager cet article
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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