From af50e568081f2cc1d082e4171309c531552e9a49 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 26 Jun 2026 14:53:34 +0200 Subject: [PATCH 1/2] docs(atomic-ehr-codegen-python-us-core-profiles): correct codegen option name to client: "fhirpy" The README and post.py docstring referenced `fhirpyClient: true`, but the current @atomic-ehr/codegen API (and this example`s generate.ts) use `client: "fhirpy"`. Update the three references to match. --- .../atomic-ehr-codegen-python-us-core-profiles/README.md | 4 ++-- .../atomic-ehr-codegen-python-us-core-profiles/post.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/README.md b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/README.md index 78189767..eb00ada5 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/README.md +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/README.md @@ -1,6 +1,6 @@ # US Core Profiles in Python with @atomic-ehr/codegen -A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) profile class generation for US Core. The Python counterpart of [atomic-ehr-codegen-typescript-us-core-profiles](../atomic-ehr-codegen-typescript-us-core-profiles), generated for **Pydantic** with the **[fhirpy](https://github.com/beda-software/fhirpy)** async client enabled (`fhirpyClient: true`). +A small CSV-to-FHIR converter demonstrating [`@atomic-ehr/codegen`](https://github.com/atomic-ehr/codegen) profile class generation for US Core. The Python counterpart of [atomic-ehr-codegen-typescript-us-core-profiles](../atomic-ehr-codegen-typescript-us-core-profiles), generated for **Pydantic** with the **[fhirpy](https://github.com/beda-software/fhirpy)** async client enabled (`client: "fhirpy"`). The example: @@ -73,7 +73,7 @@ python post.py ## Notes on the Code - **The generator is a Node tool; the output is Python.** `generate.ts` runs once to emit `fhir_types/`. After that you only need Python + Pydantic (and fhirpy for `post.py`). -- **`fhirpyClient: true`** makes the generated resources extend `FhirpyBaseModel`: they expose `resourceType` at class level and serialize via `model_dump`, which is everything fhirpy's typed client needs to `create` / `search` / `fetch` them. +- **`client: "fhirpy"`** makes the generated resources extend `FhirpyBaseModel`: they expose `resourceType` at class level and serialize via `model_dump`, which is everything fhirpy's typed client needs to `create` / `search` / `fetch` them. - **camelCase attributes (`fieldFormat: "camelCase"`).** Resource fields use the FHIR wire names (`resourceType`, `birthDate`, `effectiveDateTime`), so attribute names match the JSON. This is what lets `client.resources(Observation).search(...).fetch()` be statically typed: fhirpy's `ResourceProtocol` looks for a `resourceType` attribute, which only exists as a real attribute under camelCase. (The generator also supports `snake_case`, but then `resourceType` is injected only at runtime and fhirpy's typed client can't see it statically.) Profile **method** names stay snake_case (`set_systolic`, `set_race`, `from_resource`). - **Must-support base fields** (`gender`, `birthDate`) aren't profiled further by US Core, so the profile class emits no `.set_gender()`-style setters. `load.py` sets them on the base `Patient`, then calls `UscorePatientProfile.apply()`. `validate()` warns if a must-support field is missing. - **No `is()` type guard.** Unlike the TypeScript API, the Python classes don't ship a `.filter()`-style guard. `avg.py` selects BP observations by `resourceType` + `meta.profile`, then calls `from_resource()`. diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/post.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/post.py index 1e833a8a..657e4757 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/post.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/post.py @@ -9,7 +9,7 @@ FHIR_URL=http://localhost:8090/fhir python post.py -fhirpy is wired in because the types were generated with `fhirpyClient: true`: +fhirpy is wired in because the types were generated with `client: "fhirpy"`: the generated resources extend FhirpyBaseModel, expose `resourceType` at class level, and serialize via `model_dump`, which is everything fhirpy's typed client needs to create/search/deserialize them. From 45e86c4ee231b01201c9777b2fd2da02b121ebe0 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 26 Jun 2026 14:58:34 +0200 Subject: [PATCH 2/2] chore(atomic-ehr-codegen-python-us-core-profiles): regenerate fhir_types with @atomic-ehr/codegen canary Regenerated with @atomic-ehr/codegen@0.0.16-canary.20260626094300.e87ff88. Canary changes: concrete resources type resourceType as `str` (pattern still fixes the value) instead of `Literal[...]`, abstract base types (Resource, DomainResource) no longer carry a resourceType default, and FhirpyBaseModel guards against PydanticUndefined so it does not register a class-level resourceType for those abstract types. Verified: mypy clean (35 files), load.py / avg.py run, and post.py round-trips a transaction against a local Aidbox with typed read-back. --- .../fhir_types/fhirpy_base_model.py | 6 +++++- .../fhir_types/hl7_fhir_r4_core/bundle.py | 2 +- .../fhir_types/hl7_fhir_r4_core/domain_resource.py | 1 - .../fhir_types/hl7_fhir_r4_core/observation.py | 2 +- .../fhir_types/hl7_fhir_r4_core/operation_outcome.py | 2 +- .../fhir_types/hl7_fhir_r4_core/organization.py | 2 +- .../fhir_types/hl7_fhir_r4_core/patient.py | 2 +- .../fhir_types/hl7_fhir_r4_core/resource.py | 1 - .../atomic-ehr-codegen-python-us-core-profiles/package.json | 2 +- 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/fhirpy_base_model.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/fhirpy_base_model.py index 4c41c0bd..862422df 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/fhirpy_base_model.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/fhirpy_base_model.py @@ -1,5 +1,6 @@ from typing import Any, Union, Optional, Iterator, Tuple, Dict from pydantic import BaseModel, Field +from pydantic_core import PydanticUndefined from typing import Protocol @@ -21,7 +22,10 @@ class FhirpyBaseModel(BaseModel): def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: super().__pydantic_init_subclass__(**kwargs) field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType") - if field is not None and field.default is not None: + # Only concrete resources carry a default resourceType. Abstract/family base types + # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a + # class attribute that concrete subclasses would shadow. + if field is not None and field.default is not None and field.default is not PydanticUndefined: type.__setattr__(cls, "resourceType", str(field.default)) def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override] diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/bundle.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/bundle.py index fcf8207f..57a83652 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/bundle.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/bundle.py @@ -71,7 +71,7 @@ class BundleLink(BackboneElement): class Bundle(Resource, Generic[T1, T2]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - resourceType: Literal['Bundle'] = Field( + resourceType: str = Field( default='Bundle', alias='resourceType', serialization_alias='resourceType', diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/domain_resource.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/domain_resource.py index 4dc0e5f9..c93c8bce 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/domain_resource.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/domain_resource.py @@ -18,7 +18,6 @@ class DomainResource(Resource, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='DomainResource', alias='resourceType', serialization_alias='resourceType', pattern='DomainResource' diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/observation.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/observation.py index 65eb1697..2807d46f 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/observation.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/observation.py @@ -46,7 +46,7 @@ class ObservationReferenceRange(BackboneElement): class Observation(DomainResource): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - resourceType: Literal['Observation'] = Field( + resourceType: str = Field( default='Observation', alias='resourceType', serialization_alias='resourceType', diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/operation_outcome.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/operation_outcome.py index 76db80c3..d8c72001 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/operation_outcome.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/operation_outcome.py @@ -24,7 +24,7 @@ class OperationOutcomeIssue(BackboneElement): class OperationOutcome(DomainResource): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - resourceType: Literal['OperationOutcome'] = Field( + resourceType: str = Field( default='OperationOutcome', alias='resourceType', serialization_alias='resourceType', diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/organization.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/organization.py index 54f9dd94..cebea2a4 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/organization.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/organization.py @@ -25,7 +25,7 @@ class OrganizationContact(BackboneElement): class Organization(DomainResource): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - resourceType: Literal['Organization'] = Field( + resourceType: str = Field( default='Organization', alias='resourceType', serialization_alias='resourceType', diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/patient.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/patient.py index fb252a70..d9691092 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/patient.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/patient.py @@ -38,7 +38,7 @@ class PatientLink(BackboneElement): class Patient(DomainResource): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") - resourceType: Literal['Patient'] = Field( + resourceType: str = Field( default='Patient', alias='resourceType', serialization_alias='resourceType', diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/resource.py b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/resource.py index 3fd02b76..25d887cd 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/resource.py +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/fhir_types/hl7_fhir_r4_core/resource.py @@ -15,7 +15,6 @@ class Resource(FhirpyBaseModel): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='Resource', alias='resourceType', serialization_alias='resourceType', pattern='Resource' diff --git a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/package.json b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/package.json index b69f2e02..e3611e08 100644 --- a/developer-experience/atomic-ehr-codegen-python-us-core-profiles/package.json +++ b/developer-experience/atomic-ehr-codegen-python-us-core-profiles/package.json @@ -8,7 +8,7 @@ "generate": "tsx generate.ts" }, "devDependencies": { - "@atomic-ehr/codegen": "^0.0.16", + "@atomic-ehr/codegen": "^0.0.16-canary.20260626094300.e87ff88", "@types/node": "^25.6.0", "tsx": "^4.21.0", "typescript": "^5.9.3"