Skip to content

Commit 4f9fb17

Browse files
python extension profile handling implementation
1 parent f499905 commit 4f9fb17

11 files changed

Lines changed: 730 additions & 108 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# WARNING: This file is autogenerated by @atomic-ehr/codegen.
2+
# GitHub: https://github.com/atomic-ehr/codegen
3+
# Any manual changes made to this file may be overwritten.
4+
5+
from fhir_types.hl7_fhir_r4_core.profiles.extension_birth_place import BirthPlaceExtension
6+
from fhir_types.hl7_fhir_r4_core.profiles.extension_birth_time import BirthTimeExtension
7+
from fhir_types.hl7_fhir_r4_core.profiles.extension_nationality import (\
8+
NationalityExtension, NationalityCodeExtension, NationalityPeriodExtension
9+
)
10+
from fhir_types.hl7_fhir_r4_core.profiles.extension_own_prefix import OwnPrefixExtension
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# WARNING: This file is autogenerated by @atomic-ehr/codegen.
2+
# GitHub: https://github.com/atomic-ehr/codegen
3+
# Any manual changes made to this file may be overwritten.
4+
5+
from __future__ import annotations
6+
from typing import Literal
7+
from pydantic import Field
8+
from fhir_types.hl7_fhir_r4_core.base import Address, Extension
9+
10+
11+
class BirthPlaceExtension(Extension):
12+
"""The registered place of birth of the patient. A sytem may use the address.text if they don't store the birthPlace address in discrete elements.
13+
14+
CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-birthPlace
15+
"""
16+
url: Literal["http://hl7.org/fhir/StructureDefinition/patient-birthPlace"] = Field(
17+
"http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
18+
alias="url", serialization_alias="url",
19+
)
20+
value_address: Address = Field(alias="valueAddress", serialization_alias="valueAddress")
21+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# WARNING: This file is autogenerated by @atomic-ehr/codegen.
2+
# GitHub: https://github.com/atomic-ehr/codegen
3+
# Any manual changes made to this file may be overwritten.
4+
5+
from __future__ import annotations
6+
from typing import Literal
7+
from pydantic import Field
8+
from fhir_types.hl7_fhir_r4_core.base import Extension
9+
10+
11+
class BirthTimeExtension(Extension):
12+
"""The time of day that the Patient was born. This includes the date to ensure that the timezone information can be communicated effectively.
13+
14+
CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-birthTime
15+
"""
16+
url: Literal["http://hl7.org/fhir/StructureDefinition/patient-birthTime"] = Field(
17+
"http://hl7.org/fhir/StructureDefinition/patient-birthTime",
18+
alias="url", serialization_alias="url",
19+
)
20+
value_date_time: str = Field(alias="valueDateTime", serialization_alias="valueDateTime")
21+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# WARNING: This file is autogenerated by @atomic-ehr/codegen.
2+
# GitHub: https://github.com/atomic-ehr/codegen
3+
# Any manual changes made to this file may be overwritten.
4+
5+
from __future__ import annotations
6+
from typing import Annotated, Literal, Union
7+
from pydantic import Discriminator, Field, Tag
8+
from fhir_types.hl7_fhir_r4_core.base import CodeableConcept, Extension, Period
9+
10+
11+
class NationalityCodeExtension(Extension):
12+
"""Sub-extension: code"""
13+
url: Literal["code"] = Field("code", alias="url", serialization_alias="url")
14+
value_codeable_concept: CodeableConcept = Field(alias="valueCodeableConcept", serialization_alias="valueCodeableConcept")
15+
16+
17+
class NationalityPeriodExtension(Extension):
18+
"""Sub-extension: period"""
19+
url: Literal["period"] = Field("period", alias="url", serialization_alias="url")
20+
value_period: Period = Field(alias="valuePeriod", serialization_alias="valuePeriod")
21+
22+
23+
NationalitySubExtension = Annotated[
24+
Union[
25+
Annotated[NationalityCodeExtension, Tag("code")],
26+
Annotated[NationalityPeriodExtension, Tag("period")],
27+
],
28+
Discriminator("url"),
29+
]
30+
31+
32+
class NationalityExtension(Extension):
33+
"""The nationality of the patient.
34+
35+
CanonicalURL: http://hl7.org/fhir/StructureDefinition/patient-nationality
36+
"""
37+
url: Literal["http://hl7.org/fhir/StructureDefinition/patient-nationality"] = Field(
38+
"http://hl7.org/fhir/StructureDefinition/patient-nationality",
39+
alias="url", serialization_alias="url",
40+
)
41+
extension: list[NationalitySubExtension] | None = Field(None, alias="extension", serialization_alias="extension")
42+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# WARNING: This file is autogenerated by @atomic-ehr/codegen.
2+
# GitHub: https://github.com/atomic-ehr/codegen
3+
# Any manual changes made to this file may be overwritten.
4+
5+
from __future__ import annotations
6+
from typing import Literal
7+
from pydantic import Field
8+
from fhir_types.hl7_fhir_r4_core.base import Extension
9+
10+
11+
class OwnPrefixExtension(Extension):
12+
"""The prefix portion (e.g. voorvoegsel) of the family name that is derived from the person's own surname, as distinguished from any portion that is derived from the surname of the person's partner or spouse.
13+
14+
CanonicalURL: http://hl7.org/fhir/StructureDefinition/humanname-own-prefix
15+
"""
16+
url: Literal["http://hl7.org/fhir/StructureDefinition/humanname-own-prefix"] = Field(
17+
"http://hl7.org/fhir/StructureDefinition/humanname-own-prefix",
18+
alias="url", serialization_alias="url",
19+
)
20+
value_string: str = Field(alias="valueString", serialization_alias="valueString")
21+

examples/python-extension-example/generate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const builder = new APIBuilder({ logger })
1313
.python({
1414
allowExtraFields: false,
1515
primitiveTypeExtension: true,
16+
generateProfile: true,
1617
fhirpyClient: false,
1718
fieldFormat: "snake_case",
1819
})
@@ -27,6 +28,11 @@ const builder = new APIBuilder({ logger })
2728
"http://hl7.org/fhir/StructureDefinition/Patient": {},
2829
"http://hl7.org/fhir/StructureDefinition/Observation": {},
2930
"http://hl7.org/fhir/StructureDefinition/bodyweight": {},
31+
// Extensions
32+
"http://hl7.org/fhir/StructureDefinition/patient-birthPlace": {},
33+
"http://hl7.org/fhir/StructureDefinition/patient-nationality": {},
34+
"http://hl7.org/fhir/StructureDefinition/humanname-own-prefix": {},
35+
"http://hl7.org/fhir/StructureDefinition/patient-birthTime": {},
3036
},
3137
},
3238
})
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""
2+
FHIR R4 Extension Profile Tests
3+
4+
Tests generated extension profile classes (Pydantic subclasses of Extension).
5+
"""
6+
7+
import json
8+
9+
import pytest
10+
from pydantic import ValidationError
11+
12+
from fhir_types.hl7_fhir_r4_core import Address, CodeableConcept, Coding, Element, HumanName, Period
13+
from fhir_types.hl7_fhir_r4_core.base import Extension
14+
from fhir_types.hl7_fhir_r4_core.patient import Patient
15+
from fhir_types.hl7_fhir_r4_core.profiles.extension_birth_place import BirthPlaceExtension
16+
from fhir_types.hl7_fhir_r4_core.profiles.extension_birth_time import BirthTimeExtension
17+
from fhir_types.hl7_fhir_r4_core.profiles.extension_nationality import (
18+
NationalityCodeExtension,
19+
NationalityExtension,
20+
NationalityPeriodExtension,
21+
)
22+
from fhir_types.hl7_fhir_r4_core.profiles.extension_own_prefix import OwnPrefixExtension
23+
24+
25+
# ---------------------------------------------------------------------------
26+
# Simple extensions: construction, url enforcement, required value, subclass
27+
# ---------------------------------------------------------------------------
28+
29+
30+
@pytest.mark.parametrize(
31+
("cls", "kwargs", "expected_url"),
32+
[
33+
(BirthPlaceExtension, {"value_address": Address(city="Bonn")}, "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"),
34+
(BirthTimeExtension, {"value_date_time": "1990-03-15T08:22:00-05:00"}, "http://hl7.org/fhir/StructureDefinition/patient-birthTime"),
35+
(OwnPrefixExtension, {"value_string": "van"}, "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix"),
36+
],
37+
ids=["birth_place", "birth_time", "own_prefix"],
38+
)
39+
class TestSimpleExtension:
40+
def test_construction_and_url(self, cls, kwargs, expected_url) -> None:
41+
ext = cls(**kwargs)
42+
assert ext.url == expected_url
43+
assert isinstance(ext, Extension)
44+
45+
def test_rejects_wrong_url(self, cls, kwargs, expected_url) -> None:
46+
with pytest.raises(ValidationError):
47+
cls(url="http://wrong", **kwargs)
48+
49+
def test_value_is_required(self, cls, kwargs, expected_url) -> None:
50+
with pytest.raises(ValidationError):
51+
cls()
52+
53+
def test_round_trip(self, cls, kwargs, expected_url) -> None:
54+
original = cls(**kwargs)
55+
restored = cls.model_validate_json(original.model_dump_json(by_alias=True, exclude_none=True))
56+
assert restored.url == expected_url
57+
58+
59+
# ---------------------------------------------------------------------------
60+
# Complex extension: NationalityExtension with discriminated sub-extensions
61+
# ---------------------------------------------------------------------------
62+
63+
64+
class TestNationalityExtension:
65+
def test_construction_no_sub_extensions(self) -> None:
66+
ext = NationalityExtension()
67+
assert ext.url == "http://hl7.org/fhir/StructureDefinition/patient-nationality"
68+
assert ext.extension is None
69+
70+
def test_sub_extensions_construction_and_url(self) -> None:
71+
code_ext = NationalityCodeExtension(
72+
value_codeable_concept=CodeableConcept(
73+
coding=[Coding(system="urn:iso:std:iso:3166", code="DE")],
74+
),
75+
)
76+
period_ext = NationalityPeriodExtension(value_period=Period(start="1770-12-17"))
77+
assert code_ext.url == "code"
78+
assert period_ext.url == "period"
79+
assert isinstance(code_ext, Extension)
80+
81+
def test_with_both_sub_extensions(self) -> None:
82+
ext = NationalityExtension(extension=[
83+
NationalityCodeExtension(
84+
value_codeable_concept=CodeableConcept(
85+
coding=[Coding(system="urn:iso:std:iso:3166", code="DE")],
86+
),
87+
),
88+
NationalityPeriodExtension(value_period=Period(start="1770-12-17")),
89+
])
90+
assert len(ext.extension) == 2
91+
assert isinstance(ext.extension[0], NationalityCodeExtension)
92+
assert isinstance(ext.extension[1], NationalityPeriodExtension)
93+
94+
def test_sub_extension_rejects_wrong_url(self) -> None:
95+
with pytest.raises(ValidationError):
96+
NationalityCodeExtension(url="wrong", value_codeable_concept=CodeableConcept())
97+
98+
def test_sub_extension_value_is_required(self) -> None:
99+
with pytest.raises(ValidationError):
100+
NationalityCodeExtension()
101+
102+
def test_round_trip(self) -> None:
103+
original = NationalityExtension(extension=[
104+
NationalityCodeExtension(
105+
value_codeable_concept=CodeableConcept(
106+
coding=[Coding(system="urn:iso:std:iso:3166", code="DE")],
107+
),
108+
),
109+
NationalityPeriodExtension(value_period=Period(start="1770-12-17", end="1827-03-26")),
110+
])
111+
json_str = original.model_dump_json(by_alias=True, exclude_none=True)
112+
restored = NationalityExtension.model_validate_json(json_str)
113+
assert len(restored.extension) == 2
114+
assert isinstance(restored.extension[0], NationalityCodeExtension)
115+
assert restored.extension[0].value_codeable_concept.coding[0].code == "DE"
116+
assert isinstance(restored.extension[1], NationalityPeriodExtension)
117+
assert restored.extension[1].value_period.start == "1770-12-17"
118+
119+
def test_deserialization_from_fhir_json(self) -> None:
120+
"""Discriminated union routes sub-extensions by url during deserialization."""
121+
raw = json.dumps({
122+
"url": "http://hl7.org/fhir/StructureDefinition/patient-nationality",
123+
"extension": [
124+
{"url": "code", "valueCodeableConcept": {"coding": [{"system": "urn:iso:std:iso:3166", "code": "FR"}]}},
125+
{"url": "period", "valuePeriod": {"start": "1990-01-01"}},
126+
],
127+
})
128+
ext = NationalityExtension.model_validate_json(raw)
129+
assert isinstance(ext.extension[0], NationalityCodeExtension)
130+
assert ext.extension[0].value_codeable_concept.coding[0].code == "FR"
131+
assert isinstance(ext.extension[1], NationalityPeriodExtension)
132+
133+
134+
# ---------------------------------------------------------------------------
135+
# Serialization: FHIR-aliased JSON keys
136+
# ---------------------------------------------------------------------------
137+
138+
139+
def test_simple_extension_serializes_with_fhir_aliases() -> None:
140+
ext = OwnPrefixExtension(value_string="van")
141+
raw = json.loads(ext.model_dump_json(by_alias=True, exclude_none=True))
142+
assert raw == {
143+
"url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix",
144+
"valueString": "van",
145+
}
146+
147+
148+
def test_complex_extension_serializes_with_fhir_aliases() -> None:
149+
ext = NationalityExtension(extension=[
150+
NationalityCodeExtension(
151+
value_codeable_concept=CodeableConcept(
152+
coding=[Coding(system="urn:iso:std:iso:3166", code="DE")],
153+
),
154+
),
155+
NationalityPeriodExtension(value_period=Period(start="2000-01-01")),
156+
])
157+
data = json.loads(ext.model_dump_json(by_alias=True, exclude_none=True))
158+
assert data["extension"][0]["url"] == "code"
159+
assert "valueCodeableConcept" in data["extension"][0]
160+
assert "value_codeable_concept" not in data["extension"][0]
161+
assert data["extension"][1]["valuePeriod"]["start"] == "2000-01-01"
162+
163+
164+
# ---------------------------------------------------------------------------
165+
# Patient integration: all extension placement types + full round-trip
166+
# ---------------------------------------------------------------------------
167+
168+
169+
def test_patient_with_all_extension_types() -> None:
170+
"""Resource-level, element-level, and primitive-level extensions on one Patient."""
171+
patient = Patient(
172+
resource_type="Patient",
173+
birth_date="1770-12-17",
174+
birth_date_extension=Element(
175+
extension=[BirthTimeExtension(value_date_time="1770-12-17T12:00:00+01:00")],
176+
),
177+
extension=[
178+
BirthPlaceExtension(value_address=Address(city="Bonn", country="DE")),
179+
NationalityExtension(extension=[
180+
NationalityCodeExtension(
181+
value_codeable_concept=CodeableConcept(
182+
coding=[Coding(system="urn:iso:std:iso:3166", code="DE")],
183+
),
184+
),
185+
NationalityPeriodExtension(value_period=Period(start="1770-12-17")),
186+
]),
187+
],
188+
name=[
189+
HumanName(
190+
family="van Beethoven",
191+
family_extension=Element(extension=[OwnPrefixExtension(value_string="van")]),
192+
given=["Ludwig"],
193+
),
194+
],
195+
)
196+
197+
# Resource-level
198+
assert patient.extension[0].url == "http://hl7.org/fhir/StructureDefinition/patient-birthPlace"
199+
assert patient.extension[1].url == "http://hl7.org/fhir/StructureDefinition/patient-nationality"
200+
# Primitive-level
201+
assert patient.birth_date_extension.extension[0].url == "http://hl7.org/fhir/StructureDefinition/patient-birthTime"
202+
# Element-level
203+
assert patient.name[0].family_extension.extension[0].url == "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix"
204+
205+
# Round-trip through FHIR JSON
206+
json_str = patient.model_dump_json(by_alias=True, exclude_none=True)
207+
raw = json.loads(json_str)
208+
209+
assert raw["_birthDate"]["extension"][0]["url"] == "http://hl7.org/fhir/StructureDefinition/patient-birthTime"
210+
assert raw["_birthDate"]["extension"][0]["valueDateTime"] == "1770-12-17T12:00:00+01:00"
211+
assert raw["extension"][0]["valueAddress"]["city"] == "Bonn"
212+
assert raw["name"][0]["_family"]["extension"][0]["valueString"] == "van"
213+
214+
restored = Patient.model_validate_json(json_str)
215+
assert restored.birth_date == "1770-12-17"
216+
assert len(restored.extension) == 2
217+
assert restored.name[0].family == "van Beethoven"

src/api/builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
type TgzPackageConfig,
1616
} from "@atomic-ehr/fhir-canonical-manager";
1717
import { CSharp, type CSharpGeneratorOptions } from "@root/api/writer-generator/csharp/csharp";
18-
import { Python, type PythonGeneratorOptions } from "@root/api/writer-generator/python";
18+
import { Python, type PythonGeneratorOptions } from "@root/api/writer-generator/python/writer";
1919
import { generateTypeSchemas } from "@root/typeschema";
2020
import { promoteLogical } from "@root/typeschema/ir/logic-promotion";
2121
import { treeShake } from "@root/typeschema/ir/tree-shake";

0 commit comments

Comments
 (0)