From 186992fe243f54d11d884ae51cdce79d22f58e50 Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Wed, 4 Mar 2026 18:04:55 +0000 Subject: [PATCH 1/3] py: binding support --- .../fhir_types/hl7_fhir_r4_core/base.py | 21 +- .../hl7_fhir_r4_core/observation.py | 12 +- .../fhir_types/hl7_fhir_r4_core/patient.py | 4 +- src/api/writer-generator/python.ts | 68 +++++- .../__snapshots__/python.test.ts.snap | 4 +- .../__snapshots__/typescript.test.ts.snap | 193 ------------------ .../__snapshots__/local-package.test.ts.snap | 4 +- test/api/write-generator/python.test.ts | 22 ++ 8 files changed, 104 insertions(+), 224 deletions(-) diff --git a/examples/python/fhir_types/hl7_fhir_r4_core/base.py b/examples/python/fhir_types/hl7_fhir_r4_core/base.py index 03f974466..a68278f58 100644 --- a/examples/python/fhir_types/hl7_fhir_r4_core/base.py +++ b/examples/python/fhir_types/hl7_fhir_r4_core/base.py @@ -4,7 +4,10 @@ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, PositiveInt -from typing import List as PyList, Literal +from typing import Generic, List as PyList, Literal +from typing_extensions import TypeVar + +T = TypeVar('T', bound=str, default=str) class Element(BaseModel): @@ -65,15 +68,15 @@ class BackboneElement(Element): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") -class CodeableConcept(Element): +class CodeableConcept(Element, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - coding: PyList[Coding] | None = Field(None, alias="coding", serialization_alias="coding") + coding: PyList[Coding[T]] | None = Field(None, alias="coding", serialization_alias="coding") text: str | None = Field(None, alias="text", serialization_alias="text") -class Coding(Element): +class Coding(Element, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - code: str | None = Field(None, alias="code", serialization_alias="code") + code: T | None = Field(None, alias="code", serialization_alias="code") display: str | None = Field(None, alias="display", serialization_alias="display") system: str | None = Field(None, alias="system", serialization_alias="system") user_selected: bool | None = Field(None, alias="userSelected", serialization_alias="userSelected") @@ -259,7 +262,7 @@ class Identifier(Element): assigner: Reference | None = Field(None, alias="assigner", serialization_alias="assigner") period: Period | None = Field(None, alias="period", serialization_alias="period") system: str | None = Field(None, alias="system", serialization_alias="system") - type: CodeableConcept | None = Field(None, alias="type", serialization_alias="type") + type: CodeableConcept[Literal["DL", "PPN", "BRN", "MR", "MCN", "EN", "TAX", "NIIP", "PRN", "MD", "DR", "ACSN", "UDI", "SNO", "SB", "PLAC", "FILL", "JHN"] | str] | None = Field(None, alias="type", serialization_alias="type") use: Literal["usual", "official", "temp", "secondary", "old"] | None = Field(None, alias="use", serialization_alias="use") value: str | None = Field(None, alias="value", serialization_alias="value") @@ -351,7 +354,7 @@ class Signature(Element): on_behalf_of: Reference | None = Field(None, alias="onBehalfOf", serialization_alias="onBehalfOf") sig_format: str | None = Field(None, alias="sigFormat", serialization_alias="sigFormat") target_format: str | None = Field(None, alias="targetFormat", serialization_alias="targetFormat") - type: PyList[Coding] = Field(alias="type", serialization_alias="type") + type: PyList[Coding[Literal["1.2.840.10065.1.12.1.1", "1.2.840.10065.1.12.1.2", "1.2.840.10065.1.12.1.3", "1.2.840.10065.1.12.1.4", "1.2.840.10065.1.12.1.5", "1.2.840.10065.1.12.1.6", "1.2.840.10065.1.12.1.7", "1.2.840.10065.1.12.1.8", "1.2.840.10065.1.12.1.9", "1.2.840.10065.1.12.1.10", "1.2.840.10065.1.12.1.11", "1.2.840.10065.1.12.1.12", "1.2.840.10065.1.12.1.13", "1.2.840.10065.1.12.1.14", "1.2.840.10065.1.12.1.15", "1.2.840.10065.1.12.1.16", "1.2.840.10065.1.12.1.17", "1.2.840.10065.1.12.1.18"] | str]] = Field(alias="type", serialization_alias="type") when: str = Field(alias="when", serialization_alias="when") who: Reference = Field(alias="who", serialization_alias="who") @@ -379,7 +382,7 @@ class TimingRepeat(Element): class Timing(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - code: CodeableConcept | None = Field(None, alias="code", serialization_alias="code") + code: CodeableConcept[Literal["BID", "TID", "QID", "AM", "PM", "QD", "QOD", "Q1H", "Q2H", "Q3H", "Q4H", "Q6H", "Q8H", "BED", "WK", "MO"] | str] | None = Field(None, alias="code", serialization_alias="code") event: PyList[str] | None = Field(None, alias="event", serialization_alias="event") repeat: TimingRepeat | None = Field(None, alias="repeat", serialization_alias="repeat") @@ -398,7 +401,7 @@ class TriggerDefinition(Element): class UsageContext(Element): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - code: Coding = Field(alias="code", serialization_alias="code") + code: Coding[Literal["gender", "age", "focus", "user", "workflow", "task", "venue", "species", "program"] | str] = Field(alias="code", serialization_alias="code") value_codeable_concept: CodeableConcept | None = Field(None, alias="valueCodeableConcept", serialization_alias="valueCodeableConcept") value_quantity: Quantity | None = Field(None, alias="valueQuantity", serialization_alias="valueQuantity") value_range: Range | None = Field(None, alias="valueRange", serialization_alias="valueRange") diff --git a/examples/python/fhir_types/hl7_fhir_r4_core/observation.py b/examples/python/fhir_types/hl7_fhir_r4_core/observation.py index 6f629cb61..3ee3a4d4e 100644 --- a/examples/python/fhir_types/hl7_fhir_r4_core/observation.py +++ b/examples/python/fhir_types/hl7_fhir_r4_core/observation.py @@ -17,8 +17,8 @@ class ObservationComponent(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") code: CodeableConcept = Field(alias="code", serialization_alias="code") - data_absent_reason: CodeableConcept | None = Field(None, alias="dataAbsentReason", serialization_alias="dataAbsentReason") - interpretation: PyList[CodeableConcept] | None = Field(None, alias="interpretation", serialization_alias="interpretation") + data_absent_reason: CodeableConcept[Literal["unknown", "asked-unknown", "temp-unknown", "not-asked", "asked-declined", "masked", "not-applicable", "unsupported", "as-text", "error", "not-a-number", "negative-infinity", "positive-infinity", "not-performed", "not-permitted"] | str] | None = Field(None, alias="dataAbsentReason", serialization_alias="dataAbsentReason") + interpretation: PyList[CodeableConcept[Literal["_GeneticObservationInterpretation", "CAR", "Carrier", "_ObservationInterpretationChange", "B", "D", "U", "W", "_ObservationInterpretationExceptions", "<", ">", "AC", "IE", "QCF", "TOX", "_ObservationInterpretationNormality", "A", "AA", "HH", "LL", "H", "H>", "HU", "L", "L<", "LU", "N", "_ObservationInterpretationSusceptibility", "I", "MS", "NCL", "NS", "R", "SYN-R", "S", "SDD", "SYN-S", "VS", "EX", "HX", "LX", "HM", "ObservationInterpretationDetection", "IND", "E", "NEG", "ND", "POS", "DET", "ObservationInterpretationExpectation", "EXP", "UNE", "OBX", "ReactivityObservationInterpretation", "NR", "RR", "WR"] | str]] | None = Field(None, alias="interpretation", serialization_alias="interpretation") reference_range: PyList[ObservationReferenceRange] | None = Field(None, alias="referenceRange", serialization_alias="referenceRange") value_boolean: bool | None = Field(None, alias="valueBoolean", serialization_alias="valueBoolean") value_codeable_concept: CodeableConcept | None = Field(None, alias="valueCodeableConcept", serialization_alias="valueCodeableConcept") @@ -39,7 +39,7 @@ class ObservationReferenceRange(BackboneElement): high: Quantity | None = Field(None, alias="high", serialization_alias="high") low: Quantity | None = Field(None, alias="low", serialization_alias="low") text: str | None = Field(None, alias="text", serialization_alias="text") - type: CodeableConcept | None = Field(None, alias="type", serialization_alias="type") + type: CodeableConcept[Literal["type", "normal", "recommended", "treatment", "therapeutic", "pre", "post", "endocrine", "pre-puberty", "follicular", "midcycle", "luteal", "postmenopausal"] | str] | None = Field(None, alias="type", serialization_alias="type") class Observation(DomainResource): @@ -53,10 +53,10 @@ class Observation(DomainResource): ) based_on: PyList[Reference] | None = Field(None, alias="basedOn", serialization_alias="basedOn") body_site: CodeableConcept | None = Field(None, alias="bodySite", serialization_alias="bodySite") - category: PyList[CodeableConcept] | None = Field(None, alias="category", serialization_alias="category") + category: PyList[CodeableConcept[Literal["social-history", "vital-signs", "imaging", "laboratory", "procedure", "survey", "exam", "therapy", "activity"] | str]] | None = Field(None, alias="category", serialization_alias="category") code: CodeableConcept = Field(alias="code", serialization_alias="code") component: PyList[ObservationComponent] | None = Field(None, alias="component", serialization_alias="component") - data_absent_reason: CodeableConcept | None = Field(None, alias="dataAbsentReason", serialization_alias="dataAbsentReason") + data_absent_reason: CodeableConcept[Literal["unknown", "asked-unknown", "temp-unknown", "not-asked", "asked-declined", "masked", "not-applicable", "unsupported", "as-text", "error", "not-a-number", "negative-infinity", "positive-infinity", "not-performed", "not-permitted"] | str] | None = Field(None, alias="dataAbsentReason", serialization_alias="dataAbsentReason") derived_from: PyList[Reference] | None = Field(None, alias="derivedFrom", serialization_alias="derivedFrom") device: Reference | None = Field(None, alias="device", serialization_alias="device") effective_date_time: str | None = Field(None, alias="effectiveDateTime", serialization_alias="effectiveDateTime") @@ -67,7 +67,7 @@ class Observation(DomainResource): focus: PyList[Reference] | None = Field(None, alias="focus", serialization_alias="focus") has_member: PyList[Reference] | None = Field(None, alias="hasMember", serialization_alias="hasMember") identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") - interpretation: PyList[CodeableConcept] | None = Field(None, alias="interpretation", serialization_alias="interpretation") + interpretation: PyList[CodeableConcept[Literal["_GeneticObservationInterpretation", "CAR", "Carrier", "_ObservationInterpretationChange", "B", "D", "U", "W", "_ObservationInterpretationExceptions", "<", ">", "AC", "IE", "QCF", "TOX", "_ObservationInterpretationNormality", "A", "AA", "HH", "LL", "H", "H>", "HU", "L", "L<", "LU", "N", "_ObservationInterpretationSusceptibility", "I", "MS", "NCL", "NS", "R", "SYN-R", "S", "SDD", "SYN-S", "VS", "EX", "HX", "LX", "HM", "ObservationInterpretationDetection", "IND", "E", "NEG", "ND", "POS", "DET", "ObservationInterpretationExpectation", "EXP", "UNE", "OBX", "ReactivityObservationInterpretation", "NR", "RR", "WR"] | str]] | None = Field(None, alias="interpretation", serialization_alias="interpretation") issued: str | None = Field(None, alias="issued", serialization_alias="issued") method: CodeableConcept | None = Field(None, alias="method", serialization_alias="method") note: PyList[Annotation] | None = Field(None, alias="note", serialization_alias="note") diff --git a/examples/python/fhir_types/hl7_fhir_r4_core/patient.py b/examples/python/fhir_types/hl7_fhir_r4_core/patient.py index 9cf3ec849..86758601b 100644 --- a/examples/python/fhir_types/hl7_fhir_r4_core/patient.py +++ b/examples/python/fhir_types/hl7_fhir_r4_core/patient.py @@ -15,7 +15,7 @@ class PatientCommunication(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - language: CodeableConcept = Field(alias="language", serialization_alias="language") + language: CodeableConcept[Literal["ar", "bn", "cs", "da", "de", "de-AT", "de-CH", "de-DE", "el", "en", "en-AU", "en-CA", "en-GB", "en-IN", "en-NZ", "en-SG", "en-US", "es", "es-AR", "es-ES", "es-UY", "fi", "fr", "fr-BE", "fr-CH", "fr-FR", "fy", "fy-NL", "hi", "hr", "it", "it-CH", "it-IT", "ja", "ko", "nl", "nl-BE", "nl-NL", "no", "no-NO", "pa", "pl", "pt", "pt-BR", "ru", "ru-RU", "sr", "sr-RS", "sv", "sv-SE", "te", "zh", "zh-CN", "zh-HK", "zh-SG", "zh-TW"] | str] = Field(alias="language", serialization_alias="language") preferred: bool | None = Field(None, alias="preferred", serialization_alias="preferred") class PatientContact(BackboneElement): @@ -55,7 +55,7 @@ class Patient(DomainResource): identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") managing_organization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") - marital_status: CodeableConcept | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") + marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multiple_birth_boolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multiple_birth_integer: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger") name: PyList[HumanName] | None = Field(None, alias="name", serialization_alias="name") diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index adacf9c40..d73b3389c 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url"; import { camelCase, pascalCase, snakeCase, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils"; import { Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts"; import { groupByPackages, sortAsDeclarationSequence, type TypeSchemaIndex } from "@root/typeschema/utils"; -import type { Field, Identifier, RegularTypeSchema } from "@typeschema/types.ts"; +import type { EnumDefinition, Field, Identifier, RegularTypeSchema } from "@typeschema/types.ts"; const PRIMITIVE_TYPE_MAP: Record = { boolean: "bool", @@ -80,6 +80,18 @@ const PYTHON_KEYWORDS = new Set([ const MAX_IMPORT_LINE_LENGTH = 100; +const GENERIC_COMPLEX_TYPES = new Set(["Coding", "CodeableConcept"]); + +const GENERIC_FIELD_REWRITES: Record> = { + Coding: { code: "T" }, + CodeableConcept: { coding: "Coding[T]" }, +}; + +const pyEnumType = (enumDef: EnumDefinition): string => { + const values = enumDef.values.map((e) => `"${e}"`).join(", "); + return enumDef.isOpen ? `Literal[${values}] | str` : `Literal[${values}]`; +}; + export interface PythonGeneratorOptions extends WriterOptions { allowExtraFields?: boolean; rootPackageName: string; /// e.g. .hl7_fhir_r4_core.Patient. @@ -147,6 +159,7 @@ export class Python extends Writer { private tsIndex: TypeSchemaIndex | undefined; private readonly forFhirpyClient: boolean; private readonly fieldFormat: StringFormatKey; + private currentSchemaName: string | undefined; constructor(options: PythonGeneratorOptions) { super({ ...options, resolveAssets: options.resolveAssets ?? resolvePyAssets }); @@ -247,9 +260,14 @@ export class Python extends Writer { } private generateBasePy(packageComplexTypes: RegularTypeSchema[]): void { + const hasGenericTypes = packageComplexTypes.some((s) => GENERIC_COMPLEX_TYPES.has(s.identifier.name)); this.cat("base.py", () => { this.generateDisclaimer(); - this.generateDefaultImports(); + this.generateDefaultImports(hasGenericTypes); + if (hasGenericTypes) { + this.line(); + this.line("T = TypeVar('T', bound=str, default=str)"); + } this.line(); this.generateComplexTypes(packageComplexTypes); this.line(); @@ -402,14 +420,20 @@ export class Python extends Writer { } private getSuperClasses(schema: RegularTypeSchema): string[] { - return [...(schema.base ? [schema.base.name] : []), ...this.injectSuperClasses(schema.identifier.url)]; + const bases = [...(schema.base ? [schema.base.name] : []), ...this.injectSuperClasses(schema.identifier.url)]; + if (GENERIC_COMPLEX_TYPES.has(schema.identifier.name)) { + bases.push("Generic[T]"); + } + return bases; } private generateClassBody(schema: RegularTypeSchema): void { + this.currentSchemaName = schema.identifier.name; this.generateModelConfig(); if (!schema.fields) { this.line("pass"); + this.currentSchemaName = undefined; return; } @@ -422,6 +446,7 @@ export class Python extends Writer { if (schema.identifier.kind === "resource") { this.generateResourceMethods(schema); } + this.currentSchemaName = undefined; } private generateModelConfig(): void { @@ -464,7 +489,7 @@ export class Python extends Writer { private buildFieldInfo(fieldName: string, field: Field): FieldInfo { const pyFieldName = fixReservedWords(this.nameFormatFunction(fieldName)); - const fieldType = this.determineFieldType(field); + const fieldType = this.determineFieldType(field, fieldName); const defaultValue = this.getFieldDefaultValue(field, fieldName); return { @@ -474,12 +499,28 @@ export class Python extends Writer { }; } - private determineFieldType(field: Field): string { + private determineFieldType(field: Field, fieldName?: string): string { let fieldType = field ? this.getBaseFieldType(field) : ""; - if ("enum" in field && field.enum && !field.enum.isOpen) { - const s: string = field.enum.values.map((e: string) => `"${e}"`).join(", "); - fieldType = `Literal[${s}]`; + // Check for generic type field rewrites (e.g., Coding.code → T, CodeableConcept.coding → Coding[T]) + if (this.currentSchemaName && fieldName) { + const rewrite = GENERIC_FIELD_REWRITES[this.currentSchemaName]?.[fieldName]; + if (rewrite) { + fieldType = rewrite; + if (field.array) fieldType = `PyList[${fieldType}]`; + if (!field.required) fieldType = `${fieldType} | None`; + return fieldType; + } + } + + if ("enum" in field && field.enum) { + const baseTypeName = "type" in field ? field.type.name : ""; + if (GENERIC_COMPLEX_TYPES.has(baseTypeName)) { + fieldType = `${fieldType}[${pyEnumType(field.enum)}]`; + } else if (!field.enum.isOpen) { + const s: string = field.enum.values.map((e: string) => `"${e}"`).join(", "); + fieldType = `Literal[${s}]`; + } } if (field.array) { @@ -535,10 +576,17 @@ export class Python extends Writer { } } - private generateDefaultImports(): void { + private generateDefaultImports(includeGenericImports = false): void { this.pyImportFrom("__future__", "annotations"); this.pyImportFrom("pydantic", "BaseModel", "ConfigDict", "Field", "PositiveInt"); - this.pyImportFrom("typing", "List as PyList", "Literal"); + const typingImports = ["List as PyList", "Literal"]; + if (includeGenericImports) { + typingImports.push("Generic"); + } + this.pyImportFrom("typing", ...typingImports.sort()); + if (includeGenericImports) { + this.pyImportFrom("typing_extensions", "TypeVar"); + } } private generateDependenciesImports(schema: RegularTypeSchema): void { diff --git a/test/api/write-generator/__snapshots__/python.test.ts.snap b/test/api/write-generator/__snapshots__/python.test.ts.snap index 953f52fb0..87e8361cb 100644 --- a/test/api/write-generator/__snapshots__/python.test.ts.snap +++ b/test/api/write-generator/__snapshots__/python.test.ts.snap @@ -18,7 +18,7 @@ from fhir_types.hl7_fhir_r4_core.resource_families import DomainResourceFamily class PatientCommunication(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - language: CodeableConcept = Field(alias="language", serialization_alias="language") + language: CodeableConcept[Literal["ar", "bn", "cs", "da", "de", "de-AT", "de-CH", "de-DE", "el", "en", "en-AU", "en-CA", "en-GB", "en-IN", "en-NZ", "en-SG", "en-US", "es", "es-AR", "es-ES", "es-UY", "fi", "fr", "fr-BE", "fr-CH", "fr-FR", "fy", "fy-NL", "hi", "hr", "it", "it-CH", "it-IT", "ja", "ko", "nl", "nl-BE", "nl-NL", "no", "no-NO", "pa", "pl", "pt", "pt-BR", "ru", "ru-RU", "sr", "sr-RS", "sv", "sv-SE", "te", "zh", "zh-CN", "zh-HK", "zh-SG", "zh-TW"] | str] = Field(alias="language", serialization_alias="language") preferred: bool | None = Field(None, alias="preferred", serialization_alias="preferred") class PatientContact(BackboneElement): @@ -58,7 +58,7 @@ class Patient(DomainResource): identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") managing_organization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") - marital_status: CodeableConcept | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") + marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multiple_birth_boolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multiple_birth_integer: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger") name: PyList[HumanName] | None = Field(None, alias="name", serialization_alias="name") diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index c100b9264..6ee40ca94 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -83,199 +83,6 @@ export const isPatient = (resource: unknown): resource is Patient => { " `; -exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 1`] = ` -"// WARNING: This file is autogenerated by @atomic-ehr/codegen. -// GitHub: https://github.com/atomic-ehr/codegen -// Any manual changes made to this file may be overwritten. - -import type { CD } from "../hl7-cda-uv-core/CD"; -import type { CE } from "../hl7-cda-uv-core/CE"; - -// CanonicalURL: http://hl7.org/cda/stds/core/StructureDefinition/CV (pkg: hl7.cda.uv.core#2.0.1-sd) -export interface CV extends CE { - translation?: CD[]; -} -" -`; - -exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 2`] = ` -"export * from "./profiles"; -export type { Act } from "./Act"; -export type { AD, ADItem } from "./AD"; -export type { ADXP } from "./ADXP"; -export type { AlternateIdentification } from "./AlternateIdentification"; -export type { ANY } from "./ANY"; -export type { AssignedAuthor } from "./AssignedAuthor"; -export type { AssignedCustodian } from "./AssignedCustodian"; -export type { AssignedEntity, AssignedEntitySdtcPatient } from "./AssignedEntity"; -export type { AssociatedEntity } from "./AssociatedEntity"; -export type { Authenticator } from "./Authenticator"; -export type { Author } from "./Author"; -export type { AuthoringDevice } from "./AuthoringDevice"; -export type { Authorization } from "./Authorization"; -export type { Birthplace } from "./Birthplace"; -export type { BL } from "./BL"; -export type { CD } from "./CD"; -export type { CE } from "./CE"; -export type { ClinicalDocument } from "./ClinicalDocument"; -export type { CO } from "./CO"; -export type { Component } from "./Component"; -export type { ComponentOf } from "./ComponentOf"; -export type { Consent } from "./Consent"; -export type { CR } from "./CR"; -export type { Criterion } from "./Criterion"; -export type { CS } from "./CS"; -export type { Custodian } from "./Custodian"; -export type { CustodianOrganization } from "./CustodianOrganization"; -export type { CV } from "./CV"; -export type { DataEnterer } from "./DataEnterer"; -export type { Device } from "./Device"; -export type { DocumentationOf } from "./DocumentationOf"; -export type { ED } from "./ED"; -export type { EIVL_TS } from "./EIVL_TS"; -export type { EN, ENItem } from "./EN"; -export type { EncompassingEncounter, EncompassingEncounterLocation, EncompassingEncounterResponsibleParty } from "./EncompassingEncounter"; -export type { Encounter } from "./Encounter"; -export type { EncounterParticipant } from "./EncounterParticipant"; -export type { Entity } from "./Entity"; -export type { Entry } from "./Entry"; -export type { EntryRelationship } from "./EntryRelationship"; -export type { ENXP } from "./ENXP"; -export type { ExternalAct } from "./ExternalAct"; -export type { ExternalDocument } from "./ExternalDocument"; -export type { ExternalObservation } from "./ExternalObservation"; -export type { ExternalProcedure } from "./ExternalProcedure"; -export type { Guardian } from "./Guardian"; -export type { HealthCareFacility } from "./HealthCareFacility"; -export type { IdentifiedBy } from "./IdentifiedBy"; -export type { II } from "./II"; -export type { Informant } from "./Informant"; -export type { InformationRecipient } from "./InformationRecipient"; -export type { InfrastructureRoot } from "./InfrastructureRoot"; -export type { InFulfillmentOf } from "./InFulfillmentOf"; -export type { InFulfillmentOf1, InFulfillmentOf1ActReference } from "./InFulfillmentOf1"; -export type { INT } from "./INT"; -export type { INT_POS } from "./INT_POS"; -export type { IntendedRecipient } from "./IntendedRecipient"; -export type { IVL_INT } from "./IVL_INT"; -export type { IVL_PQ } from "./IVL_PQ"; -export type { IVL_TS } from "./IVL_TS"; -export type { IVXB_INT } from "./IVXB_INT"; -export type { IVXB_PQ } from "./IVXB_PQ"; -export type { IVXB_TS } from "./IVXB_TS"; -export type { LabeledDrug } from "./LabeledDrug"; -export type { LanguageCommunication } from "./LanguageCommunication"; -export type { LegalAuthenticator } from "./LegalAuthenticator"; -export type { MaintainedEntity } from "./MaintainedEntity"; -export type { ManufacturedProduct } from "./ManufacturedProduct"; -export type { Material } from "./Material"; -export { isMaterial } from "./Material"; -export type { MO } from "./MO"; -export type { NonXMLBody } from "./NonXMLBody"; -export type { Observation, ObservationReferenceRange } from "./Observation"; -export type { ObservationMedia } from "./ObservationMedia"; -export type { ObservationRange, ObservationRangeSdtcPrecondition1 } from "./ObservationRange"; -export type { ON, ONItem } from "./ON"; -export type { Order } from "./Order"; -export type { Organization } from "./Organization"; -export type { OrganizationPartOf } from "./OrganizationPartOf"; -export type { Organizer } from "./Organizer"; -export type { OrganizerComponent } from "./OrganizerComponent"; -export type { ParentDocument } from "./ParentDocument"; -export type { Participant1 } from "./Participant1"; -export type { Participant2 } from "./Participant2"; -export type { ParticipantRole } from "./ParticipantRole"; -export type { Patient } from "./Patient"; -export type { PatientRole } from "./PatientRole"; -export type { Performer1 } from "./Performer1"; -export type { Performer2 } from "./Performer2"; -export type { Person, PersonSdtcAsPatientRelationship } from "./Person"; -export type { PIVL_TS } from "./PIVL_TS"; -export type { Place } from "./Place"; -export type { PlayingEntity } from "./PlayingEntity"; -export type { PN } from "./PN"; -export type { PQ } from "./PQ"; -export type { PQR } from "./PQR"; -export type { Precondition } from "./Precondition"; -export type { Precondition2 } from "./Precondition2"; -export type { PreconditionBase } from "./PreconditionBase"; -export type { Procedure } from "./Procedure"; -export type { QTY } from "./QTY"; -export type { REAL } from "./REAL"; -export type { RecordTarget } from "./RecordTarget"; -export type { Reference } from "./Reference"; -export type { RegionOfInterest } from "./RegionOfInterest"; -export type { RelatedDocument } from "./RelatedDocument"; -export type { RelatedEntity } from "./RelatedEntity"; -export type { RelatedSubject } from "./RelatedSubject"; -export type { RTO_PQ_PQ } from "./RTO_PQ_PQ"; -export type { SC } from "./SC"; -export type { Section, SectionComponent } from "./Section"; -export type { ServiceEvent } from "./ServiceEvent"; -export type { Specimen } from "./Specimen"; -export type { SpecimenRole } from "./SpecimenRole"; -export type { ST } from "./ST"; -export type { StructuredBody, StructuredBodyComponent } from "./StructuredBody"; -export type { Subject } from "./Subject"; -export type { SubjectPerson } from "./SubjectPerson"; -export type { SubstanceAdministration, SubstanceAdministrationConsumable } from "./SubstanceAdministration"; -export type { Supply, SupplyProduct } from "./Supply"; -export type { SXCM_TS } from "./SXCM_TS"; -export type { SXPR_TS } from "./SXPR_TS"; -export type { TEL } from "./TEL"; -export type { TS } from "./TS"; -" -`; - -exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 3`] = ` -"export { TNProfile } from "./EN_TN"; -export { binProfile } from "./Base64Binary_bin"; -export { blProfile } from "./Boolean_bl"; -export { bnProfile } from "./Boolean_bn"; -export { csProfile } from "./Code_cs"; -export { intProfile } from "./Integer_int"; -export { oidProfile } from "./String_oid"; -export { probabilityProfile } from "./Decimal_probability"; -export { realProfile } from "./Decimal_real"; -export { ruidProfile } from "./String_ruid"; -export { stProfile } from "./String_st"; -export { tsProfile } from "./DateTime_ts"; -export { uidProfile } from "./String_uid"; -export { urlProfile } from "./Url_url"; -export { uuidProfile } from "./String_uuid"; -export { xs_IDProfile } from "./Id_xs_ID"; -" -`; - -exports[`TypeScript CDA with Logical Model Promotion to Resource with resourceType 1`] = ` -"// WARNING: This file is autogenerated by @atomic-ehr/codegen. -// GitHub: https://github.com/atomic-ehr/codegen -// Any manual changes made to this file may be overwritten. - -import type { CE } from "../hl7-cda-uv-core/CE"; -import type { EN } from "../hl7-cda-uv-core/EN"; -import type { InfrastructureRoot } from "../hl7-cda-uv-core/InfrastructureRoot"; -import type { ST } from "../hl7-cda-uv-core/ST"; - -import type { Element } from "../hl7-fhir-r5-core/Element"; -// CanonicalURL: http://hl7.org/cda/stds/core/StructureDefinition/Material (pkg: hl7.cda.uv.core#2.0.1-sd) -export interface Material extends InfrastructureRoot { - resourceType: "Material"; - - classCode?: string; - _classCode?: Element; - code?: CE; - determinerCode?: string; - _determinerCode?: Element; - lotNumberText?: ST; - name?: EN; -} -export const isMaterial = (resource: unknown): resource is Material => { - return resource !== null && typeof resource === "object" && (resource as {resourceType: string}).resourceType === "Material"; -} -" -`; - exports[`TypeScript R4 Example (with generateProfile) file rewrite warnings match expected collisions 1`] = ` [ "File will be rewritten 'generated/types/hl7-fhir-r4-core/profiles/Extension_assertedDate.ts'", diff --git a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap index e292d36e6..a7c62b6bf 100644 --- a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap +++ b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap @@ -123,7 +123,7 @@ from fhir_types.hl7_fhir_r4_core.resource_families import DomainResourceFamily class PatientCommunication(BackboneElement): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - language: CodeableConcept = Field(alias="language", serialization_alias="language") + language: CodeableConcept[Literal["ar", "bn", "cs", "da", "de", "de-AT", "de-CH", "de-DE", "el", "en", "en-AU", "en-CA", "en-GB", "en-IN", "en-NZ", "en-SG", "en-US", "es", "es-AR", "es-ES", "es-UY", "fi", "fr", "fr-BE", "fr-CH", "fr-FR", "fy", "fy-NL", "hi", "hr", "it", "it-CH", "it-IT", "ja", "ko", "nl", "nl-BE", "nl-NL", "no", "no-NO", "pa", "pl", "pt", "pt-BR", "ru", "ru-RU", "sr", "sr-RS", "sv", "sv-SE", "te", "zh", "zh-CN", "zh-HK", "zh-SG", "zh-TW"] | str] = Field(alias="language", serialization_alias="language") preferred: bool | None = Field(None, alias="preferred", serialization_alias="preferred") class PatientContact(BackboneElement): @@ -163,7 +163,7 @@ class Patient(DomainResource): identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier") link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link") managing_organization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization") - marital_status: CodeableConcept | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") + marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus") multiple_birth_boolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean") multiple_birth_integer: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger") name: PyList[HumanName] | None = Field(None, alias="name", serialization_alias="name") diff --git a/test/api/write-generator/python.test.ts b/test/api/write-generator/python.test.ts index 265c95308..04e99814d 100644 --- a/test/api/write-generator/python.test.ts +++ b/test/api/write-generator/python.test.ts @@ -17,4 +17,26 @@ describe("Python Writer Generator", async () => { it("static files", async () => { expect(result.filesGenerated["generated/requirements.txt"]).toMatchSnapshot(); }); + it("generates Coding with Generic[T] parameter", async () => { + const basePy = result.filesGenerated["generated/hl7_fhir_r4_core/base.py"]; + expect(basePy).toContain("class Coding(Element, Generic[T]):"); + expect(basePy).toContain("code: T | None"); + }); + it("generates CodeableConcept with Generic[T] parameter", async () => { + const basePy = result.filesGenerated["generated/hl7_fhir_r4_core/base.py"]; + expect(basePy).toContain("class CodeableConcept(Element, Generic[T]):"); + expect(basePy).toContain("coding: PyList[Coding[T]] | None"); + }); + it("generates CodeableConcept fields with enum bindings", async () => { + const patientPy = result.filesGenerated["generated/hl7_fhir_r4_core/patient.py"]; + expect(patientPy).toContain( + 'marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None', + ); + }); + it("generates base.py with TypeVar import and declaration", async () => { + const basePy = result.filesGenerated["generated/hl7_fhir_r4_core/base.py"]; + expect(basePy).toContain("from typing import Generic, List as PyList, Literal"); + expect(basePy).toContain("from typing_extensions import TypeVar"); + expect(basePy).toContain("T = TypeVar('T', bound=str, default=str)"); + }); }); From c2502cb27c67c2c1958db12cd63ed5b22254dffd Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Thu, 5 Mar 2026 14:55:13 +0000 Subject: [PATCH 2/3] fixed snapshot update --- .../__snapshots__/typescript.test.ts.snap | 195 +++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/test/api/write-generator/__snapshots__/typescript.test.ts.snap b/test/api/write-generator/__snapshots__/typescript.test.ts.snap index 6ee40ca94..4ab0cfe88 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -83,6 +83,199 @@ export const isPatient = (resource: unknown): resource is Patient => { " `; +exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CD } from "../hl7-cda-uv-core/CD"; +import type { CE } from "../hl7-cda-uv-core/CE"; + +// CanonicalURL: http://hl7.org/cda/stds/core/StructureDefinition/CV (pkg: hl7.cda.uv.core#2.0.1-sd) +export interface CV extends CE { + translation?: CD[]; +} +" +`; + +exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 2`] = ` +"export * from "./profiles"; +export type { Act } from "./Act"; +export type { AD, ADItem } from "./AD"; +export type { ADXP } from "./ADXP"; +export type { AlternateIdentification } from "./AlternateIdentification"; +export type { ANY } from "./ANY"; +export type { AssignedAuthor } from "./AssignedAuthor"; +export type { AssignedCustodian } from "./AssignedCustodian"; +export type { AssignedEntity, AssignedEntitySdtcPatient } from "./AssignedEntity"; +export type { AssociatedEntity } from "./AssociatedEntity"; +export type { Authenticator } from "./Authenticator"; +export type { Author } from "./Author"; +export type { AuthoringDevice } from "./AuthoringDevice"; +export type { Authorization } from "./Authorization"; +export type { Birthplace } from "./Birthplace"; +export type { BL } from "./BL"; +export type { CD } from "./CD"; +export type { CE } from "./CE"; +export type { ClinicalDocument } from "./ClinicalDocument"; +export type { CO } from "./CO"; +export type { Component } from "./Component"; +export type { ComponentOf } from "./ComponentOf"; +export type { Consent } from "./Consent"; +export type { CR } from "./CR"; +export type { Criterion } from "./Criterion"; +export type { CS } from "./CS"; +export type { Custodian } from "./Custodian"; +export type { CustodianOrganization } from "./CustodianOrganization"; +export type { CV } from "./CV"; +export type { DataEnterer } from "./DataEnterer"; +export type { Device } from "./Device"; +export type { DocumentationOf } from "./DocumentationOf"; +export type { ED } from "./ED"; +export type { EIVL_TS } from "./EIVL_TS"; +export type { EN, ENItem } from "./EN"; +export type { EncompassingEncounter, EncompassingEncounterLocation, EncompassingEncounterResponsibleParty } from "./EncompassingEncounter"; +export type { Encounter } from "./Encounter"; +export type { EncounterParticipant } from "./EncounterParticipant"; +export type { Entity } from "./Entity"; +export type { Entry } from "./Entry"; +export type { EntryRelationship } from "./EntryRelationship"; +export type { ENXP } from "./ENXP"; +export type { ExternalAct } from "./ExternalAct"; +export type { ExternalDocument } from "./ExternalDocument"; +export type { ExternalObservation } from "./ExternalObservation"; +export type { ExternalProcedure } from "./ExternalProcedure"; +export type { Guardian } from "./Guardian"; +export type { HealthCareFacility } from "./HealthCareFacility"; +export type { IdentifiedBy } from "./IdentifiedBy"; +export type { II } from "./II"; +export type { Informant } from "./Informant"; +export type { InformationRecipient } from "./InformationRecipient"; +export type { InfrastructureRoot } from "./InfrastructureRoot"; +export type { InFulfillmentOf } from "./InFulfillmentOf"; +export type { InFulfillmentOf1, InFulfillmentOf1ActReference } from "./InFulfillmentOf1"; +export type { INT } from "./INT"; +export type { INT_POS } from "./INT_POS"; +export type { IntendedRecipient } from "./IntendedRecipient"; +export type { IVL_INT } from "./IVL_INT"; +export type { IVL_PQ } from "./IVL_PQ"; +export type { IVL_TS } from "./IVL_TS"; +export type { IVXB_INT } from "./IVXB_INT"; +export type { IVXB_PQ } from "./IVXB_PQ"; +export type { IVXB_TS } from "./IVXB_TS"; +export type { LabeledDrug } from "./LabeledDrug"; +export type { LanguageCommunication } from "./LanguageCommunication"; +export type { LegalAuthenticator } from "./LegalAuthenticator"; +export type { MaintainedEntity } from "./MaintainedEntity"; +export type { ManufacturedProduct } from "./ManufacturedProduct"; +export type { Material } from "./Material"; +export { isMaterial } from "./Material"; +export type { MO } from "./MO"; +export type { NonXMLBody } from "./NonXMLBody"; +export type { Observation, ObservationReferenceRange } from "./Observation"; +export type { ObservationMedia } from "./ObservationMedia"; +export type { ObservationRange, ObservationRangeSdtcPrecondition1 } from "./ObservationRange"; +export type { ON, ONItem } from "./ON"; +export type { Order } from "./Order"; +export type { Organization } from "./Organization"; +export type { OrganizationPartOf } from "./OrganizationPartOf"; +export type { Organizer } from "./Organizer"; +export type { OrganizerComponent } from "./OrganizerComponent"; +export type { ParentDocument } from "./ParentDocument"; +export type { Participant1 } from "./Participant1"; +export type { Participant2 } from "./Participant2"; +export type { ParticipantRole } from "./ParticipantRole"; +export type { Patient } from "./Patient"; +export type { PatientRole } from "./PatientRole"; +export type { Performer1 } from "./Performer1"; +export type { Performer2 } from "./Performer2"; +export type { Person, PersonSdtcAsPatientRelationship } from "./Person"; +export type { PIVL_TS } from "./PIVL_TS"; +export type { Place } from "./Place"; +export type { PlayingEntity } from "./PlayingEntity"; +export type { PN } from "./PN"; +export type { PQ } from "./PQ"; +export type { PQR } from "./PQR"; +export type { Precondition } from "./Precondition"; +export type { Precondition2 } from "./Precondition2"; +export type { PreconditionBase } from "./PreconditionBase"; +export type { Procedure } from "./Procedure"; +export type { QTY } from "./QTY"; +export type { REAL } from "./REAL"; +export type { RecordTarget } from "./RecordTarget"; +export type { Reference } from "./Reference"; +export type { RegionOfInterest } from "./RegionOfInterest"; +export type { RelatedDocument } from "./RelatedDocument"; +export type { RelatedEntity } from "./RelatedEntity"; +export type { RelatedSubject } from "./RelatedSubject"; +export type { RTO_PQ_PQ } from "./RTO_PQ_PQ"; +export type { SC } from "./SC"; +export type { Section, SectionComponent } from "./Section"; +export type { ServiceEvent } from "./ServiceEvent"; +export type { Specimen } from "./Specimen"; +export type { SpecimenRole } from "./SpecimenRole"; +export type { ST } from "./ST"; +export type { StructuredBody, StructuredBodyComponent } from "./StructuredBody"; +export type { Subject } from "./Subject"; +export type { SubjectPerson } from "./SubjectPerson"; +export type { SubstanceAdministration, SubstanceAdministrationConsumable } from "./SubstanceAdministration"; +export type { Supply, SupplyProduct } from "./Supply"; +export type { SXCM_TS } from "./SXCM_TS"; +export type { SXPR_TS } from "./SXPR_TS"; +export type { TEL } from "./TEL"; +export type { TS } from "./TS"; +" +`; + +exports[`TypeScript CDA with Logical Model Promotion to Resource without resourceType 3`] = ` +"export { TNProfile } from "./EN_TN"; +export { binProfile } from "./Base64Binary_bin"; +export { blProfile } from "./Boolean_bl"; +export { bnProfile } from "./Boolean_bn"; +export { csProfile } from "./Code_cs"; +export { intProfile } from "./Integer_int"; +export { oidProfile } from "./String_oid"; +export { probabilityProfile } from "./Decimal_probability"; +export { realProfile } from "./Decimal_real"; +export { ruidProfile } from "./String_ruid"; +export { stProfile } from "./String_st"; +export { tsProfile } from "./DateTime_ts"; +export { uidProfile } from "./String_uid"; +export { urlProfile } from "./Url_url"; +export { uuidProfile } from "./String_uuid"; +export { xs_IDProfile } from "./Id_xs_ID"; +" +`; + +exports[`TypeScript CDA with Logical Model Promotion to Resource with resourceType 1`] = ` +"// WARNING: This file is autogenerated by @atomic-ehr/codegen. +// GitHub: https://github.com/atomic-ehr/codegen +// Any manual changes made to this file may be overwritten. + +import type { CE } from "../hl7-cda-uv-core/CE"; +import type { EN } from "../hl7-cda-uv-core/EN"; +import type { InfrastructureRoot } from "../hl7-cda-uv-core/InfrastructureRoot"; +import type { ST } from "../hl7-cda-uv-core/ST"; + +import type { Element } from "../hl7-fhir-r5-core/Element"; +// CanonicalURL: http://hl7.org/cda/stds/core/StructureDefinition/Material (pkg: hl7.cda.uv.core#2.0.1-sd) +export interface Material extends InfrastructureRoot { + resourceType: "Material"; + + classCode?: string; + _classCode?: Element; + code?: CE; + determinerCode?: string; + _determinerCode?: Element; + lotNumberText?: ST; + name?: EN; +} +export const isMaterial = (resource: unknown): resource is Material => { + return resource !== null && typeof resource === "object" && (resource as {resourceType: string}).resourceType === "Material"; +} +" +`; + exports[`TypeScript R4 Example (with generateProfile) file rewrite warnings match expected collisions 1`] = ` [ "File will be rewritten 'generated/types/hl7-fhir-r4-core/profiles/Extension_assertedDate.ts'", @@ -572,4 +765,4 @@ export class observation_bpProfile { } " -`; +`; \ No newline at end of file From 8590ffd59fc6909fccb8df99c00d50434ee22ec1 Mon Sep 17 00:00:00 2001 From: MikhailArtemyev Date: Mon, 9 Mar 2026 16:46:36 +0000 Subject: [PATCH 3/3] minor fixes --- src/api/writer-generator/python.ts | 49 ++++++++++++------------------ 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/api/writer-generator/python.ts b/src/api/writer-generator/python.ts index d73b3389c..dcd033197 100644 --- a/src/api/writer-generator/python.ts +++ b/src/api/writer-generator/python.ts @@ -80,8 +80,6 @@ const PYTHON_KEYWORDS = new Set([ const MAX_IMPORT_LINE_LENGTH = 100; -const GENERIC_COMPLEX_TYPES = new Set(["Coding", "CodeableConcept"]); - const GENERIC_FIELD_REWRITES: Record> = { Coding: { code: "T" }, CodeableConcept: { coding: "Coding[T]" }, @@ -159,8 +157,6 @@ export class Python extends Writer { private tsIndex: TypeSchemaIndex | undefined; private readonly forFhirpyClient: boolean; private readonly fieldFormat: StringFormatKey; - private currentSchemaName: string | undefined; - constructor(options: PythonGeneratorOptions) { super({ ...options, resolveAssets: options.resolveAssets ?? resolvePyAssets }); this.nameFormatFunction = this.getFieldFormatFunction(options.fieldFormat); @@ -260,7 +256,7 @@ export class Python extends Writer { } private generateBasePy(packageComplexTypes: RegularTypeSchema[]): void { - const hasGenericTypes = packageComplexTypes.some((s) => GENERIC_COMPLEX_TYPES.has(s.identifier.name)); + const hasGenericTypes = packageComplexTypes.some((s) => s.identifier.name in GENERIC_FIELD_REWRITES); this.cat("base.py", () => { this.generateDisclaimer(); this.generateDefaultImports(hasGenericTypes); @@ -392,7 +388,7 @@ export class Python extends Writer { private generateResourceModule(schema: RegularTypeSchema): void { this.cat(`${snakeCase(schema.identifier.name)}.py`, () => { this.generateDisclaimer(); - this.generateDefaultImports(); + this.generateDefaultImports(false); this.generateFhirBaseModelImport(); this.line(); this.generateDependenciesImports(schema); @@ -420,20 +416,18 @@ export class Python extends Writer { } private getSuperClasses(schema: RegularTypeSchema): string[] { - const bases = [...(schema.base ? [schema.base.name] : []), ...this.injectSuperClasses(schema.identifier.url)]; - if (GENERIC_COMPLEX_TYPES.has(schema.identifier.name)) { - bases.push("Generic[T]"); - } + const bases: string[] = []; + if (schema.base) bases.push(schema.base.name); + bases.push(...this.injectSuperClasses(schema.identifier.url)); + if (schema.identifier.name in GENERIC_FIELD_REWRITES) bases.push("Generic[T]"); return bases; } private generateClassBody(schema: RegularTypeSchema): void { - this.currentSchemaName = schema.identifier.name; this.generateModelConfig(); if (!schema.fields) { this.line("pass"); - this.currentSchemaName = undefined; return; } @@ -441,12 +435,11 @@ export class Python extends Writer { this.generateResourceTypeField(schema); } - this.generateFields(schema); + this.generateFields(schema, schema.identifier.name); if (schema.identifier.kind === "resource") { this.generateResourceMethods(schema); } - this.currentSchemaName = undefined; } private generateModelConfig(): void { @@ -476,20 +469,20 @@ export class Python extends Writer { this.line(")"); } - private generateFields(schema: RegularTypeSchema): void { + private generateFields(schema: RegularTypeSchema, schemaName: string): void { const sortedFields = Object.entries(schema.fields ?? []).sort(([a], [b]) => a.localeCompare(b)); for (const [fieldName, field] of sortedFields) { if ("choices" in field && field.choices) continue; - const fieldInfo = this.buildFieldInfo(fieldName, field); + const fieldInfo = this.buildFieldInfo(fieldName, field, schemaName); this.line(`${fieldInfo.name}: ${fieldInfo.type}${fieldInfo.defaultValue}`); } } - private buildFieldInfo(fieldName: string, field: Field): FieldInfo { + private buildFieldInfo(fieldName: string, field: Field, schemaName: string): FieldInfo { const pyFieldName = fixReservedWords(this.nameFormatFunction(fieldName)); - const fieldType = this.determineFieldType(field, fieldName); + const fieldType = this.determineFieldType(field, fieldName, schemaName); const defaultValue = this.getFieldDefaultValue(field, fieldName); return { @@ -499,23 +492,21 @@ export class Python extends Writer { }; } - private determineFieldType(field: Field, fieldName?: string): string { + private determineFieldType(field: Field, fieldName: string, schemaName: string): string { let fieldType = field ? this.getBaseFieldType(field) : ""; // Check for generic type field rewrites (e.g., Coding.code → T, CodeableConcept.coding → Coding[T]) - if (this.currentSchemaName && fieldName) { - const rewrite = GENERIC_FIELD_REWRITES[this.currentSchemaName]?.[fieldName]; - if (rewrite) { - fieldType = rewrite; - if (field.array) fieldType = `PyList[${fieldType}]`; - if (!field.required) fieldType = `${fieldType} | None`; - return fieldType; - } + const rewrite = GENERIC_FIELD_REWRITES[schemaName]?.[fieldName]; + if (rewrite) { + fieldType = rewrite; + if (field.array) fieldType = `PyList[${fieldType}]`; + if (!field.required) fieldType = `${fieldType} | None`; + return fieldType; } if ("enum" in field && field.enum) { const baseTypeName = "type" in field ? field.type.name : ""; - if (GENERIC_COMPLEX_TYPES.has(baseTypeName)) { + if (baseTypeName in GENERIC_FIELD_REWRITES) { fieldType = `${fieldType}[${pyEnumType(field.enum)}]`; } else if (!field.enum.isOpen) { const s: string = field.enum.values.map((e: string) => `"${e}"`).join(", "); @@ -576,7 +567,7 @@ export class Python extends Writer { } } - private generateDefaultImports(includeGenericImports = false): void { + private generateDefaultImports(includeGenericImports: boolean): void { this.pyImportFrom("__future__", "annotations"); this.pyImportFrom("pydantic", "BaseModel", "ConfigDict", "Field", "PositiveInt"); const typingImports = ["List as PyList", "Literal"];