Skip to content

Commit 1b03613

Browse files
py: minor fixes
1 parent f9bee32 commit 1b03613

6 files changed

Lines changed: 123 additions & 207 deletions

File tree

examples/python/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,4 @@ def search(
103103
url = f"{self.base_url}/{resource_type}"
104104
response = self.session.get(url, params=params)
105105
response.raise_for_status()
106-
return Bundle[resource_class].model_validate(response.json()) # type: ignore[valid-type]
106+
return Bundle.model_validate(response.json())

examples/python/test_bundle.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
14
from fhir_types.hl7_fhir_r4_core.base import CodeableConcept
25
from fhir_types.hl7_fhir_r4_core.bundle import Bundle, BundleEntry
36
from fhir_types.hl7_fhir_r4_core.observation import Observation
@@ -37,4 +40,20 @@ def test_bundle_without_type_param_is_backwards_compatible() -> None:
3740
type="collection",
3841
entry=[BundleEntry(resource=patient)],
3942
)
40-
assert len(bundle.entry) == 1
43+
assert len(bundle.entry) == 1
44+
45+
46+
def test_bundle_from_json_raises_on_invalid_resource() -> None:
47+
# Observation requires `status` and `code` — omitting them causes a runtime ValidationError
48+
bundle_json = """{
49+
"resourceType": "Bundle",
50+
"type": "searchset",
51+
"entry": [{
52+
"resource": {
53+
"resourceType": "Observation",
54+
"id": "obs-1"
55+
}
56+
}]
57+
}"""
58+
with pytest.raises(ValidationError):
59+
Bundle.from_json(bundle_json)

examples/python/test_sdk.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,7 @@ def test_bundle_from_json() -> None:
237237
resource = bundle.entry[0].resource
238238
assert resource is not None
239239
assert resource.id == "p-1"
240-
from fhir_types.hl7_fhir_r4_core.patient import Patient as PatientClass
241-
assert type(resource) is PatientClass
240+
assert type(resource) is Patient
242241

243242

244243
def test_to_json_shape() -> None:

src/api/writer-generator/python.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -732,10 +732,6 @@ export class Python extends Writer<PythonGeneratorOptions> {
732732

733733
for (const dep of resourceDeps) {
734734
this.pyImportType(dep);
735-
736-
// const familyName = `${pascalCase(dep.name)}Family`;
737-
// const familyPackage = `${this.pyFhirPackage(dep)}.resource_families`;
738-
// this.pyImportFrom(familyPackage, familyName);
739735
}
740736
}
741737

Lines changed: 45 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,5 @@
11
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
22

3-
exports[`Python Writer Generator generates Patient resource in inMemoryOnly mode with snapshot 1`] = `
4-
"# WARNING: This file is autogenerated by @atomic-ehr/codegen.
5-
# GitHub: https://github.com/atomic-ehr/codegen
6-
# Any manual changes made to this file may be overwritten.
7-
8-
from __future__ import annotations
9-
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
10-
from typing import Any, List as PyList, Literal
11-
from typing_extensions import Self
12-
13-
from fhir_types.hl7_fhir_r4_core.base import (\\
14-
Address, Attachment, BackboneElement, CodeableConcept, ContactPoint, HumanName, Identifier, Period, Reference
15-
)
16-
from fhir_types.hl7_fhir_r4_core.domain_resource import DomainResource
17-
18-
19-
class PatientCommunication(BackboneElement):
20-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
21-
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")
22-
preferred: bool | None = Field(None, alias="preferred", serialization_alias="preferred")
23-
24-
class PatientContact(BackboneElement):
25-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
26-
address: Address | None = Field(None, alias="address", serialization_alias="address")
27-
gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender")
28-
name: HumanName | None = Field(None, alias="name", serialization_alias="name")
29-
organization: Reference | None = Field(None, alias="organization", serialization_alias="organization")
30-
period: Period | None = Field(None, alias="period", serialization_alias="period")
31-
relationship: PyList[CodeableConcept] | None = Field(None, alias="relationship", serialization_alias="relationship")
32-
telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom")
33-
34-
class PatientLink(BackboneElement):
35-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
36-
other: Reference = Field(alias="other", serialization_alias="other")
37-
type: Literal["replaced-by", "replaces", "refer", "seealso"] = Field(alias="type", serialization_alias="type")
38-
39-
40-
class Patient(DomainResource):
41-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
42-
resource_type: Literal['Patient'] = Field(
43-
default='Patient',
44-
alias='resourceType',
45-
serialization_alias='resourceType',
46-
frozen=True,
47-
pattern='Patient'
48-
)
49-
active: bool | None = Field(None, alias="active", serialization_alias="active")
50-
address: PyList[Address] | None = Field(None, alias="address", serialization_alias="address")
51-
birth_date: str | None = Field(None, alias="birthDate", serialization_alias="birthDate")
52-
communication: PyList[PatientCommunication] | None = Field(None, alias="communication", serialization_alias="communication")
53-
contact: PyList[PatientContact] | None = Field(None, alias="contact", serialization_alias="contact")
54-
deceased_boolean: bool | None = Field(None, alias="deceasedBoolean", serialization_alias="deceasedBoolean")
55-
deceased_date_time: str | None = Field(None, alias="deceasedDateTime", serialization_alias="deceasedDateTime")
56-
gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender")
57-
general_practitioner: PyList[Reference] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner")
58-
identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier")
59-
link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link")
60-
managing_organization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization")
61-
marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus")
62-
multiple_birth_boolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean")
63-
multiple_birth_integer: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger")
64-
name: PyList[HumanName] | None = Field(None, alias="name", serialization_alias="name")
65-
photo: PyList[Attachment] | None = Field(None, alias="photo", serialization_alias="photo")
66-
telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom")
67-
68-
def model_post_init(self, __context: Any) -> None:
69-
self.__pydantic_fields_set__.add("resource_type")
70-
71-
def to_json(self, indent: int | None = None) -> str:
72-
return self.model_dump_json(exclude_unset=True, exclude_none=True, indent=indent)
73-
74-
@classmethod
75-
def from_json(cls, json: str) -> Self:
76-
return cls.model_validate_json(json)
77-
78-
"
79-
`;
80-
813
exports[`Python Writer Generator static files 1`] = `
824
"fhirpy>=2.0.0,<3.0.0
835
mypy>=1.9.0,<2.0.0
@@ -89,7 +11,7 @@ types-requests>=2.32.0,<3.0.0
8911
"
9012
`;
9113

92-
exports[`Python Writer Generator generates BundleEntry with generic type-family parameters 1`] = `
14+
exports[`Python Writer Generator bundle.py matches snapshot 1`] = `
9315
"# WARNING: This file is autogenerated by @atomic-ehr/codegen.
9416
# GitHub: https://github.com/atomic-ehr/codegen
9517
# Any manual changes made to this file may be overwritten.
@@ -196,92 +118,70 @@ class Bundle(Resource, Generic[T1, T2]):
196118
"
197119
`;
198120

199-
exports[`Python Writer Generator generates Bundle with inherited generic params from BundleEntry 1`] = `
121+
exports[`Python Writer Generator patient.py matches snapshot 1`] = `
200122
"# WARNING: This file is autogenerated by @atomic-ehr/codegen.
201123
# GitHub: https://github.com/atomic-ehr/codegen
202124
# Any manual changes made to this file may be overwritten.
203125
204126
from __future__ import annotations
205-
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, model_validator
206-
from typing import Any, Generic, List as PyList, Literal
207-
from typing_extensions import Self, TypeVar
208-
209-
from fhir_types.hl7_fhir_r4_core.base import BackboneElement, Identifier, Signature
210-
from fhir_types.hl7_fhir_r4_core.resource import Resource
211-
from fhir_types.hl7_fhir_r4_core.resource_preprocessor import preprocess_resource_fields
212-
213-
T = TypeVar('T', bound=Resource, default=Resource)
214-
T1 = TypeVar('T1', bound=Resource, default=Resource)
215-
T2 = TypeVar('T2', bound=Resource, default=Resource)
216-
217-
218-
class BundleEntry(BackboneElement, Generic[T1, T2]):
219-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
220-
full_url: str | None = Field(None, alias="fullUrl", serialization_alias="fullUrl")
221-
link: PyList[BundleLink] | None = Field(None, alias="link", serialization_alias="link")
222-
request: BundleEntryRequest | None = Field(None, alias="request", serialization_alias="request")
223-
resource: T1 | None = Field(None, alias="resource", serialization_alias="resource")
224-
response: BundleEntryResponse[T2] | None = Field(None, alias="response", serialization_alias="response")
225-
search: BundleEntrySearch | None = Field(None, alias="search", serialization_alias="search")
127+
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
128+
from typing import Any, List as PyList, Literal
129+
from typing_extensions import Self
226130
227-
@model_validator(mode='before')
228-
@classmethod
229-
def _preprocess_resources(cls, data: Any) -> Any:
230-
if isinstance(data, dict):
231-
return preprocess_resource_fields(data, "fhir_types.hl7_fhir_r4_core")
232-
return data
131+
from fhir_types.hl7_fhir_r4_core.base import (\\
132+
Address, Attachment, BackboneElement, CodeableConcept, ContactPoint, HumanName, Identifier, Period, Reference
133+
)
134+
from fhir_types.hl7_fhir_r4_core.domain_resource import DomainResource
233135
234-
class BundleEntryRequest(BackboneElement):
235-
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
236-
if_match: str | None = Field(None, alias="ifMatch", serialization_alias="ifMatch")
237-
if_modified_since: str | None = Field(None, alias="ifModifiedSince", serialization_alias="ifModifiedSince")
238-
if_none_exist: str | None = Field(None, alias="ifNoneExist", serialization_alias="ifNoneExist")
239-
if_none_match: str | None = Field(None, alias="ifNoneMatch", serialization_alias="ifNoneMatch")
240-
method: Literal["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"] = Field(alias="method", serialization_alias="method")
241-
url: str = Field(alias="url", serialization_alias="url")
242136
243-
class BundleEntryResponse(BackboneElement, Generic[T]):
137+
class PatientCommunication(BackboneElement):
244138
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
245-
etag: str | None = Field(None, alias="etag", serialization_alias="etag")
246-
last_modified: str | None = Field(None, alias="lastModified", serialization_alias="lastModified")
247-
location: str | None = Field(None, alias="location", serialization_alias="location")
248-
outcome: T | None = Field(None, alias="outcome", serialization_alias="outcome")
249-
status: str = Field(alias="status", serialization_alias="status")
250-
251-
@model_validator(mode='before')
252-
@classmethod
253-
def _preprocess_resources(cls, data: Any) -> Any:
254-
if isinstance(data, dict):
255-
return preprocess_resource_fields(data, "fhir_types.hl7_fhir_r4_core")
256-
return data
139+
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")
140+
preferred: bool | None = Field(None, alias="preferred", serialization_alias="preferred")
257141
258-
class BundleEntrySearch(BackboneElement):
142+
class PatientContact(BackboneElement):
259143
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
260-
mode: Literal["match", "include", "outcome"] | None = Field(None, alias="mode", serialization_alias="mode")
261-
score: float | None = Field(None, alias="score", serialization_alias="score")
144+
address: Address | None = Field(None, alias="address", serialization_alias="address")
145+
gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender")
146+
name: HumanName | None = Field(None, alias="name", serialization_alias="name")
147+
organization: Reference | None = Field(None, alias="organization", serialization_alias="organization")
148+
period: Period | None = Field(None, alias="period", serialization_alias="period")
149+
relationship: PyList[CodeableConcept] | None = Field(None, alias="relationship", serialization_alias="relationship")
150+
telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom")
262151
263-
class BundleLink(BackboneElement):
152+
class PatientLink(BackboneElement):
264153
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
265-
relation: str = Field(alias="relation", serialization_alias="relation")
266-
url: str = Field(alias="url", serialization_alias="url")
154+
other: Reference = Field(alias="other", serialization_alias="other")
155+
type: Literal["replaced-by", "replaces", "refer", "seealso"] = Field(alias="type", serialization_alias="type")
267156
268157
269-
class Bundle(Resource, Generic[T1, T2]):
158+
class Patient(DomainResource):
270159
model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid")
271-
resource_type: Literal['Bundle'] = Field(
272-
default='Bundle',
160+
resource_type: Literal['Patient'] = Field(
161+
default='Patient',
273162
alias='resourceType',
274163
serialization_alias='resourceType',
275164
frozen=True,
276-
pattern='Bundle'
165+
pattern='Patient'
277166
)
278-
entry: PyList[BundleEntry[T1, T2]] | None = Field(None, alias="entry", serialization_alias="entry")
279-
identifier: Identifier | None = Field(None, alias="identifier", serialization_alias="identifier")
280-
link: PyList[BundleLink] | None = Field(None, alias="link", serialization_alias="link")
281-
signature: Signature | None = Field(None, alias="signature", serialization_alias="signature")
282-
timestamp: str | None = Field(None, alias="timestamp", serialization_alias="timestamp")
283-
total: int | None = Field(None, alias="total", serialization_alias="total")
284-
type: Literal["document", "message", "transaction", "transaction-response", "batch", "batch-response", "history", "searchset", "collection"] = Field(alias="type", serialization_alias="type")
167+
active: bool | None = Field(None, alias="active", serialization_alias="active")
168+
address: PyList[Address] | None = Field(None, alias="address", serialization_alias="address")
169+
birth_date: str | None = Field(None, alias="birthDate", serialization_alias="birthDate")
170+
communication: PyList[PatientCommunication] | None = Field(None, alias="communication", serialization_alias="communication")
171+
contact: PyList[PatientContact] | None = Field(None, alias="contact", serialization_alias="contact")
172+
deceased_boolean: bool | None = Field(None, alias="deceasedBoolean", serialization_alias="deceasedBoolean")
173+
deceased_date_time: str | None = Field(None, alias="deceasedDateTime", serialization_alias="deceasedDateTime")
174+
gender: Literal["male", "female", "other", "unknown"] | None = Field(None, alias="gender", serialization_alias="gender")
175+
general_practitioner: PyList[Reference] | None = Field(None, alias="generalPractitioner", serialization_alias="generalPractitioner")
176+
identifier: PyList[Identifier] | None = Field(None, alias="identifier", serialization_alias="identifier")
177+
link: PyList[PatientLink] | None = Field(None, alias="link", serialization_alias="link")
178+
managing_organization: Reference | None = Field(None, alias="managingOrganization", serialization_alias="managingOrganization")
179+
marital_status: CodeableConcept[Literal["A", "D", "I", "L", "M", "P", "S", "T", "U", "W", "UNK"] | str] | None = Field(None, alias="maritalStatus", serialization_alias="maritalStatus")
180+
multiple_birth_boolean: bool | None = Field(None, alias="multipleBirthBoolean", serialization_alias="multipleBirthBoolean")
181+
multiple_birth_integer: int | None = Field(None, alias="multipleBirthInteger", serialization_alias="multipleBirthInteger")
182+
name: PyList[HumanName] | None = Field(None, alias="name", serialization_alias="name")
183+
photo: PyList[Attachment] | None = Field(None, alias="photo", serialization_alias="photo")
184+
telecom: PyList[ContactPoint] | None = Field(None, alias="telecom", serialization_alias="telecom")
285185
286186
def model_post_init(self, __context: Any) -> None:
287187
self.__pydantic_fields_set__.add("resource_type")
@@ -293,12 +193,5 @@ class Bundle(Resource, Generic[T1, T2]):
293193
def from_json(cls, json: str) -> Self:
294194
return cls.model_validate_json(json)
295195
296-
@model_validator(mode='before')
297-
@classmethod
298-
def _preprocess_resources(cls, data: Any) -> Any:
299-
if isinstance(data, dict):
300-
return preprocess_resource_fields(data, "fhir_types.hl7_fhir_r4_core")
301-
return data
302-
303196
"
304197
`;

0 commit comments

Comments
 (0)