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..dcd033197 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,16 @@ const PYTHON_KEYWORDS = new Set([ const MAX_IMPORT_LINE_LENGTH = 100; +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,7 +157,6 @@ export class Python extends Writer { private tsIndex: TypeSchemaIndex | undefined; private readonly forFhirpyClient: boolean; private readonly fieldFormat: StringFormatKey; - constructor(options: PythonGeneratorOptions) { super({ ...options, resolveAssets: options.resolveAssets ?? resolvePyAssets }); this.nameFormatFunction = this.getFieldFormatFunction(options.fieldFormat); @@ -247,9 +256,14 @@ export class Python extends Writer { } private generateBasePy(packageComplexTypes: RegularTypeSchema[]): void { + const hasGenericTypes = packageComplexTypes.some((s) => s.identifier.name in GENERIC_FIELD_REWRITES); 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(); @@ -374,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); @@ -402,7 +416,11 @@ export class Python extends Writer { } private getSuperClasses(schema: RegularTypeSchema): string[] { - return [...(schema.base ? [schema.base.name] : []), ...this.injectSuperClasses(schema.identifier.url)]; + 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 { @@ -417,7 +435,7 @@ 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); @@ -451,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); + const fieldType = this.determineFieldType(field, fieldName, schemaName); const defaultValue = this.getFieldDefaultValue(field, fieldName); return { @@ -474,12 +492,26 @@ export class Python extends Writer { }; } - private determineFieldType(field: Field): string { + private determineFieldType(field: Field, fieldName: string, schemaName: 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]) + 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 (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(", "); + fieldType = `Literal[${s}]`; + } } if (field.array) { @@ -535,10 +567,17 @@ export class Python extends Writer { } } - private generateDefaultImports(): void { + private generateDefaultImports(includeGenericImports: boolean): 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..4ab0cfe88 100644 --- a/test/api/write-generator/__snapshots__/typescript.test.ts.snap +++ b/test/api/write-generator/__snapshots__/typescript.test.ts.snap @@ -765,4 +765,4 @@ export class observation_bpProfile { } " -`; +`; \ No newline at end of file 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)"); + }); });