|
6 min de lectura
|

@atomic-ehr/codegen: Python FHIR Types

Resumir este artículo con:
ChatGPTPerplexityClaudeGrok

El desarrollo FHIR en Python suele implicar lidiar con diccionarios sin tipo, adivinar nombres de campo en una especificación enorme y descubrir errores tipográficos solo en tiempo de ejecución. Hemos creado un generador que elimina todo eso.

@atomic-ehr/codegen produce modelos Pydantic fuertemente tipados a partir de cualquier paquete FHIR — con validación, autocompletado en el IDE e integración opcional con fhirpy. Funciona con cualquier servidor FHIR, no solo con Aidbox. El proyecto es de código abierto (MIT) y agradecemos las contribuciones.

Esta entrada actualiza nuestro artículo anterior sobre el SDK de Python. Desde entonces, el generador ha sido reescrito desde cero en un único stack TypeScript como @atomic-ehr/codegen.

Lo que obtendrá:

  • Modelos Pydantic v2 para cada recurso y tipo de dato
  • Value sets — enumeraciones Literal y CodeableConcept[T] genérico con bindings en el tipo
  • Extensiones de tipos primitivos — campos _birthDate, _family tipados
  • Cliente asíncrono fhirpy — conéctese directamente a AsyncFHIRClient
  • Cualquier paquete FHIR — R4, US Core y personalizados en un solo paso; parchee paquetes al vuelo
  • Type Schema — tree shaking, promoción de modelos lógicos y resolución de colisiones

Genere Sus Tipos FHIR

Instale el generador (bun add y pnpm add también funcionan):

npm install @atomic-ehr/codegen

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

Ejecútelo:

npx tsx generate.ts

Ahora tendrá un paquete Python listo para usar en ./fhir_types/. El tree shaking incluye únicamente los recursos que ha listado, junto con sus dependencias (DomainResource, Element, OperationOutcome, etc.) de forma automática.

Qué Se Genera

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.

Cada recurso es un modelo Pydantic con tipos de campo correctos, alias y validación:

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

Consulte el archivo generado completo para ver el modelo Patient completo.

Crear y Trabajar con Recursos

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

El generador produce tipos y validación — no incluye ningún cliente HTTP. Para la comunicación con el servidor, use fhirpy o cualquier biblioteca HTTP. Aquí trabajamos directamente con los modelos:

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'

Comprobación de Tipos y Validación

¿Qué ocurre cuando cometemos errores?

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
)

Análisis estático con mypy. Añada el plugin de Pydantic a mypy.ini:

[mypy]
python_version = 3.12
plugins = pydantic.mypy

[pydantic-mypy]
init_typed = True

A continuación, ejecútelo:

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

Validación en tiempo de ejecución — ejecute el script y Pydantic detecta ambos errores en la instanciación:

$ 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

No es necesario ejecutar un validador por separado — los modelos aplican las restricciones en la construcción. Si necesita aceptar campos adicionales, genere con allowExtraFields: true.

Opciones de Personalización

OpciónTipoValor por defectoEfecto
fieldFormat"snake_case" | "camelCase""camelCase"Nomenclatura de campos: birth_date frente a birthDate
allowExtraFieldsbooleanfalseAcepta campos desconocidos en la entrada JSON
primitiveTypeExtensionbooleanfalseGenera campos complementarios tipados _birthDate, _family
clientfhirpy, nonefhirpyClase base compatible con FhirpyBaseModel

Estas opciones se combinan con las funcionalidades a nivel de APIBuilder — tree shaking para incluir solo los recursos que necesita, carga de múltiples paquetes para mezclar el núcleo R4 con IGs y perfiles personalizados, y preprocesamiento de paquetes para parchear paquetes upstream defectuosos antes de la generación.

Próximamente: Soporte de Perfiles para Python

Nuestro generador TypeScript ya admite clases de perfil FHIR — clases generadas que rellenan automáticamente valores fijos, proporcionan accesores tipados para slices y extensiones, e incluyen validación del lado del cliente. Estamos trabajando en llevar las mismas capacidades a Python:

  • Accesores de slice con valores discriminadores aplicados automáticamente
  • Getters/setters de extensiones
  • Validación que devuelve errores y advertencias estructurados

Si le interesa esto, agradecemos sus comentarios sobre qué patrones de perfil son más relevantes para sus flujos de trabajo en Python en GitHub.

Más Allá de Python: Qué Más Hace @atomic-ehr/codegen

Esta entrada se ha centrado en el generador de Python, pero @atomic-ehr/codegen es una plataforma de generación de código multilenguaje. Esto es lo que más ofrece:

  • Cuatro generadores integrados — Python, TypeScript, C# y un motor de plantillas Mustache para cualquier lenguaje
  • Preprocesamiento de paquetes — los paquetes FHIR del mundo real suelen incluir errores: URLs canónicas malformadas, conceptos de CodeSystem faltantes, ValueSets externos no disponibles. El hook preprocessPackage permite parchearlos antes de la generación, de modo que no tenga que bifurcar el paquete ni solucionar errores más adelante
  • Promoción de modelos lógicos — convierte StructureDefinitions lógicas en recursos de primera clase para la generación de código
  • Resolución de colisiones de esquema — cuando varios paquetes definen bindings superpuestos, puede especificar qué fuente tiene prioridad
  • Carga de múltiples fuentes — combine paquetes NPM, archivos TGZ locales y archivos JSON de StructureDefinition sueltos en un único paso de generación

Ejemplos de Python en funcionamiento:

@atomic-ehr/codegen es de código abierto bajo la licencia MIT. Si trabaja con FHIR en Python — ya sea desarrollando pipelines de datos, funcionalidades de ML o aplicaciones clínicas — pruébelo y cuéntenos qué le parece. Las incidencias, las solicitudes de funcionalidades y los PRs son bienvenidos.

GitHub | NPM | Aleksandr Penskoi en LinkedIn

Compartir este artículo
Comments
Comments
Sign in
Loading comments...
Subscribe to our blog

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