Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions examples/python/fhir_types/hl7_fhir_r4_core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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")

Expand All @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions examples/python/fhir_types/hl7_fhir_r4_core/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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):
Expand All @@ -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")
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions examples/python/fhir_types/hl7_fhir_r4_core/patient.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down
71 changes: 55 additions & 16 deletions src/api/writer-generator/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
boolean: "bool",
Expand Down Expand Up @@ -80,6 +80,16 @@ const PYTHON_KEYWORDS = new Set([

const MAX_IMPORT_LINE_LENGTH = 100;

const GENERIC_FIELD_REWRITES: Record<string, Record<string, string>> = {
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. <rootPackageName>.hl7_fhir_r4_core.Patient.
Expand Down Expand Up @@ -147,7 +157,6 @@ export class Python extends Writer<PythonGeneratorOptions> {
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);
Expand Down Expand Up @@ -247,9 +256,14 @@ export class Python extends Writer<PythonGeneratorOptions> {
}

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();
Expand Down Expand Up @@ -374,7 +388,7 @@ export class Python extends Writer<PythonGeneratorOptions> {
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);
Expand Down Expand Up @@ -402,7 +416,11 @@ export class Python extends Writer<PythonGeneratorOptions> {
}

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;
Comment thread
MikhailArtemyev marked this conversation as resolved.
}

private generateClassBody(schema: RegularTypeSchema): void {
Expand All @@ -417,7 +435,7 @@ export class Python extends Writer<PythonGeneratorOptions> {
this.generateResourceTypeField(schema);
}

this.generateFields(schema);
this.generateFields(schema, schema.identifier.name);

if (schema.identifier.kind === "resource") {
this.generateResourceMethods(schema);
Expand Down Expand Up @@ -451,20 +469,20 @@ export class Python extends Writer<PythonGeneratorOptions> {
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 {
Expand All @@ -474,12 +492,26 @@ export class Python extends Writer<PythonGeneratorOptions> {
};
}

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) {
Expand Down Expand Up @@ -535,10 +567,17 @@ export class Python extends Writer<PythonGeneratorOptions> {
}
}

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"];
Comment thread
ryukzak marked this conversation as resolved.
if (includeGenericImports) {
typingImports.push("Generic");
}
this.pyImportFrom("typing", ...typingImports.sort());
if (includeGenericImports) {
this.pyImportFrom("typing_extensions", "TypeVar");
}
}

private generateDependenciesImports(schema: RegularTypeSchema): void {
Expand Down
Loading