Skip to content

FHIR IG generation (dhis2w_fhir)

dhis2w_fhir is the package behind d2w fhir: the fhir.toml document, the emitters that turn DHIS2 metadata into an Implementation Guide - FSH for the definitional artifacts, pre-built R4 JSON for the organisation-unit registry, the option-set terminology, and the category terminology - and the DHIS2 period grammar they share. It mounts onto the CLI through the dhis2w.plugins.v1 entry point, and every component symbol re-exports from the top-level package, so from dhis2w_fhir import GenerateConfig, parse_period keeps working however the components are arranged internally. The R4 resource models are the one exception: they live in dhis2w_fhir.r4, which is FHIR's own vocabulary rather than part of the plugin surface.

When to reach for it

  • Read or write a project's fhir.toml from Python (load_project, load_fhir_config, find_project_fhir_config, write_fhir_config).
  • Parse a DHIS2 ISO period into its type and date range, or walk backwards from a date (parse_period, recent_periods, PERIOD_TYPE_DEFINITIONS).
  • Carry DHIS2 attribute values onto a generated resource (AttributeValueIn, AttributeCodeIndex, resolve_attribute_code_index, attribute_value_extensions).
  • Build FSH artifacts without the CLI (build_foundation_artifacts, build_questionnaire_artifacts, build_page_artifacts) and sync them to disk (sync_artifacts).
  • Build the pre-built R4 documents - the registry (dhis2w_fhir.r4.Organization, dhis2w_fhir.r4.Location), the option-set terminology (build_option_set_artifacts), and the category terminology (build_category_artifacts), the last two on dhis2w_fhir.r4.CodeSystem and dhis2w_fhir.r4.ValueSet - and sync them to disk (sync_json_artifacts), one owned directory per source.
  • Map an option set's generated concept codes back to the DHIS2 option UID and option code (build_option_set_concept_maps for the dhis2w_fhir.r4.ConceptMap models, build_option_set_concept_map_artifacts for the JSON files that land in CONCEPT_MAP_DIRECTORY).
  • Generate one target, or the whole guide, without the command (generate_foundation, generate_option_sets, generate_categories, generate_questionnaires, generate_examples, generate_organisation_units, generate_pages, generate_full). Each returns a GenerateReport, and each raises BuildAbortingCodeError or BuildAbortingNameError rather than writing a guide the IG publisher will die on hours later.
  • Scaffold a project, or re-render one that already exists (init_project, refresh_project, read_project_scaffold_state, ProjectScaffoldState).
  • Produce a validation report rather than only rendering one (validate_codes, with resolve_validation_context, resolve_validation_scope, and resolve_code_source for the three things a run has to decide first). build_aborting_code and build_aborting_name are the two predicates the report's error grade and the generate refusal share, so a caller can ask the same question either command asks; display_code renders a DHIS2 code for human eyes the way the report does.
  • Ask the same question of the files a build publishes, with no instance behind it (check_publishable_artifacts, returning an ArtifactCheckReport of ArtifactFindings). It reads a project's compiled resources, pre-built JSON, and FSH sources through those same two predicates, which is what d2w fhir check-artifacts and make build run.
  • Run the conformance chain in process, or grade one phase of it on its own (run_doctor, DoctorOptions, DoctorReport, render_doctor_markdown, phase_evidence, resolve_doctor_profile, and the graders grade, grade_capture, grade_forward, grade_oracle over DoctorFinding, CaptureOutcome, and FamilyOutcome). Read run_doctor's own docstring first: it writes a workspace, shells out to a compiler, and posts a corpus, which the graders do not.
  • Assemble one person's International Patient Summary out of a projected subject and the doses somebody already read (build_patient_summary, AssembledSummary, RecordedDose, summary_caveat, REQUIRED_SECTIONS, IpsSection), and publish the section mapping behind it as a ConceptMap (build_section_concept_map, build_section_concept_map_artifacts). The assembly opens no connection and reads no store, so a command is as free to call it as a served facade is.
  • Record why a spooled response was refused, and read the record back (record_refusal, read_refusal_record, ForwardRefusalRecord, RefusalReason, SPOOL_RELATIVE_PATH). ForwardRefusalRecord is the declared type of SpooledReceipt.refusal, so a caller reading receipts holds instances of it either way.
  • Talk to a running facade from Python without composing a request (FacadeClient, with submit_response answering a CaptureReceipt, generate, read_response, read, search over a ResourceQuery, resolve for a canonical, capability, and evaluate answering an EvaluationOutcome). Credentials are BearerToken, UsernamePassword, or PersonalAccessToken; a refusal raises FacadeError carrying the OperationOutcome the facade stated its reason in.

Every capability above that reads DHIS2 takes the connection as an argument: client= on validate_codes, run_doctor, and each generate target, with the Profile form kept as the convenience wrapper the commands use. A handed-in client is used as it stands and left open, so an application already holding an authenticated connection makes one connection rather than one per call.

Worked example — parse a period, then walk backwards

import datetime

from dhis2w_fhir import parse_period, recent_periods

parse_period("2024BiW2")
# PeriodValue(iso='2024BiW2', period_type='BiWeekly',
#             start_date=date(2024, 1, 15), end_date=date(2024, 1, 28))

recent_periods("Monthly", 3, datetime.date(2026, 8, 2))
# ['202607', '202606', '202605']

Reference

Periods

period

DHIS2 reporting periods: the ISO period grammar, its period-type catalogue, the parser, and its inverse.

Classes

PeriodTypeDefinition

Bases: BaseModel

One DHIS2 period type as terminology: its name, its ISO format, and a display phrase.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/schemas.py
class PeriodTypeDefinition(BaseModel):
    """One DHIS2 period type as terminology: its name, its ISO format, and a display phrase."""

    model_config = ConfigDict(frozen=True)

    name: str
    iso_format: str
    display: str

PeriodValue

Bases: BaseModel

One DHIS2 reporting period: its ISO identifier, its period type, and the dates it covers.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/schemas.py
class PeriodValue(BaseModel):
    """One DHIS2 reporting period: its ISO identifier, its period type, and the dates it covers."""

    model_config = ConfigDict(frozen=True)

    iso: str
    period_type: str
    start_date: datetime.date
    end_date: datetime.date

Functions:

parse_period(iso)

Parse a DHIS2 ISO period string into its period type, start date, and end date.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/parser.py
def parse_period(iso: str) -> PeriodValue:
    """Parse a DHIS2 ISO period string into its period type, start date, and end date."""
    if len(iso) < _MINIMUM_LENGTH or len(iso) > _MAXIMUM_LENGTH:
        raise ValueError(f"not a DHIS2 ISO period: {iso!r} (expected 4 to 11 characters)")
    year = _digits(iso, 0, 4)
    parsed = _parse_by_length(iso, year)
    if parsed is None:
        raise ValueError(f"not a DHIS2 ISO period: {iso!r}")
    return parsed

recent_periods(period_type, count, today)

The count most recent completed ISO periods of period_type, newest first.

A period counts as completed when its end date falls strictly before today. An unregistered period type, or a non-positive count, yields an empty list rather than raising - the caller is discovering data, not validating configuration.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/recent.py
def recent_periods(period_type: str, count: int, today: datetime.date) -> list[str]:
    """The `count` most recent completed ISO periods of `period_type`, newest first.

    A period counts as completed when its end date falls strictly before `today`. An
    unregistered period type, or a non-positive count, yields an empty list rather than
    raising - the caller is discovering data, not validating configuration.
    """
    enumerator = _ISO_ENUMERATORS.get(period_type)
    if enumerator is None or count <= 0:
        return []
    completed: list[PeriodValue] = []
    for year in range(today.year + 1, today.year - count - _EXTRA_YEARS, -1):
        completed.extend(value for value in _parsed(enumerator(year)) if value.end_date < today)
    completed.sort(key=lambda value: (value.end_date, value.iso), reverse=True)
    return [value.iso for value in completed[:count]]

Project configuration

config

The fhir.toml document: its models plus discovery, load, and save.

A FHIR IG project is any directory holding a fhir.toml (scaffolded by d2w fhir init). Discovery walks up from the working directory, mirroring how .dhis2/profiles.toml is found.

The document composes the per-component selection tables, so this module depends on the components and never the other way round - an emitter receives its config as a parameter.

Classes

NoFhirProjectError

Bases: LookupError

Raised when no fhir.toml is found walking up from the working directory.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class NoFhirProjectError(LookupError):
    """Raised when no `fhir.toml` is found walking up from the working directory."""

UnknownFhirConfigKeyError

Bases: CliUserError

Raised when fhir.toml names keys the configuration document does not declare.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class UnknownFhirConfigKeyError(CliUserError):
    """Raised when `fhir.toml` names keys the configuration document does not declare."""

MalformedFhirConfigError

Bases: CliUserError

Raised when fhir.toml is not valid TOML, naming the file and where the parser stopped.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class MalformedFhirConfigError(CliUserError):
    """Raised when `fhir.toml` is not valid TOML, naming the file and where the parser stopped."""

IgConfig

Bases: BaseModel

SUSHI IG identity - the [ig] table of fhir.toml.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class IgConfig(BaseModel):
    """SUSHI IG identity - the `[ig]` table of `fhir.toml`."""

    model_config = ConfigDict(extra="forbid")

    id: str
    canonical: str
    name: str
    title: str
    publisher: str
    status: IgStatus = "draft"

    _normalize_canonical = field_validator("canonical")(strip_trailing_slash)

NamingConfig

Bases: BaseModel

The identity source and the FSH naming tokens - the [generate.naming] table of fhir.toml.

source picks the identity stem every artifact of an object derives from: the FHIR resource id, the canonical URL, the file name, and the FSH name all follow one resolved segment. "id" (the default) takes the DHIS2 id verbatim; "code-or-id" takes the object's code when it is usable as a stem and unique in the run, falling back to the id with a note; "code" requires such a code on every selected object and refuses the run otherwise.

Artifact names merge the prefix and kind tokens and underscore the rest (D2 + OS + _BirthType + _CS); ids join the kebab of each non-empty token (d2-os-birth-type-cs). prefix, option_set, category, attribute_option_combo, data_set, program, and program_stage may be empty to drop them; organisation_unit must stay non-empty or the org-unit artifact names would degenerate to bare _CS/_Level_CS. tracked_entity_type names the person-only registration form a tracked entity type publishes. attribute_option_combo names the vocabulary a data set's non-default category combo publishes, and it takes a token of its own rather than the COC the data dictionary uses: D2COC_CS is the disaggregation vocabulary a question's cells are coded from, while D2AOC_*_VS is the vocabulary a response's attribute option combo is drawn from, and the two are bound in different places. Future group / group-set artifacts follow the same scheme (OUG, OUGS).

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class NamingConfig(BaseModel):
    """The identity source and the FSH naming tokens - the `[generate.naming]` table of `fhir.toml`.

    `source` picks the identity stem every artifact of an object derives from: the FHIR
    resource id, the canonical URL, the file name, and the FSH name all follow one resolved
    segment. `"id"` (the default) takes the DHIS2 id verbatim; `"code-or-id"` takes the
    object's code when it is usable as a stem and unique in the run, falling back to the id
    with a note; `"code"` requires such a code on every selected object and refuses the run
    otherwise.

    Artifact names merge the prefix and kind tokens and underscore the rest
    (`D2` + `OS` + `_BirthType` + `_CS`); ids join the kebab of each non-empty token
    (`d2-os-birth-type-cs`). `prefix`, `option_set`, `category`, `attribute_option_combo`,
    `data_set`, `program`, and `program_stage` may be empty to drop them;
    `organisation_unit` must stay non-empty or the org-unit artifact names would degenerate
    to bare `_CS`/`_Level_CS`. `tracked_entity_type` names the person-only registration form a
    tracked entity type publishes. `attribute_option_combo` names the vocabulary a data
    set's non-default category combo publishes, and it takes a token of its own rather than the
    `COC` the data dictionary uses: `D2COC_CS` is the disaggregation vocabulary a
    question's cells are coded from, while `D2AOC_*_VS` is the vocabulary a response's
    attribute option combo is drawn from, and the two are bound in different places. Future
    group / group-set artifacts follow the same scheme (`OUG`, `OUGS`).
    """

    model_config = ConfigDict(extra="forbid")

    source: NamingSource = "id"
    prefix: str = "D2"
    option_set: str = "OS"
    category: str = "CAT"
    attribute_option_combo: str = "AOC"
    organisation_unit: str = "OU"
    data_set: str = "DS"
    program: str = "PR"
    program_stage: str = "PS"
    tracked_entity_type: str = "TET"

    @field_validator(
        "prefix",
        "option_set",
        "category",
        "attribute_option_combo",
        "data_set",
        "program",
        "program_stage",
        "tracked_entity_type",
    )
    @classmethod
    def _optional_token(cls, value: str) -> str:
        """Every token but organisation_unit may be empty, which drops it from the composed name."""
        return _validate_fsh_token(value, allow_empty=True)

    @field_validator("organisation_unit")
    @classmethod
    def _required_token(cls, value: str) -> str:
        """organisation_unit must be a non-empty FSH-name-safe token."""
        return _validate_fsh_token(value, allow_empty=False)

HostileNamePosture

Bases: StrEnum

What a generate run does with a DHIS2 name the IG publisher's own build cannot survive.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class HostileNamePosture(StrEnum):
    """What a generate run does with a DHIS2 name the IG publisher's own build cannot survive."""

    #: Publish every name byte-true and refuse the run when one of the gated names carries '<'. The
    #: name is then changed in DHIS2, or the selection narrowed, before a build is spent on it.
    REFUSE = "refuse"

    #: Publish the wording the rewrite produces - "5 to < 15 years" becomes "5 to under 15 years" -
    #: and note every name the guide states differently from the instance. DHIS2 is never modified,
    #: and every emitted identifier stays exactly as it is.
    SUBSTITUTE = "substitute"

GenerateConfig

Bases: BaseModel

Generation behaviour - the [generate] table of fhir.toml.

The four data-definition tables select the questionnaire form kinds: data_sets picks aggregate data sets, event_programs picks programs without registration, tracker_programs picks programs with registration (one Questionnaire per program stage plus the program's own registration form), and tracked_entity_forms picks the tracked entity types that publish a person-only registration form - a form that creates a person and enrols them in nothing. Empty, tracked_entity_forms publishes one form per type the selected tracker programs track; the other three publish everything of their kind the instance holds.

hostile_names is what the run does with a DHIS2 name carrying <, which the IG publisher writes into a page it strict-parses and then dies on. Unset, the run asks on a terminal and refuses without one, so a script is never left hanging on a question; "refuse" answers it with today's refusal and "substitute" publishes the rewritten wording. d2w fhir generate --substitute-hostile-names and --refuse-hostile-names answer it for one run.

timezone is the IANA zone the instance's zone-less timestamps are wall-clock readings in (BUGS.md #62). Naming it turns every emitted dateTime into the numeric offset that zone was on at that very instant, DST included; leaving it unset keeps the UTC reading.

tracked_entity_types maps a DHIS2 tracked entity type UID onto the FHIR resource type its registrations are about. A DHIS2 tracked entity is not always a person - a project tracks households, buildings, herds, and equipment as readily as patients - so the type says what it is and every form of every program tracking it follows. A type named here is not selected by it: selection stays with the three data-definition tables, and a type this table never mentions is a Patient, which is what keeps a person-tracking project's config empty.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class GenerateConfig(BaseModel):
    """Generation behaviour - the `[generate]` table of `fhir.toml`.

    The four data-definition tables select the questionnaire form kinds: `data_sets` picks
    aggregate data sets, `event_programs` picks programs without registration, `tracker_programs`
    picks programs with registration (one Questionnaire per program stage plus the program's own
    registration form), and `tracked_entity_forms` picks the tracked entity types that publish a
    person-only registration form - a form that creates a person and enrols them in nothing.
    Empty, `tracked_entity_forms` publishes one form per type the selected tracker programs
    track; the other three publish everything of their kind the instance holds.

    `hostile_names` is what the run does with a DHIS2 name carrying `<`, which the IG publisher
    writes into a page it strict-parses and then dies on. Unset, the run asks on a terminal and
    refuses without one, so a script is never left hanging on a question; `"refuse"` answers it
    with today's refusal and `"substitute"` publishes the rewritten wording. `d2w fhir generate
    --substitute-hostile-names` and `--refuse-hostile-names` answer it for one run.

    `timezone` is the IANA zone the instance's zone-less timestamps are wall-clock readings in
    (BUGS.md #62). Naming it turns every emitted `dateTime` into the numeric offset that zone
    was on at that very instant, DST included; leaving it unset keeps the UTC reading.

    `tracked_entity_types` maps a DHIS2 tracked entity type UID onto the FHIR resource type its
    registrations are about. A DHIS2 tracked entity is not always a person - a project tracks
    households, buildings, herds, and equipment as readily as patients - so the type says what it
    is and every form of every program tracking it follows. A type named here is not selected by
    it: selection stays with the three data-definition tables, and a type this table never
    mentions is a `Patient`, which is what keeps a person-tracking project's config empty.
    """

    model_config = ConfigDict(extra="forbid")

    identifier_system_base: str = "http://dhis2.org/fhir"
    concept_code_source: Literal["id", "code"] = "id"
    hostile_names: HostileNamePosture | None = None
    timezone: str | None = None
    locales: list[str] = Field(default_factory=list)
    naming: NamingConfig = Field(default_factory=NamingConfig)
    option_sets: OptionSetSelection = Field(default_factory=OptionSetSelection)
    categories: CategorySelection = Field(default_factory=CategorySelection)
    organisation_units: OrganisationUnitSelection = Field(default_factory=OrganisationUnitSelection)
    data_sets: TargetSelection = Field(default_factory=TargetSelection)
    event_programs: TargetSelection = Field(default_factory=TargetSelection)
    tracker_programs: TargetSelection = Field(default_factory=TargetSelection)
    tracked_entity_forms: TargetSelection = Field(default_factory=TargetSelection)
    tracked_entity_types: dict[str, str] = Field(default_factory=dict)
    examples: ExampleSelection = Field(default_factory=ExampleSelection)

    _normalize_identifier_base = field_validator("identifier_system_base")(strip_trailing_slash)

    @field_validator("tracked_entity_types")
    @classmethod
    def _known_subject_resource_types(cls, value: dict[str, str]) -> dict[str, str]:
        """Require an R4 resource type a tracked entity can be - a typo here mis-types every form of a program."""
        for uid, resource_type in value.items():
            if resource_type not in SUBJECT_RESOURCE_TYPES:
                raise ValueError(
                    f"tracked entity type {uid} is mapped to {resource_type!r}, which is not a FHIR resource "
                    f"type a tracked entity is published as: name one of {', '.join(SUBJECT_RESOURCE_TYPES)}"
                )
        return value

    @field_validator("timezone")
    @classmethod
    def _known_timezone(cls, value: str | None) -> str | None:
        """Require an IANA zone name the tz database actually holds - a typo here mis-stamps every timestamp."""
        if value is None:
            return value
        try:
            zoneinfo.ZoneInfo(value)
        except (zoneinfo.ZoneInfoNotFoundError, ValueError) as error:
            raise ValueError(
                f"unknown IANA time zone {value!r}: name a zone from the tz database "
                "(e.g. 'Asia/Vientiane', 'Europe/Oslo', 'UTC')"
            ) from error
        return value

    @field_validator("locales")
    @classmethod
    def _normalize_locales(cls, value: list[str]) -> list[str]:
        """Accept BCP-47 or DHIS2-style tags and hold them in the BCP-47 form the emitters compare against."""
        return [normalize_locale(locale) for locale in value]

BasemapSource

Bases: BaseModel

One named raster tile source the capture UI's map offers as a layer.

name is what the map's layer control calls it and is the deployment's own word - this project never renames a source it was pointed at. url is the {z}/{x}/{y} template the tiles are fetched from, and it is the one thing in the whole UI that reaches an origin other than the server the page came from.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class BasemapSource(BaseModel):
    """One named raster tile source the capture UI's map offers as a layer.

    `name` is what the map's layer control calls it and is the deployment's own word - this
    project never renames a source it was pointed at. `url` is the `{z}/{x}/{y}` template the
    tiles are fetched from, and it is the one thing in the whole UI that reaches an origin other
    than the server the page came from.
    """

    model_config = ConfigDict(frozen=True)

    name: str
    url: str

DataSetsConfig

Bases: BaseModel

What a live run answers about the instance's aggregate data - the [serve.data_sets] table.

The register's sibling, and the same posture: every default offers everything, and the table exists for the deployment that wants less. It says what this facade will tell a client about the values the instance holds for a data set, which is a decision a project makes once rather than one per invocation, so no flag overrides it.

responses is the whole surface: false and GET /facade/data-sets/{uid}/responses answers the not-supported outcome naming this key, while the guide's own forms, the receipts, and the register are served exactly as they were. There is no second enabled key beside it, because there is no aggregate register for one to take away - a data set's values are the only thing this table is about.

page_size is what one page carries when the client names no _count, and page_size_limit is the largest _count honoured: a client asking for more is served the limit rather than refused.

data_sets means "the ones the guide publishes" when empty, which is what keeps this table absent from a project that publishes what it serves. Naming UIDs restricts the surface to them, and a data set outside the list is answered exactly as one the guide publishes no form for.

period_limit is the most periods one read may name. Every read is answered whole, so the periods a request names are what bounds its cost; a request naming more is refused with the limit and the count it gave.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class DataSetsConfig(BaseModel):
    """What a live run answers about the instance's aggregate data - the `[serve.data_sets]` table.

    The register's sibling, and the same posture: every default offers everything, and the table
    exists for the deployment that wants less. It says what this facade will tell a client about the
    values the instance holds for a data set, which is a decision a project makes once rather than
    one per invocation, so no flag overrides it.

    `responses` is the whole surface: false and `GET /facade/data-sets/{uid}/responses` answers the
    not-supported outcome naming this key, while the guide's own forms, the receipts, and the
    register are served exactly as they were. There is no second `enabled` key beside it, because
    there is no aggregate register for one to take away - a data set's values are the only thing
    this table is about.

    `page_size` is what one page carries when the client names no `_count`, and `page_size_limit` is
    the largest `_count` honoured: a client asking for more is served the limit rather than refused.

    `data_sets` means "the ones the guide publishes" when empty, which is what keeps this table
    absent from a project that publishes what it serves. Naming UIDs restricts the surface to them,
    and a data set outside the list is answered exactly as one the guide publishes no form for.

    `period_limit` is the most periods one read may name. Every read is answered whole, so the
    periods a request names are what bounds its cost; a request naming more is refused with the
    limit and the count it gave.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    responses: bool = True
    page_size: int = DEFAULT_REGISTER_PAGE_SIZE
    page_size_limit: int = DEFAULT_REGISTER_PAGE_SIZE_LIMIT
    data_sets: list[str] = Field(default_factory=list)
    period_limit: int = DEFAULT_DATA_SET_PERIOD_LIMIT

    @field_validator("page_size")
    @classmethod
    def _at_least_one_response_per_page(cls, value: int) -> int:
        """A page carrying no response is a listing that never ends, so the smallest page is one response."""
        if value < 1:
            raise ValueError(f"page_size is {value}: a page carries at least one response")
        return value

    @field_validator("period_limit")
    @classmethod
    def _at_least_one_period_per_read(cls, value: int) -> int:
        """A read naming no period is refused, so a limit below one would refuse every read there is."""
        if value < 1:
            raise ValueError(f"period_limit is {value}: a read names at least one period")
        return value

    @field_validator("data_sets")
    @classmethod
    def _dhis2_data_set_uids(cls, value: list[str]) -> list[str]:
        """The list names DHIS2 data sets by UID - a name or a code here would select nothing, silently."""
        for uid in value:
            if not is_dhis2_uid(uid):
                raise ValueError(
                    f"{uid!r} is not a DHIS2 UID (one letter followed by ten alphanumeric places): "
                    "name the data set by its UID, since names and codes are not unique in DHIS2"
                )
        return value

    @model_validator(mode="after")
    def _limit_holds_the_default(self) -> DataSetsConfig:
        """The limit is the largest page this run serves, so a default above it could never be served."""
        if self.page_size_limit < self.page_size:
            raise ValueError(
                f"page_size_limit is {self.page_size_limit} and page_size is {self.page_size}: the limit is "
                "the largest page this server serves, so it cannot be smaller than the page it serves by default"
            )
        return self

TrackedEntitiesConfig

Bases: BaseModel

The register a live run serves - the [serve.tracked_entities] table of fhir.toml.

Tracked entities are the one thing this facade answers from the DHIS2 instance rather than from what it published, so what it will say about them is stated here rather than inferred from the guide. The table is register-wide: it says the same thing about every FHIR resource the published map takes a tracked entity type onto, because whether this process answers about the instance's subjects at all is one decision rather than one per resource type.

enabled is the whole register: false and every register route answers the not-supported outcome and /metadata declares none of its resource types, in a live process exactly as in a compiled one. listing is the no-parameter search alone - false leaves identifier search untouched and refuses only the request that means "everybody", which is the posture for an instance whose register is not something a capture client may page through.

events is one entity's own record - the events of its enrollments, each served as the QuestionnaireResponse the guide publishes for its program stage. False and the record is refused while identity is still served, which is the posture for a deployment that publishes who its subjects are and not what was recorded about them.

page_size is what one page carries when the client names no _count, and page_size_limit is the largest _count honoured: a client asking for more is served the limit rather than refused, which is what FHIR says a server may do with a _count it will not meet.

tracked_entity_types and search_attributes both mean "the ones the guide publishes" when empty, which is what keeps this table absent from a project that publishes what it serves. Naming types restricts search and listing alike to them. Naming attributes defines the search keys outright - a named attribute is a key whether or not DHIS2 declares it unique or searchable, because the operator naming it has said it names a subject here.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class TrackedEntitiesConfig(BaseModel):
    """The register a live run serves - the `[serve.tracked_entities]` table of `fhir.toml`.

    Tracked entities are the one thing this facade answers from the DHIS2 instance rather than from
    what it published, so what it will say about them is stated here rather than inferred from the
    guide. The table is register-wide: it says the same thing about every FHIR resource the published
    map takes a tracked entity type onto, because whether this process answers about the instance's
    subjects at all is one decision rather than one per resource type.

    `enabled` is the whole register: false and every register route answers the not-supported outcome
    and `/metadata` declares none of its resource types, in a live process exactly as in a compiled
    one. `listing` is the no-parameter search alone - false leaves identifier search untouched and
    refuses only the request that means "everybody", which is the posture for an instance whose
    register is not something a capture client may page through.

    `events` is one entity's own record - the events of its enrollments, each served as the
    QuestionnaireResponse the guide publishes for its program stage. False and the record is refused
    while identity is still served, which is the posture for a deployment that publishes who its
    subjects are and not what was recorded about them.

    `page_size` is what one page carries when the client names no `_count`, and `page_size_limit`
    is the largest `_count` honoured: a client asking for more is served the limit rather than
    refused, which is what FHIR says a server may do with a `_count` it will not meet.

    `tracked_entity_types` and `search_attributes` both mean "the ones the guide publishes" when
    empty, which is what keeps this table absent from a project that publishes what it serves.
    Naming types restricts search and listing alike to them. Naming attributes defines the search
    keys outright - a named attribute is a key whether or not DHIS2 declares it unique or searchable,
    because the operator naming it has said it names a subject here.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    enabled: bool = True
    listing: bool = True
    events: bool = True
    page_size: int = DEFAULT_REGISTER_PAGE_SIZE
    page_size_limit: int = DEFAULT_REGISTER_PAGE_SIZE_LIMIT
    tracked_entity_types: list[str] = Field(default_factory=list)
    search_attributes: list[str] = Field(default_factory=list)

    @field_validator("page_size")
    @classmethod
    def _at_least_one_subject_per_page(cls, value: int) -> int:
        """A page carrying nobody is a listing that never ends, so the smallest page is one tracked entity."""
        if value < 1:
            raise ValueError(f"page_size is {value}: a page carries at least one tracked entity")
        return value

    @field_validator("tracked_entity_types", "search_attributes")
    @classmethod
    def _dhis2_uids(cls, value: list[str]) -> list[str]:
        """Both lists name DHIS2 objects by UID - a name or a code here would select nothing, silently."""
        for uid in value:
            if not is_dhis2_uid(uid):
                raise ValueError(
                    f"{uid!r} is not a DHIS2 UID (one letter followed by ten alphanumeric places): "
                    "name the object by its UID, since names and codes are not unique in DHIS2"
                )
        return value

    @model_validator(mode="after")
    def _limit_holds_the_default(self) -> TrackedEntitiesConfig:
        """The limit is the largest page this run serves, so a default above it could never be served."""
        if self.page_size_limit < self.page_size:
            raise ValueError(
                f"page_size_limit is {self.page_size_limit} and page_size is {self.page_size}: the limit is "
                "the largest page this server serves, so it cannot be smaller than the page it serves by default"
            )
        return self

SearchBackend

Bases: StrEnum

What answers a register search - the [serve.search] backend key.

Two values ship. "index" is the name reserved for the OpenSearch backend of step 6 in docs/fhir/design/projection.md, and it is deliberately not a member here for the reason ServeAuth reserves oauth2 in its own docstring: a value that parses and then has nothing to run on is worse than a value the file refuses by name, naming the key it refused.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class SearchBackend(StrEnum):
    """What answers a register search - the `[serve.search] backend` key.

    Two values ship. `"index"` is the name reserved for the OpenSearch backend of step 6 in
    `docs/fhir/design/projection.md`, and it is deliberately not a member here for the reason
    `ServeAuth` reserves `oauth2` in its own docstring: a value that parses and then has nothing to
    run on is worse than a value the file refuses by name, naming the key it refused.
    """

    #: The DHIS2 instance itself, asked one filtered tracker search per key while the caller waits.
    #: Search is as wide as `filter=<attribute>:eq:<value>` is, and every match is authorized by the
    #: instance on the read that resolves it.
    DHIS2 = "dhis2"

    #: The materialized projection `d2w fhir sync` fills, asked one indexed query however many keys
    #: and types are in scope. Finding is answered from the store and stated as of its cursor; the
    #: record behind a match is still read from the instance under the caller's own credentials, so
    #: what the projection decides is who is on the page and what DHIS2 decides is who may see them.
    #: Needs `[serve.projection] store` to name a store, and the run is refused when it names none.
    PROJECTION = "projection"

SearchConfig

Bases: BaseModel

How a register search is answered - the [serve.search] table of fhir.toml.

A register search reaches the instance through one seam, NameSearchIndex, and this table says which backend sits behind it. backend = "dhis2" is the instance itself: the search a live run has always run, with the authorization properties it has always had - the matches a search discloses are identifiers, and the record behind one is read back under the caller's own credentials, so DHIS2 decides per match per caller what may be seen.

backend = "projection" moves the finding half into the store [serve.projection] names and leaves the disclosing half exactly where it is. A search then costs one indexed query rather than one tracker query per key per tracked entity type, it answers a name the exact-match filter could never answer, and every answer states the cursor it is as of. What it does not do is hand over a record the instance has not authorized: each match is read back live under the caller's own credentials, which is the R9 posture of docs/fhir/design/projection.md section 6 in full.

The table has no command-line flag. Which searches this server answers is its contract - it is what /metadata declares - rather than a property of one invocation, which is the rule [serve.tracked_entities] follows for the same reason.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class SearchConfig(BaseModel):
    """How a register search is answered - the `[serve.search]` table of `fhir.toml`.

    A register search reaches the instance through one seam, `NameSearchIndex`, and this table says
    which backend sits behind it. `backend = "dhis2"` is the instance itself: the search a live run
    has always run, with the authorization properties it has always had - the matches a search
    discloses are identifiers, and the record behind one is read back under the caller's own
    credentials, so DHIS2 decides per match per caller what may be seen.

    `backend = "projection"` moves the finding half into the store `[serve.projection]` names and
    leaves the disclosing half exactly where it is. A search then costs one indexed query rather
    than one tracker query per key per tracked entity type, it answers a name the exact-match filter
    could never answer, and every answer states the cursor it is as of. What it does not do is hand
    over a record the instance has not authorized: each match is read back live under the caller's
    own credentials, which is the R9 posture of `docs/fhir/design/projection.md` section 6 in full.

    The table has no command-line flag. Which searches this server answers is its contract - it is
    what `/metadata` declares - rather than a property of one invocation, which is the rule
    `[serve.tracked_entities]` follows for the same reason.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    backend: SearchBackend = SearchBackend.DHIS2

ProjectionBackend

Bases: StrEnum

Which durable store holds this project's materialized projection - [serve.projection] store.

"postgres" is the name reserved for the document store of step 7 in docs/fhir/design/projection.md, and it is deliberately not a member here for the reason SearchBackend reserves "index": a value that parses and then has nothing to run on is worse than a value the file refuses by name.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ProjectionBackend(StrEnum):
    """Which durable store holds this project's materialized projection - `[serve.projection] store`.

    `"postgres"` is the name reserved for the document store of step 7 in
    `docs/fhir/design/projection.md`, and it is deliberately not a member here for the reason
    `SearchBackend` reserves `"index"`: a value that parses and then has nothing to run on is worse
    than a value the file refuses by name.
    """

    #: No projection. The default, and the whole of what makes a facade configured without one behave
    #: exactly as it always did: `d2w fhir sync` refuses and names this key, and every register answer
    #: is read from the instance while the caller waits.
    NONE = "none"

    #: One SQLite file under the project root, over SQLAlchemy and aiosqlite. Needs no service, no
    #: operator, and no port - which is what makes a synced FHIR server something a district office
    #: can run on the machine it already has.
    SQLITE = "sqlite"

ProjectionConfig

Bases: BaseModel

The materialized projection this project holds - the [serve.projection] table of fhir.toml.

A projection is a durable copy of the mapped scope of a DHIS2 instance, held as the FHIR resources this project's map publishes, filled by d2w fhir sync and written by nothing else. It is derived, it is rebuildable from zero, and every answer served out of it states the instant it is as of. docs/fhir/design/projection.md section 4 is the doctrine in seven rules, and the first of them is the one this table exists under: DHIS2 stays the record for everything DHIS2 can hold, and this holds a copy.

store is which backend holds it, and "none" - the default - is no projection at all. That is not a degraded mode: a facade reading the instance per request is the product, it needs no operator, and nothing in this table may make it harder to run (docs/fhir/design/projection.md R11).

path is where a SQLite projection's one file lives, relative to the project root unless it is absolute - the same rule [serve] spool_dir follows, because the two are the same kind of directory work. Deleting the file is a supported operation: the next d2w fhir sync fills it from zero, which is what "rebuildable" means when it is not frightening.

overlap_seconds is how far back before its own watermark an incremental run re-reads. A sync that polled from exactly its watermark would drop the rows written in the instant it was reading, so it re-reads a window and relies on a write being idempotent by resource id.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ProjectionConfig(BaseModel):
    """The materialized projection this project holds - the `[serve.projection]` table of `fhir.toml`.

    A projection is a durable copy of the mapped scope of a DHIS2 instance, held as the FHIR
    resources this project's map publishes, filled by `d2w fhir sync` and written by nothing else.
    It is derived, it is rebuildable from zero, and every answer served out of it states the instant
    it is as of. `docs/fhir/design/projection.md` section 4 is the doctrine in seven rules, and the
    first of them is the one this table exists under: DHIS2 stays the record for everything DHIS2
    can hold, and this holds a copy.

    `store` is which backend holds it, and `"none"` - the default - is no projection at all. That is
    not a degraded mode: a facade reading the instance per request is the product, it needs no
    operator, and nothing in this table may make it harder to run
    (`docs/fhir/design/projection.md` R11).

    `path` is where a SQLite projection's one file lives, relative to the project root unless it is
    absolute - the same rule `[serve] spool_dir` follows, because the two are the same kind of
    directory work. Deleting the file is a supported operation: the next `d2w fhir sync` fills it
    from zero, which is what "rebuildable" means when it is not frightening.

    `overlap_seconds` is how far back before its own watermark an incremental run re-reads. A sync
    that polled from exactly its watermark would drop the rows written in the instant it was reading,
    so it re-reads a window and relies on a write being idempotent by resource id.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    store: ProjectionBackend = ProjectionBackend.NONE
    path: str = DEFAULT_PROJECTION_RELATIVE_PATH
    overlap_seconds: int = DEFAULT_SYNC_OVERLAP_SECONDS

    @field_validator("path")
    @classmethod
    def _names_a_file(cls, value: str) -> str:
        """An empty path names nothing, and a project that states one has to mean it."""
        if value.strip() == "":
            raise ValueError(
                "path is empty: name the file the projection lives in, relative to the project root "
                f"or absolute, or leave the key out for {DEFAULT_PROJECTION_RELATIVE_PATH!r}"
            )
        return value

    @field_validator("overlap_seconds")
    @classmethod
    def _not_negative(cls, value: int) -> int:
        """A negative overlap would poll from after the watermark, which is how a sync loses rows silently."""
        if value < 0:
            raise ValueError(
                f"overlap_seconds is {value}: an incremental run re-reads a window BEFORE its own "
                "watermark, so the window is zero or more seconds"
            )
        return value

    def enabled(self) -> bool:
        """Whether this project holds a projection at all, which is the one thing every caller asks first."""
        return self.store is not ProjectionBackend.NONE
Methods:
enabled()

Whether this project holds a projection at all, which is the one thing every caller asks first.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def enabled(self) -> bool:
    """Whether this project holds a projection at all, which is the one thing every caller asks first."""
    return self.store is not ProjectionBackend.NONE

ServeAuth

Bases: StrEnum

How the served facade decides who is calling it - the [serve] auth posture.

Four postures ship, and they are a ladder rather than a menu: none serves every caller, token takes a secret this deployment issued, dhis2 takes the caller's own DHIS2 credentials, and jwt takes a token an external OpenID Connect issuer minted, validated here against that issuer's JWKS. Climbing a rung is editing [serve] auth and the table beside it.

oauth2 is the name reserved for an authorization server this facade runs itself, and is deliberately not a value here: DHIS2 2.43.1's own authorization server 500s for any client the API creates (BUGS.md 96), so a project could state it and nothing would answer. A posture that parses and then refuses is worse than one that is not offered, so the reservation lives in this docstring and in docs/fhir/301-serving.md rather than in the enum. A deployment that wants tokens from an authorization server today states jwt and names the one it already runs.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ServeAuth(StrEnum):
    """How the served facade decides who is calling it - the `[serve] auth` posture.

    Four postures ship, and they are a ladder rather than a menu: `none` serves every caller,
    `token` takes a secret this deployment issued, `dhis2` takes the caller's own DHIS2 credentials,
    and `jwt` takes a token an external OpenID Connect issuer minted, validated here against that
    issuer's JWKS. Climbing a rung is editing `[serve] auth` and the table beside it.

    `oauth2` is the name reserved for an authorization server this facade runs itself, and is
    deliberately not a value here: DHIS2 2.43.1's own authorization server 500s for any client the
    API creates (BUGS.md 96), so a project could state it and nothing would answer. A posture that
    parses and then refuses is worse than one that is not offered, so the reservation lives in this
    docstring and in `docs/fhir/301-serving.md` rather than in the enum. A deployment that wants
    tokens from an authorization server today states `jwt` and names the one it already runs.
    """

    #: Every caller is served. The default, and the posture the loopback demo runs in.
    NONE = "none"

    #: A static bearer token out of `D2W_FHIR_SERVE_TOKENS`, compared in constant time.
    TOKEN = "token"

    #: The caller's own DHIS2 credentials, checked against the instance this run reads.
    DHIS2 = "dhis2"

    #: A JWT an external OIDC issuer minted, verified here against that issuer's published keys.
    JWT = "jwt"

ServeJwtConfig

Bases: BaseModel

Which issuer the jwt posture trusts, and what one of its tokens means - the [serve.jwt] table.

issuer is the OpenID Connect issuer identifier, the value its tokens carry as iss. The facade reads {issuer}/.well-known/openid-configuration once while it starts, takes the jwks_uri from it, and verifies every token against the keys published there. It is the one key the posture cannot run without, and a jwt run that names none is refused before the socket opens rather than at the first caller.

audience is checked only when it is stated. An issuer that mints tokens for several audiences is minting tokens this facade should not accept on another audience's behalf, so a deployment sharing an issuer with other services states the value it was registered under. Left out, a token this issuer signed is a token this facade takes.

username_claim names the claim whose value becomes the request identity - what a receipt records as its submitter. preferred_username is the OpenID Connect claim for exactly that, and it is the default; a deployment whose issuer puts the DHIS2 username somewhere else names that claim instead. A token that carries no such claim is refused: an identity nobody stated is not one to invent.

forward_bearer is whether a register read carries the caller's token on to DHIS2. It is false by default and it is the honest default: DHIS2 accepts a foreign issuer's JWT only when it was configured to trust the same issuer (oidc.jwt.token.authentication.enabled), and a facade that quietly read the register as its own profile instead would answer every caller with that profile's rights. So under forward_bearer = false the live register is not answered at all, and the refusal says what would make it answerable.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ServeJwtConfig(BaseModel):
    """Which issuer the `jwt` posture trusts, and what one of its tokens means - the `[serve.jwt]` table.

    `issuer` is the OpenID Connect issuer identifier, the value its tokens carry as `iss`. The
    facade reads `{issuer}/.well-known/openid-configuration` once while it starts, takes the
    `jwks_uri` from it, and verifies every token against the keys published there. It is the one
    key the posture cannot run without, and a `jwt` run that names none is refused before the socket
    opens rather than at the first caller.

    `audience` is checked only when it is stated. An issuer that mints tokens for several audiences
    is minting tokens this facade should not accept on another audience's behalf, so a deployment
    sharing an issuer with other services states the value it was registered under. Left out, a
    token this issuer signed is a token this facade takes.

    `username_claim` names the claim whose value becomes the request identity - what a receipt
    records as its submitter. `preferred_username` is the OpenID Connect claim for exactly that, and
    it is the default; a deployment whose issuer puts the DHIS2 username somewhere else names that
    claim instead. A token that carries no such claim is refused: an identity nobody stated is not
    one to invent.

    `forward_bearer` is whether a register read carries the caller's token on to DHIS2. It is false
    by default and it is the honest default: DHIS2 accepts a foreign issuer's JWT only when it was
    configured to trust the same issuer (`oidc.jwt.token.authentication.enabled`), and a facade that
    quietly read the register as its own profile instead would answer every caller with that
    profile's rights. So under `forward_bearer = false` the live register is not answered at all,
    and the refusal says what would make it answerable.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    issuer: str | None = None
    """The OIDC issuer identifier, or None when this project's table states none."""

    audience: str | None = None
    """The `aud` every accepted token must carry, or None to accept whatever this issuer minted."""

    username_claim: str = "preferred_username"
    """The claim whose value names the caller, and becomes the identity a receipt records."""

    forward_bearer: bool = False
    """Whether a register read carries the caller's own token to DHIS2 - see the note above."""

    @field_validator("issuer")
    @classmethod
    def _names_an_issuer(cls, value: str | None) -> str | None:
        """A blank issuer states nothing, and a stated one is the `https://` identifier its tokens carry."""
        if value is None or value.strip() == "":
            return None
        issuer = value.strip().rstrip("/")
        scheme = urlsplit(issuer).scheme
        if scheme not in {"http", "https"}:
            raise ValueError(
                f"{value!r} is not an issuer identifier: name the `https://` URL the issuer publishes as its "
                "own `iss`, which is what this server appends `/.well-known/openid-configuration` to"
            )
        return issuer

    @field_validator("username_claim")
    @classmethod
    def _names_a_claim(cls, value: str) -> str:
        """A blank claim name would read every token as naming nobody, which is a posture nobody asked for."""
        if value.strip() == "":
            raise ValueError(
                "username_claim is empty: name the claim whose value identifies the caller, or leave the "
                "key out for `preferred_username`"
            )
        return value.strip()
Attributes
issuer = None class-attribute instance-attribute

The OIDC issuer identifier, or None when this project's table states none.

audience = None class-attribute instance-attribute

The aud every accepted token must carry, or None to accept whatever this issuer minted.

username_claim = 'preferred_username' class-attribute instance-attribute

The claim whose value names the caller, and becomes the identity a receipt records.

forward_bearer = False class-attribute instance-attribute

Whether a register read carries the caller's own token to DHIS2 - see the note above.

ServeAuthScope

Bases: StrEnum

How much of the served surface one [serve] auth posture covers.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ServeAuthScope(StrEnum):
    """How much of the served surface one `[serve] auth` posture covers."""

    #: The state-changing surface only: `POST /QuestionnaireResponse`. Reads stay open.
    WRITE = "write"

    #: Everything but `/metadata`, which stays open so a client can read the posture it must meet.
    ALL = "all"

ServeConfig

Bases: BaseModel

How d2w fhir serve runs this project - the [serve] table of fhir.toml.

Where a project is served from is a property of the project, not of the invocation: a developer whose DHIS2 stack already owns 8080 states another port once here and every make serve and bare d2w fhir serve in that project honours it. A command-line flag still wins over the table, and the table wins over these defaults.

ui serves the capture UI at / alongside the FHIR routes. A project whose whole workflow is people filling in forms states it once here and gets the UI from every make serve.

basemaps names the raster tile layers the capture UI's organisation-unit map offers, in the order it offers them; the first is the one the map opens with. The layer control always carries a None entry beside them, so drawing the boundaries on a plain canvas is a click rather than a config change. An empty list is therefore the air-gapped posture in full - the only layer on offer is None, and the page reaches no origin but this server. It is the one part of this table that makes the browser talk to anybody else, which is why it is stated rather than inferred.

capture is whether this server receives submissions at all. True - the default - mounts the create route, declares create on QuestionnaireResponse in /metadata, and lets the capture screens submit. False is the viewer posture: the guide is published, read, searched, and $generated against, and nothing is received. $generate stays because it is a read of a published form that happens to answer with a draft, and writes nothing here.

THE RECEIPTS STAY READABLE WITH CAPTURE OFF. read and search-type on QuestionnaireResponse are untouched by this dial, and so is GET /facade/spool. The receipts a project already holds are as true as they were, and a server that stopped answering for them would make every id it handed out at capture time expire on the day somebody edited one line of this file. Only create goes.

spool_dir is where the receipt tree lives - received/, forwarded/, rejected/, and the malformed/ holding pen beside them. A relative path is resolved against the project root; an absolute one is taken as written. It is a [serve] key because the spool belongs to the serve surface - this server is what writes it - and d2w fhir forward reads the same key rather than carrying a spool location of its own, so the process that writes a receipt and the process that drains it can never disagree about where it is.

[serve.tracked_entities] is the register: whether the instance's tracked entities are served at all, whether they can be listed rather than only searched for, and how a listing is paged. It is one of the two parts of this table that say what a live run will tell a client about the instance behind it.

[serve.data_sets] is the other: whether a live run answers what the instance holds for a data set, which data sets it answers for, how a page is sized, and how many periods one read may name. The register says what this facade will tell a client about the instance's subjects, and this says what it will tell them about its aggregate values.

[serve.search] is what answers a register search - the instance itself, or the materialized projection beside it. It says how a lookup is answered where [serve.tracked_entities] says what may be looked up.

[serve.projection] is the materialized projection: which store holds a durable copy of the mapped scope of the instance, where that store lives, and how far back an incremental sync re-reads. store = "none" - the default - is no projection at all, which is the posture every facade started before this table existed runs in and will keep running in.

auth is who the facade serves, and auth_scope is how much of it the posture covers. The key is ServeAuth | None rather than ServeAuth because absence is a third state this table has to be able to tell apart: a project that never wrote the key is served on loopback with no authentication, and refused on any other interface, so that binding the world is a sentence somebody wrote rather than a default nobody read. auth = "none" written out is that sentence.

[serve.jwt] is what auth = "jwt" runs on: the issuer whose tokens this facade takes, the audience it checks when one is stated, the claim that names the caller, and whether a register read carries the caller's token on to DHIS2. It is always present as a table of defaults, so a jwt run that names no issuer is refused by the key's name rather than by a missing section.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ServeConfig(BaseModel):
    """How `d2w fhir serve` runs this project - the `[serve]` table of `fhir.toml`.

    Where a project is served from is a property of the project, not of the invocation: a
    developer whose DHIS2 stack already owns 8080 states another port once here and every
    `make serve` and bare `d2w fhir serve` in that project honours it. A command-line flag still
    wins over the table, and the table wins over these defaults.

    `ui` serves the capture UI at `/` alongside the FHIR routes. A project whose whole workflow is
    people filling in forms states it once here and gets the UI from every `make serve`.

    `basemaps` names the raster tile layers the capture UI's organisation-unit map offers, in the
    order it offers them; the first is the one the map opens with. The layer control always carries
    a `None` entry beside them, so drawing the boundaries on a plain canvas is a click rather than a
    config change. An empty list is therefore the air-gapped posture in full - the only layer on
    offer is `None`, and the page reaches no origin but this server. It is the one part of this
    table that makes the browser talk to anybody else, which is why it is stated rather than
    inferred.

    `capture` is whether this server receives submissions at all. True - the default - mounts the
    create route, declares `create` on QuestionnaireResponse in `/metadata`, and lets the capture
    screens submit. False is the viewer posture: the guide is published, read, searched, and
    `$generate`d against, and nothing is received. `$generate` stays because it is a read of a
    published form that happens to answer with a draft, and writes nothing here.

    THE RECEIPTS STAY READABLE WITH CAPTURE OFF. `read` and `search-type` on QuestionnaireResponse
    are untouched by this dial, and so is `GET /facade/spool`. The receipts a project already holds are as
    true as they were, and a server that stopped answering for them would make every id it handed
    out at capture time expire on the day somebody edited one line of this file. Only `create` goes.

    `spool_dir` is where the receipt tree lives - `received/`, `forwarded/`, `rejected/`, and the
    `malformed/` holding pen beside them. A relative path is resolved against the project root; an
    absolute one is taken as written. It is a `[serve]` key because the spool belongs to the serve
    surface - this server is what writes it - and `d2w fhir forward` reads the same key rather than
    carrying a spool location of its own, so the process that writes a receipt and the process that
    drains it can never disagree about where it is.

    `[serve.tracked_entities]` is the register: whether the instance's tracked entities are served at
    all, whether they can be listed rather than only searched for, and how a listing is paged. It is
    one of the two parts of this table that say what a live run will tell a client about the instance
    behind it.

    `[serve.data_sets]` is the other: whether a live run answers what the instance holds for a data
    set, which data sets it answers for, how a page is sized, and how many periods one read may name.
    The register says what this facade will tell a client about the instance's subjects, and this
    says what it will tell them about its aggregate values.

    `[serve.search]` is what answers a register search - the instance itself, or the materialized
    projection beside it. It says how a lookup is answered where `[serve.tracked_entities]` says
    what may be looked up.

    `[serve.projection]` is the materialized projection: which store holds a durable copy of the
    mapped scope of the instance, where that store lives, and how far back an incremental sync
    re-reads. `store = "none"` - the default - is no projection at all, which is the posture every
    facade started before this table existed runs in and will keep running in.

    `auth` is who the facade serves, and `auth_scope` is how much of it the posture covers. The key
    is `ServeAuth | None` rather than `ServeAuth` because absence is a third state this table has to
    be able to tell apart: a project that never wrote the key is served on loopback with no
    authentication, and refused on any other interface, so that binding the world is a sentence
    somebody wrote rather than a default nobody read. `auth = "none"` written out is that sentence.

    `[serve.jwt]` is what `auth = "jwt"` runs on: the issuer whose tokens this facade takes, the
    audience it checks when one is stated, the claim that names the caller, and whether a register
    read carries the caller's token on to DHIS2. It is always present as a table of defaults, so a
    `jwt` run that names no issuer is refused by the key's name rather than by a missing section.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    host: str = "127.0.0.1"
    port: int = 8080
    auth: ServeAuth | None = None
    """The posture, or None when this project's table states none - see the note above on absence."""

    auth_scope: ServeAuthScope = ServeAuthScope.WRITE
    jwt: ServeJwtConfig = Field(default_factory=ServeJwtConfig)
    """What the `jwt` posture runs on - the `[serve.jwt]` table, defaults when the project writes none."""

    strict_codes: bool = False
    capture: bool = True
    ui: bool = False
    spool_dir: str = SPOOL_RELATIVE_PATH
    basemaps: list[BasemapSource] = Field(default_factory=lambda: list(DEFAULT_BASEMAPS))
    tracked_entities: TrackedEntitiesConfig = Field(default_factory=TrackedEntitiesConfig)
    data_sets: DataSetsConfig = Field(default_factory=DataSetsConfig)
    search: SearchConfig = Field(default_factory=SearchConfig)
    projection: ProjectionConfig = Field(default_factory=ProjectionConfig)

    @model_validator(mode="after")
    def _a_projection_search_has_a_projection(self) -> ServeConfig:
        """`[serve.search] backend = "projection"` needs a store to search, and this file has to name it.

        Refused here rather than at the first lookup, because a server that starts and then answers
        every search with a failure is a decision nobody reads until somebody meets it - and the two
        keys are three lines apart in the same file.
        """
        if self.search.backend is SearchBackend.PROJECTION and not self.projection.enabled():
            raise ValueError(
                'serve.search.backend is "projection" and serve.projection.store is "none": a search '
                "answered from the materialized projection needs one to read, so state "
                '`[serve.projection] store = "sqlite"` and fill it with `d2w fhir sync`'
            )
        return self

    @field_validator("spool_dir")
    @classmethod
    def _names_a_directory(cls, value: str) -> str:
        """An empty spool directory names nothing, and a project that states one has to mean it."""
        if value.strip() == "":
            raise ValueError(
                "spool_dir is empty: name a directory the receipts live in, relative to the project "
                f"root or absolute, or leave the key out for {SPOOL_RELATIVE_PATH!r}"
            )
        return value
Attributes
auth = None class-attribute instance-attribute

The posture, or None when this project's table states none - see the note above on absence.

jwt = Field(default_factory=ServeJwtConfig) class-attribute instance-attribute

What the jwt posture runs on - the [serve.jwt] table, defaults when the project writes none.

OverwritePosture

Bases: StrEnum

What a drain does with an aggregate value a forwarded receipt of this spool already sent.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class OverwritePosture(StrEnum):
    """What a drain does with an aggregate value a forwarded receipt of this spool already sent."""

    #: Post the value and name it. DHIS2 keeps the newest number for a cell, and this posture is the
    #: toolkit agreeing with that: the instance ends up holding what the newest submission carried,
    #: and the run states every value it replaced, the receipt that sent it before, and when that
    #: receipt arrived - which is the one thing no import summary can say.
    ALLOW = "allow"

    #: Refuse the whole response and leave it in the queue with a record naming every covered value.
    #: The posture for a deployment where forwarded data changes only through a declared correction.
    REFUSE = "refuse"

CorrectionPosture

Bases: StrEnum

Whether this deployment accepts a submission that amends one it already forwarded.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class CorrectionPosture(StrEnum):
    """Whether this deployment accepts a submission that amends one it already forwarded."""

    #: Amendment is not something this deployment does. A submission that declares itself a
    #: correction is refused, and forwarded data changes only through whatever `overwrites` allows.
    OFF = "off"

    #: A submission carrying `status = "amended"` and naming the receipt it corrects is a correction,
    #: and the identity it lands on is the corrected receipt's rather than its own.
    AMEND = "amend"

WithdrawalPosture

Bases: StrEnum

Whether this deployment retracts from DHIS2 what one of its forwarded receipts landed.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class WithdrawalPosture(StrEnum):
    """Whether this deployment retracts from DHIS2 what one of its forwarded receipts landed."""

    #: Nothing this project forwarded is retracted. `d2w fhir withdraw` refuses, naming this key.
    OFF = "off"

    #: A forwarded receipt can be retracted, which deletes the object it landed in DHIS2 and files
    #: the receipt under `withdrawn/`. Terminal: DHIS2 burns the UID, so the receipt never forwards
    #: again - see `docs/fhir/design/data-lifecycle.md`.
    RETRACT = "retract"

ForwardConfig

Bases: BaseModel

How d2w fhir forward drains this project's spool - the [forward] table of fhir.toml.

live is what a project with no compiled guide forwards through. A drain reads the published Questionnaires and terminology to translate a receipt against, and a project that has run SUSHI has them on disk. A project captured through d2w fhir serve --live has never built anything, so the same documents are built off the instance instead - one full metadata read per drain, where a compiled guide costs a directory listing.

Left on, that read happens only when there is no compiled guide to read instead; a project that builds its IG never pays it. Turned off, a drain against a project with no compiled guide is refused and says which two commands produce one - which is the posture for a deployment that wants its forwards reading a reviewed, published guide and nothing else.

import is the posture every drain of this project runs in when the command line says nothing. False - the default - is a dry run: every payload still goes to the real endpoint under that endpoint's own validate-only mode, so DHIS2 decides the answer while nothing is written and no receipt moves. True makes the bare d2w fhir forward of this project commit, which is what a project whose drains are routine states once rather than remembering --import every time. The field is import_responses in Python because import is a Python keyword; the key in the file is import, and the file accepts no other spelling of it.

register_completeness is the second write an aggregate response asks for. An aggregate submission reporting itself completed marks its data set complete for the very tuple its values landed under, once DHIS2 has taken them. True - the default - honours that; false leaves the values imported and registers nothing, which is the posture for a deployment where completeness is somebody else's decision to record.

overwrites is what a drain does with an aggregate value a forwarded receipt of this spool already sent, arriving in a response that declares no correction. "allow" - the default - posts it and names it: DHIS2 keeps the newest number for a cell, and the toolkit follows the platform it writes into rather than inventing a stricter one. "refuse" posts no payload holding such a value; the response stays in the queue with a record naming every covered value and the receipt that sent it, which is the posture for a deployment where forwarded data changes only through a declared correction. The dial reaches aggregate values alone - a tracker event carries its own DHIS2 identity, so it collides rather than overwriting.

corrections and withdrawals are the other half of the same question, and they govern a marked submission where overwrites governs an unmarked one. A correction says which receipt it corrects; an overwrite says nothing and is simply a second capture of the same tuple. Both default to "off", because a deployment that publishes forms and forwards them is not thereby a deployment that lets a submitter reach back into what DHIS2 already holds. corrections = "amend" accepts a submission that names the receipt it amends and lands on that receipt's own DHIS2 identity; withdrawals = "retract" is what d2w fhir withdraw requires before it deletes anything. Withdrawal is terminal - DHIS2 burns the UID it deletes, so the withdrawn receipt can never be forwarded again - which is why the capability is stated rather than assumed.

A flag on the command line wins over all six keys for one run, and either key wins over these defaults - the same order [serve] states for its own dials.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class ForwardConfig(BaseModel):
    """How `d2w fhir forward` drains this project's spool - the `[forward]` table of `fhir.toml`.

    `live` is what a project with no compiled guide forwards through. A drain reads the published
    Questionnaires and terminology to translate a receipt against, and a project that has run SUSHI
    has them on disk. A project captured through `d2w fhir serve --live` has never built anything,
    so the same documents are built off the instance instead - one full metadata read per drain,
    where a compiled guide costs a directory listing.

    Left on, that read happens only when there is no compiled guide to read instead; a project that
    builds its IG never pays it. Turned off, a drain against a project with no compiled guide is
    refused and says which two commands produce one - which is the posture for a deployment that
    wants its forwards reading a reviewed, published guide and nothing else.

    `import` is the posture every drain of this project runs in when the command line says nothing.
    False - the default - is a dry run: every payload still goes to the real endpoint under that
    endpoint's own validate-only mode, so DHIS2 decides the answer while nothing is written and no
    receipt moves. True makes the bare `d2w fhir forward` of this project commit, which is what a
    project whose drains are routine states once rather than remembering `--import` every time. The
    field is `import_responses` in Python because `import` is a Python keyword; the key in the file
    is `import`, and the file accepts no other spelling of it.

    `register_completeness` is the second write an aggregate response asks for. An aggregate
    submission reporting itself `completed` marks its data set complete for the very tuple its
    values landed under, once DHIS2 has taken them. True - the default - honours that; false leaves
    the values imported and registers nothing, which is the posture for a deployment where
    completeness is somebody else's decision to record.

    `overwrites` is what a drain does with an aggregate value a forwarded receipt of this spool
    already sent, arriving in a response that declares no correction. `"allow"` - the default -
    posts it and names it: DHIS2 keeps the newest number for a cell, and the toolkit follows the
    platform it writes into rather than inventing a stricter one. `"refuse"` posts no payload
    holding such a value; the response stays in the queue with a record naming every covered value
    and the receipt that sent it, which is the posture for a deployment where forwarded data changes
    only through a declared correction. The dial reaches aggregate values alone - a tracker event
    carries its own DHIS2 identity, so it collides rather than overwriting.

    `corrections` and `withdrawals` are the other half of the same question, and they govern a
    *marked* submission where `overwrites` governs an unmarked one. A correction says which receipt
    it corrects; an overwrite says nothing and is simply a second capture of the same tuple. Both
    default to `"off"`, because a deployment that publishes forms and forwards them is not thereby a
    deployment that lets a submitter reach back into what DHIS2 already holds. `corrections =
    "amend"` accepts a submission that names the receipt it amends and lands on that receipt's own
    DHIS2 identity; `withdrawals = "retract"` is what `d2w fhir withdraw` requires before it deletes
    anything. Withdrawal is terminal - DHIS2 burns the UID it deletes, so the withdrawn receipt can
    never be forwarded again - which is why the capability is stated rather than assumed.

    A flag on the command line wins over all six keys for one run, and either key wins over these
    defaults - the same order `[serve]` states for its own dials.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    live: bool = True
    import_responses: bool = Field(default=False, alias="import")
    register_completeness: bool = True
    overwrites: OverwritePosture = OverwritePosture.ALLOW
    corrections: CorrectionPosture = CorrectionPosture.OFF
    withdrawals: WithdrawalPosture = WithdrawalPosture.OFF

IpsConfig

Bases: BaseModel

What this instance says about a person beyond their identifiers - the [ips] tables of fhir.toml.

[ips.identity] nominates the tracked entity attribute carrying a person's name, birth date, and sex, and maps that sex attribute's values onto R4's administrative-gender codes. DHIS2 holds no field that means any of those, so the nomination is the instance's own statement or there is nothing to publish - docs/fhir/design/ips.md section 4 is the argument, and section 9's phase 1 is what the nomination reaches: the register's own Patient.

[ips.sections] nominates which recorded values belong in which section of a patient summary, and [ips.sections.immunizations] is the one section phase 2 maps. Section 5 is the argument: DHIS2 marks no data element as an immunisation, so the section content of a summary is stated or it is absent.

enabled is the dial the whole summary surface hangs off, and it is false by default. A patient summary is a clinical document about a person, and publishing one is a decision a deployment makes rather than a default it inherits: d2w fhir serve answers $summary on a register whose subjects are people only where this key says so, and refuses it by name where it does not (R7 in section 8, "Gated and additive"). The register, the record, and everything else this facade serves are untouched either way - a summary is a new read over reads that already exist.

The tables are always present as tables of defaults, for the reason [serve.jwt] is: a project that nominates nothing is refused by the key's name rather than by a missing section, and its register answers exactly what it answered before they existed.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class IpsConfig(BaseModel):
    """What this instance says about a person beyond their identifiers - the `[ips]` tables of `fhir.toml`.

    `[ips.identity]` nominates the tracked entity attribute carrying a person's name, birth date, and
    sex, and maps that sex attribute's values onto R4's `administrative-gender` codes. DHIS2 holds no
    field that means any of those, so the nomination is the instance's own statement or there is
    nothing to publish - `docs/fhir/design/ips.md` section 4 is the argument, and section 9's phase 1
    is what the nomination reaches: the register's own `Patient`.

    `[ips.sections]` nominates which recorded values belong in which section of a patient summary,
    and `[ips.sections.immunizations]` is the one section phase 2 maps. Section 5 is the argument:
    DHIS2 marks no data element as an immunisation, so the section content of a summary is stated or
    it is absent.

    `enabled` is the dial the whole summary surface hangs off, and it is false by default. A patient
    summary is a clinical document about a person, and publishing one is a decision a deployment
    makes rather than a default it inherits: `d2w fhir serve` answers `$summary` on a register whose
    subjects are people only where this key says so, and refuses it by name where it does not
    (R7 in section 8, "Gated and additive"). The register, the record, and everything else this
    facade serves are untouched either way - a summary is a new read over reads that already exist.

    The tables are always present as tables of defaults, for the reason `[serve.jwt]` is: a project
    that nominates nothing is refused by the key's name rather than by a missing section, and its
    register answers exactly what it answered before they existed.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    enabled: bool = False
    sections: SectionMappings = Field(default_factory=SectionMappings)
    identity: IdentityNominations = Field(default_factory=IdentityNominations)

FhirProjectConfig

Bases: BaseModel

The full parsed fhir.toml document.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class FhirProjectConfig(BaseModel):
    """The full parsed `fhir.toml` document."""

    model_config = ConfigDict(extra="forbid")

    profile: str | None = None
    ig: IgConfig
    generate: GenerateConfig = Field(default_factory=GenerateConfig)
    serve: ServeConfig = Field(default_factory=ServeConfig)
    forward: ForwardConfig = Field(default_factory=ForwardConfig)
    ips: IpsConfig = Field(default_factory=IpsConfig)

FhirProject

Bases: BaseModel

A discovered FHIR IG project: parsed config plus where it lives on disk.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
class FhirProject(BaseModel):
    """A discovered FHIR IG project: parsed config plus where it lives on disk."""

    model_config = ConfigDict(frozen=True)

    config: FhirProjectConfig
    config_path: Path

    @property
    def project_root(self) -> Path:
        """Directory containing `fhir.toml`."""
        return self.config_path.parent

    @property
    def ig_directory(self) -> Path:
        """The SUSHI IG directory (`<project_root>/ig`)."""
        return self.project_root / "ig"

    @property
    def fsh_directory(self) -> Path:
        """The FSH source directory (`<project_root>/ig/input/fsh`)."""
        return self.ig_directory / "input" / "fsh"

    @property
    def resources_directory(self) -> Path:
        """The predefined-resource directory (`<project_root>/ig/input/resources`), loaded without a FSH compile."""
        return self.ig_directory / "input" / "resources"
Attributes
project_root property

Directory containing fhir.toml.

ig_directory property

The SUSHI IG directory (<project_root>/ig).

fsh_directory property

The FSH source directory (<project_root>/ig/input/fsh).

resources_directory property

The predefined-resource directory (<project_root>/ig/input/resources), loaded without a FSH compile.

Functions:

basemaps_from_options(values)

Read repeated --basemap values as the layers a run offers, or refuse the ones that say nothing.

Each value is either Name=https://.../{z}/{x}/{y}.png or a bare template, whose host becomes the layer's name - the honest word for a source this project was handed and knows nothing else about. The split is on the first = and only when what precedes it is a plain word: a template carrying ?api_key=... is one url, not a name and a url.

The single value none serves no layers, which is the command line's way of saying what basemaps = [] says in the table. Naming it beside a real layer is a contradiction rather than a shorthand, so it is refused instead of guessed at.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def basemaps_from_options(values: list[str]) -> list[BasemapSource]:
    """Read repeated `--basemap` values as the layers a run offers, or refuse the ones that say nothing.

    Each value is either `Name=https://.../{z}/{x}/{y}.png` or a bare template, whose host becomes
    the layer's name - the honest word for a source this project was handed and knows nothing else
    about. The split is on the first `=` and only when what precedes it is a plain word: a template
    carrying `?api_key=...` is one url, not a name and a url.

    The single value `none` serves no layers, which is the command line's way of saying what
    `basemaps = []` says in the table. Naming it beside a real layer is a contradiction rather than
    a shorthand, so it is refused instead of guessed at.
    """
    disabled = [value for value in values if value.strip().lower() == BASEMAP_DISABLED]
    if disabled and len(values) > 1:
        raise ValueError(
            f"--basemap {BASEMAP_DISABLED} serves no layers at all, so it cannot be combined with "
            f"{len(values) - len(disabled)} other --basemap value(s): pass one or the other"
        )
    if disabled:
        return []
    return [_basemap_from_option(value) for value in values]

find_project_fhir_config(start=None)

Walk up from start (defaulting to $PWD) looking for fhir.toml.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def find_project_fhir_config(start: Path | None = None) -> Path | None:
    """Walk up from `start` (defaulting to `$PWD`) looking for `fhir.toml`."""
    cwd = start or Path.cwd()
    for parent in [cwd, *cwd.parents]:
        candidate = parent / FHIR_CONFIG_FILENAME
        if candidate.exists():
            return candidate
    return None

load_fhir_config(path)

Parse and validate a fhir.toml file, refusing any key the document does not declare.

Every table declares its full key set, so a misspelled option is a refusal rather than a line that sets nothing: the key is named, placed in its table, and matched against the names that table accepts, and every unknown key in the file is reported in one pass. A refusal about a value rather than a name (a wrong type, a value outside its range) keeps pydantic's own report.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def load_fhir_config(path: Path) -> FhirProjectConfig:
    """Parse and validate a `fhir.toml` file, refusing any key the document does not declare.

    Every table declares its full key set, so a misspelled option is a refusal rather than a line
    that sets nothing: the key is named, placed in its table, and matched against the names that
    table accepts, and every unknown key in the file is reported in one pass. A refusal about a
    value rather than a name (a wrong type, a value outside its range) keeps pydantic's own report.
    """
    try:
        raw = tomllib.loads(path.read_text(encoding="utf-8"))
    except tomllib.TOMLDecodeError as error:
        raise MalformedFhirConfigError(f"{path}: not valid TOML - {error}") from error
    try:
        return FhirProjectConfig.model_validate(raw)
    except ValidationError as error:
        unknown_keys = [item["loc"] for item in error.errors() if item["type"] == "extra_forbidden"]
        if not unknown_keys:
            raise
        # Sorted by location so the diagnostics read as an outline of the document - a table before
        # its sub-tables - rather than in the order the validators happened to reach them.
        ordered = sorted(unknown_keys, key=lambda location: [str(segment) for segment in location])
        raise UnknownFhirConfigKeyError(*(_unknown_key_diagnostic(location) for location in ordered)) from error

write_fhir_config(path, config)

Write a fhir.toml with default permissions - it is committed project config, not a credential store.

Written under the keys the file declares rather than the Python field names, so what is written is what load_fhir_config reads back: [forward] import is import_responses in Python only because the file's own name for it is a keyword there.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def write_fhir_config(path: Path, config: FhirProjectConfig) -> None:
    """Write a `fhir.toml` with default permissions - it is committed project config, not a credential store.

    Written under the keys the file declares rather than the Python field names, so what is written
    is what `load_fhir_config` reads back: `[forward] import` is `import_responses` in Python only
    because the file's own name for it is a keyword there.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(tomli_w.dumps(config.model_dump(exclude_none=True, by_alias=True)), encoding="utf-8")

load_project(start=None)

Discover and load the nearest FHIR project, raising NoFhirProjectError when there is none.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
def load_project(start: Path | None = None) -> FhirProject:
    """Discover and load the nearest FHIR project, raising `NoFhirProjectError` when there is none."""
    path = find_project_fhir_config(start)
    if path is None:
        raise NoFhirProjectError(
            "no fhir.toml found in this directory or any parent. "
            "Run `d2w fhir init [DIRECTORY]` to scaffold a FHIR IG project first."
        )
    return FhirProject(config=load_fhir_config(path), config_path=path.resolve())

The patient summary

The [ips] tables and the document they reach. dhis2w_fhir.ips holds the two nominations an instance makes about a person - which tracked entity attribute carries their name, birth date, and sex, and which recorded values are doses - because DHIS2 states neither and a guess would be indistinguishable from a fact. dhis2w_fhir.summary assembles those into the IPS document Bundle, as a pure function of what a caller already read, and states in the document itself what it is and is not. The section mapping is published beside the vocabularies it maps, as D2Section_CM, so a consumer audits the assignment without ever holding the project's fhir.toml - Terminology and ConceptMaps covers the shape, and build_section_concept_map and build_section_concept_map_artifacts are the builders behind it.

ips

The [ips] tables: which attribute carries a person's identity, and which stage data is a dose.

docs/fhir/design/ips.md sections 4 and 5 are the argument in full, and they are one argument made twice. DHIS2 has no name field, no sex field, and no date-of-birth field, and it marks no data element as an immunisation, a problem, or an allergy. Which of an instance's tracked entity attributes mean those demographic things, and which of its data elements belong in which section of a patient summary, are decisions every instance makes for itself, usually differently. So both are nominated or both are nothing, and this module holds the two nominations and the reading of them.

[ips.identity] fills Patient.name, Patient.birthDate, and Patient.gender on the register projection, which is what section 9's phase 1 asked of it - before any summary document exists. [ips.sections] says which recorded values a summary's clinical sections carry, and phase 2 maps exactly one section: Immunizations. A section with no table is a section this project maps nothing into, and the document states that rather than inventing content for it (dhis2w_fhir.summary).

Only UIDs, never names. Attribute names are not unique in DHIS2 and change without notice, and the guide already publishes the names it reads off the instance as D2TEA_CS.

Honest failure per person, not per instance (section 4, "Honest failure per person"). An instance-wide nomination is a statement about the attribute, not a promise about every row: a person the instance holds no birth date for keeps the required element and states its absence on the data-absent-reason extension, which is the IG's own worked example. A person whose birth date is a string this server cannot read as a date states the same absence under error rather than unknown, because "nobody recorded one" and "what was recorded is not a date" are different answers and a summary that flattened them would be less true than the instance is.

name and gender state no absence, because neither is a required element on the resource the register serves: a HumanName carrying nothing but a data-absent extension satisfies no reader and no invariant, and Patient.gender is 0..1. What the instance holds is never lost either way - the raw attribute value rides the D2TrackedEntityAttributeValue extension exactly as it always has, so a nomination adds a reading of a value and removes nothing.

Classes

IdentityNominations

Bases: BaseModel

Which tracked entity attribute carries which demographic fact - the [ips.identity] table.

Three attributes are nominated by UID and a fourth key states what a sex value means. administrative_gender maps the value DHIS2 stores against the sex attribute - the option's DHIS2 code on an option-set-bound attribute - onto one of R4's four administrative-gender codes. It is a map rather than a rename because the binding on Patient.gender is required, and it is the smallest possible instance of the clinical-vocabulary source docs/fhir/design/ips.md section 3 says does not exist yet: four codes rather than forty thousand.

name publishes as Patient.name[0].text and nothing else. A single text name satisfies the IPS invariant ips-pat-1, which asks for family, given, or text, and an instance whose given and family names sit in two attributes states the one it wants read rather than having this project guess which half is which.

An empty table nominates nothing, which is what every project that never wrote it states: the register serves the identity it always served, which is none.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
class IdentityNominations(BaseModel):
    """Which tracked entity attribute carries which demographic fact - the `[ips.identity]` table.

    Three attributes are nominated by UID and a fourth key states what a sex value means.
    `administrative_gender` maps the value DHIS2 stores against the `sex` attribute - the option's
    DHIS2 code on an option-set-bound attribute - onto one of R4's four `administrative-gender`
    codes. It is a map rather than a rename because the binding on `Patient.gender` is required, and
    it is the smallest possible instance of the clinical-vocabulary source `docs/fhir/design/ips.md`
    section 3 says does not exist yet: four codes rather than forty thousand.

    `name` publishes as `Patient.name[0].text` and nothing else. A single text name satisfies the IPS
    invariant `ips-pat-1`, which asks for `family`, `given`, **or** `text`, and an instance whose
    given and family names sit in two attributes states the one it wants read rather than having this
    project guess which half is which.

    An empty table nominates nothing, which is what every project that never wrote it states: the
    register serves the identity it always served, which is none.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    name: str | None = None
    """The tracked entity attribute whose value is published as `Patient.name[0].text`."""

    birth_date: str | None = None
    """The tracked entity attribute whose value is published as `Patient.birthDate`."""

    sex: str | None = None
    """The tracked entity attribute whose value `administrative_gender` reads as `Patient.gender`."""

    administrative_gender: dict[str, str] = Field(default_factory=dict)
    """One DHIS2 value of the `sex` attribute per key, mapped onto `male`, `female`, `other`, or `unknown`."""

    @field_validator("name", "birth_date", "sex")
    @classmethod
    def _dhis2_uid(cls, value: str | None) -> str | None:
        """Every nomination names a DHIS2 object by UID - a name or a code here would nominate nothing."""
        if value is None:
            return value
        if not is_dhis2_uid(value):
            raise ValueError(
                f"{value!r} is not a DHIS2 UID (one letter followed by ten alphanumeric places): "
                "nominate the tracked entity attribute by its UID, since names and codes are not "
                "unique in DHIS2 and change without notice"
            )
        return value

    @field_validator("administrative_gender")
    @classmethod
    def _administrative_gender_codes(cls, value: dict[str, str]) -> dict[str, str]:
        """`Patient.gender` takes four codes and no others, so a fifth word here would map onto nothing."""
        for dhis2_value, gender in value.items():
            if gender not in ADMINISTRATIVE_GENDER_CODES:
                raise ValueError(
                    f"the DHIS2 value {dhis2_value!r} is mapped to {gender!r}, which is not one of R4's "
                    f"administrative-gender codes: name one of {', '.join(ADMINISTRATIVE_GENDER_CODES)}"
                )
        return value

    @model_validator(mode="after")
    def _sex_and_its_map_arrive_together(self) -> IdentityNominations:
        """One without the other states a gender nobody can serve, so the file refuses the pair broken.

        A nominated `sex` with an empty map reads every person's value as unmapped and publishes no
        `gender` at all; a map with no `sex` names values of an attribute nobody nominated. Both
        parse and neither does anything, which is the failure mode `ServeAuth` keeps `oauth2` out of
        the enum to avoid.
        """
        if self.sex is not None and not self.administrative_gender:
            raise ValueError(
                "sex nominates a tracked entity attribute and [ips.identity.administrative_gender] maps "
                "nothing: state one line per value the attribute holds, mapped onto "
                f"{', '.join(ADMINISTRATIVE_GENDER_CODES)}"
            )
        if self.sex is None and self.administrative_gender:
            raise ValueError(
                "[ips.identity.administrative_gender] maps values of an attribute nobody nominated: "
                'state `sex = "<attribute uid>"` beside it, or drop the map'
            )
        return self

    def nominates_anything(self) -> bool:
        """Whether this table nominates a single attribute, which is what every caller asks first."""
        return any((self.name, self.birth_date, self.sex))

    def nominated_attribute_uids(self) -> tuple[str, ...]:
        """Every attribute this table nominates, once each, in the order the keys are declared."""
        return tuple(dict.fromkeys(uid for uid in (self.name, self.birth_date, self.sex) if uid is not None))
Attributes
name = None class-attribute instance-attribute

The tracked entity attribute whose value is published as Patient.name[0].text.

birth_date = None class-attribute instance-attribute

The tracked entity attribute whose value is published as Patient.birthDate.

sex = None class-attribute instance-attribute

The tracked entity attribute whose value administrative_gender reads as Patient.gender.

administrative_gender = Field(default_factory=dict) class-attribute instance-attribute

One DHIS2 value of the sex attribute per key, mapped onto male, female, other, or unknown.

Methods:
nominates_anything()

Whether this table nominates a single attribute, which is what every caller asks first.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def nominates_anything(self) -> bool:
    """Whether this table nominates a single attribute, which is what every caller asks first."""
    return any((self.name, self.birth_date, self.sex))
nominated_attribute_uids()

Every attribute this table nominates, once each, in the order the keys are declared.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def nominated_attribute_uids(self) -> tuple[str, ...]:
    """Every attribute this table nominates, once each, in the order the keys are declared."""
    return tuple(dict.fromkeys(uid for uid in (self.name, self.birth_date, self.sex) if uid is not None))

ImmunizationsMapping

Bases: BaseModel

Which recorded values are doses - the [ips.sections.immunizations] table.

docs/fhir/design/ips.md section 6 puts the Immunizations row at WITH A MAPPING and says what the mapping has to state: dose events in an immunisation program stage, with the event's own date as the occurrence. This table states exactly that, in two lists.

program_stages names the stages whose events record doses. dose_data_elements names the data elements inside them that each record a dose of one vaccine - which is the shape a DHIS2 immunisation form actually has: MCH BCG dose, MCH Measles dose, MCH Penta dose, one element per vaccine, the value saying that a dose was given or which dose of the series it was. So the data element is the vaccine and the value is the dose, and Immunization.vaccineCode carries the data element's own DHIS2 coding. The IPS binds vaccineCode preferably rather than requiredly, so publishing a DHIS2 coding there violates no profile - which is what lets this section carry real doses while an international vaccine vocabulary is still missing (section 5).

Both lists are required together. A stage with no data element nominated records nothing this reads, and a data element with no stage names values on events nobody said were doses; either alone maps nothing, which is the failure mode [ips.identity] refuses the same way.

An empty table maps nothing, which is what every project that never wrote it states.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
class ImmunizationsMapping(BaseModel):
    """Which recorded values are doses - the `[ips.sections.immunizations]` table.

    `docs/fhir/design/ips.md` section 6 puts the Immunizations row at `WITH A MAPPING` and says what
    the mapping has to state: dose events in an immunisation program stage, with the event's own date
    as the occurrence. This table states exactly that, in two lists.

    `program_stages` names the stages whose events record doses. `dose_data_elements` names the data
    elements inside them that each record a dose of one vaccine - which is the shape a DHIS2
    immunisation form actually has: `MCH BCG dose`, `MCH Measles dose`, `MCH Penta dose`, one element
    per vaccine, the value saying that a dose was given or which dose of the series it was. So **the
    data element is the vaccine and the value is the dose**, and `Immunization.vaccineCode` carries
    the data element's own DHIS2 coding. The IPS binds `vaccineCode` **preferably** rather than
    requiredly, so publishing a DHIS2 coding there violates no profile - which is what lets this
    section carry real doses while an international vaccine vocabulary is still missing (section 5).

    Both lists are required together. A stage with no data element nominated records nothing this
    reads, and a data element with no stage names values on events nobody said were doses; either
    alone maps nothing, which is the failure mode `[ips.identity]` refuses the same way.

    An empty table maps nothing, which is what every project that never wrote it states.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    program_stages: tuple[str, ...] = ()
    """The program stages whose events carry doses, by UID."""

    dose_data_elements: tuple[str, ...] = ()
    """The data elements inside those stages that each record a dose of one vaccine, by UID."""

    @field_validator("program_stages", "dose_data_elements")
    @classmethod
    def _dhis2_uids(cls, value: tuple[str, ...]) -> tuple[str, ...]:
        """Every nomination names a DHIS2 object by UID - a name or a code here would nominate nothing."""
        for stated in value:
            if not is_dhis2_uid(stated):
                raise ValueError(
                    f"{stated!r} is not a DHIS2 UID (one letter followed by ten alphanumeric places): "
                    "nominate the object by its UID, since names and codes are not unique in DHIS2 and "
                    "change without notice"
                )
        return value

    @model_validator(mode="after")
    def _stages_and_elements_arrive_together(self) -> ImmunizationsMapping:
        """One list without the other maps no dose at all, so the file refuses the pair broken."""
        if self.program_stages and not self.dose_data_elements:
            raise ValueError(
                "[ips.sections.immunizations] program_stages names a stage and dose_data_elements names "
                "no data element: state which of that stage's data elements each record a dose"
            )
        if self.dose_data_elements and not self.program_stages:
            raise ValueError(
                "[ips.sections.immunizations] dose_data_elements names a data element and program_stages "
                "names no stage: state which program stage's events those values are recorded on"
            )
        return self

    def maps_anything(self) -> bool:
        """Whether this table maps a single dose, which is what every caller asks first."""
        return bool(self.program_stages and self.dose_data_elements)

    def records_dose(self, program_stage_uid: str | None, data_element_uid: str) -> bool:
        """Whether one value of one stage's event is a dose this table nominated."""
        if program_stage_uid is None:
            return False
        return program_stage_uid in self.program_stages and data_element_uid in self.dose_data_elements
Attributes
program_stages = () class-attribute instance-attribute

The program stages whose events carry doses, by UID.

dose_data_elements = () class-attribute instance-attribute

The data elements inside those stages that each record a dose of one vaccine, by UID.

Methods:
maps_anything()

Whether this table maps a single dose, which is what every caller asks first.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def maps_anything(self) -> bool:
    """Whether this table maps a single dose, which is what every caller asks first."""
    return bool(self.program_stages and self.dose_data_elements)
records_dose(program_stage_uid, data_element_uid)

Whether one value of one stage's event is a dose this table nominated.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def records_dose(self, program_stage_uid: str | None, data_element_uid: str) -> bool:
    """Whether one value of one stage's event is a dose this table nominated."""
    if program_stage_uid is None:
        return False
    return program_stage_uid in self.program_stages and data_element_uid in self.dose_data_elements

SectionMappings

Bases: BaseModel

Which recorded values belong in which section of a summary - the [ips.sections] tables.

One sub-table per IPS section this project maps, and immunizations is the only one phase 2 ships: docs/fhir/design/ips.md section 9 says why it goes first, and section 6 says why every other section needs its own nomination rather than a general rule. A section named here that this version does not map is refused by name, so a project writing [ips.sections.problems] today is told the key is not one rather than left with a table that quietly does nothing.

An absent table maps no section at all. The summary is still served - the owner's call, and dhis2w_fhir.summary states the caveat that goes with it - with its three required sections carrying an empty reason and its recommended sections omitted.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
class SectionMappings(BaseModel):
    """Which recorded values belong in which section of a summary - the `[ips.sections]` tables.

    One sub-table per IPS section this project maps, and `immunizations` is the only one phase 2
    ships: `docs/fhir/design/ips.md` section 9 says why it goes first, and section 6 says why every
    other section needs its own nomination rather than a general rule. A section named here that this
    version does not map is refused by name, so a project writing `[ips.sections.problems]` today is
    told the key is not one rather than left with a table that quietly does nothing.

    An absent table maps no section at all. The summary is still served - the owner's call, and
    `dhis2w_fhir.summary` states the caveat that goes with it - with its three required sections
    carrying an empty reason and its recommended sections omitted.
    """

    model_config = ConfigDict(frozen=True, extra="forbid")

    immunizations: ImmunizationsMapping = Field(default_factory=ImmunizationsMapping)

    def maps_anything(self) -> bool:
        """Whether a single clinical section of a summary is mapped, which decides the document's caveat."""
        return self.immunizations.maps_anything()
Methods:
maps_anything()

Whether a single clinical section of a summary is mapped, which decides the document's caveat.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def maps_anything(self) -> bool:
    """Whether a single clinical section of a summary is mapped, which decides the document's caveat."""
    return self.immunizations.maps_anything()

ServedIdentity

Bases: BaseModel

The demographic elements one nomination fills on one person, and the absences it states.

The field names are the FHIR element names on purpose: a caller carries this straight onto the resource it is serving, so what this decides and what a client reads cannot drift apart.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
class ServedIdentity(BaseModel):
    """The demographic elements one nomination fills on one person, and the absences it states.

    The field names are the FHIR element names on purpose: a caller carries this straight onto the
    resource it is serving, so what this decides and what a client reads cannot drift apart.
    """

    model_config = ConfigDict(frozen=True)

    name: list[HumanName] | None = None
    gender: AdministrativeGender | None = None
    birth_date: str | None = None
    birth_date_element: Element | None = None
    """The `_birthDate` sibling carrying the data-absent-reason extension, when the date is absent."""
Attributes
birth_date_element = None class-attribute instance-attribute

The _birthDate sibling carrying the data-absent-reason extension, when the date is absent.

NominatedValueTypeIssue

Bases: BaseModel

One nominated attribute whose published DHIS2 value type is not one the FHIR element accepts.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
class NominatedValueTypeIssue(BaseModel):
    """One nominated attribute whose published DHIS2 value type is not one the FHIR element accepts."""

    model_config = ConfigDict(frozen=True)

    key: str
    attribute_uid: str
    value_type: str
    accepted: tuple[str, ...]

    def message(self) -> str:
        """The refusal a run states, naming the key, the attribute, and the value type it found."""
        return (
            f"[ips.identity] {self.key} nominates tracked entity attribute {self.attribute_uid}, which this "
            f"guide publishes as DHIS2 value type {self.value_type}: the FHIR element it fills takes "
            f"{', '.join(self.accepted)}. Nominate an attribute of that type, or drop the key."
        )
Methods:
message()

The refusal a run states, naming the key, the attribute, and the value type it found.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def message(self) -> str:
    """The refusal a run states, naming the key, the attribute, and the value type it found."""
    return (
        f"[ips.identity] {self.key} nominates tracked entity attribute {self.attribute_uid}, which this "
        f"guide publishes as DHIS2 value type {self.value_type}: the FHIR element it fills takes "
        f"{', '.join(self.accepted)}. Nominate an attribute of that type, or drop the key."
    )

Functions:

nominated_value_type_issues(nominations, value_types)

Check every nomination against the value type D2TEA_CS publishes for it, in key order.

docs/fhir/design/ips.md section 4, "Value-shape validation": a nomination whose value type cannot fill the element it was nominated for refuses the run, in the manner [generate.tracked_entity_types] refuses a resource type that is not one.

An attribute the guide publishes nothing about raises no issue. The guide's silence is not evidence of a wrong type - it means the attribute is outside this project's selection - and [serve.tracked_entities] search_attributes already settles that an operator's nomination of an attribute outstates what the vocabulary happens to carry about it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def nominated_value_type_issues(
    nominations: IdentityNominations, value_types: Mapping[str, str]
) -> list[NominatedValueTypeIssue]:
    """Check every nomination against the value type `D2TEA_CS` publishes for it, in key order.

    `docs/fhir/design/ips.md` section 4, "Value-shape validation": a nomination whose value type
    cannot fill the element it was nominated for refuses the run, in the manner
    `[generate.tracked_entity_types]` refuses a resource type that is not one.

    An attribute the guide publishes nothing about raises no issue. The guide's silence is not
    evidence of a wrong type - it means the attribute is outside this project's selection - and
    `[serve.tracked_entities] search_attributes` already settles that an operator's nomination of an
    attribute outstates what the vocabulary happens to carry about it.
    """
    issues: list[NominatedValueTypeIssue] = []
    for key, accepted in NOMINATION_VALUE_TYPES.items():
        attribute_uid = getattr(nominations, key)
        if attribute_uid is None:
            continue
        value_type = value_types.get(attribute_uid)
        if value_type is None or value_type in accepted:
            continue
        issues.append(
            NominatedValueTypeIssue(key=key, attribute_uid=attribute_uid, value_type=value_type, accepted=accepted)
        )
    return issues

served_identity(values, nominations)

Read one person's nominated attribute values as the demographic elements they were nominated for.

values is that person's attribute values keyed by attribute UID. A nomination the person holds no value for, and a value this server cannot read, are both per-person facts rather than instance-wide ones - see this module's docstring for which of them states what.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
def served_identity(values: Mapping[str, str], nominations: IdentityNominations) -> ServedIdentity:
    """Read one person's nominated attribute values as the demographic elements they were nominated for.

    `values` is that person's attribute values keyed by attribute UID. A nomination the person holds
    no value for, and a value this server cannot read, are both per-person facts rather than
    instance-wide ones - see this module's docstring for which of them states what.
    """
    return ServedIdentity(
        name=_served_name(_stated(values, nominations.name)),
        gender=_served_gender(_stated(values, nominations.sex), nominations.administrative_gender),
        birth_date=_served_birth_date(_stated(values, nominations.birth_date)),
        birth_date_element=_birth_date_absence(nominations.birth_date, _stated(values, nominations.birth_date)),
    )

summary

One person's summary, assembled: the IPS document this toolchain can honestly build about them.

WHAT AN IPS IS, AND WHAT THIS BUILDS. One FHIR document: a Bundle whose first entry is a Composition and whose remaining entries are the resources that Composition's sections point at. Bundle-uv-ips fixes type to document, requires identifier and timestamp, and carries the invariant bdl-ips-1 - an IPS document has no Composition besides the first. Composition-uv-ips pins type to the LOINC pattern 60591-5, constrains subject to one Patient, and sets section to 3..* with title, code, and text required on every section present. This module builds exactly that, and docs/fhir/design/ips.md is the argument behind every choice in it.

IT IS A LIBRARY AND NOT A ROUTE. Everything below is a pure function of what somebody already read: the person as the register projects them, and the doses as the record surface projected them. dhis2w_fhir_serve.routes.summary is one caller and d2w fhir is free to be another - nothing here opens a connection, reads a store, or knows what a request is.

THREE REQUIRED SECTIONS, AND WHAT THEY SAY WHEN NOTHING IS MAPPED. Problems, Allergies and Intolerances, and Medication Summary are the sections the IPS puts a SHALL:populate obligation on the Creator actor for, and this project maps none of them (docs/fhir/design/ips.md section 6: Allergies is HONESTLY EMPTY, the other two are WITH A MAPPING nobody has written yet). So each carries Composition.section.emptyReason, which is what the invariant ips-comp-1 accepts in place of an entry, with the unavailable code out of R4's own list-empty-reason. Nothing is invented to fill them: an unmapped stage does not become a free-text Observation and an unmapped element does not get swept into Results on the grounds that it was numeric.

THE ONE MAPPED SECTION IS IMMUNIZATIONS. [ips.sections.immunizations] says which program stages record doses and which of their data elements each record a dose of one vaccine, and every entry here traces to a line somebody wrote in that table. A project that maps it and a person who has no dose recorded are different facts and read differently: the section is present with an empty reason for the person, and absent entirely for the project.

THE CAVEAT IS PART OF THE DOCUMENT. The IG says in as many words that a system that can never populate the three obligated sections "can produce valid IPS Bundle instances, although it cannot comply with the Creator (IPS) actor obligations" - two claims, and this toolchain has to be able to make each separately (R5). So the document says so itself, in Composition.text: R4's own human-readable rendering of a resource, which every reader of a document sees and which survives being saved to a file. summary_caveat is the same sentence as text, for a caller that states it beside the response as well - the two-place idiom dhis2w_fhir_serve.projection.serving argues for the projection's as-of instant, and for the same reason: one fact, stated where resources are read and stated again where responses are.

DETERMINISTIC (R4 in section 8). Two assemblies of an unchanged record differ in Bundle.timestamp and Composition.date and in nothing else: the sections are in a fixed order, the doses are in the record's own order, and every id is derived from a DHIS2 identifier through uuid5 rather than minted per call.

NO PROFILE IS CLAIMED. meta.profile names none of the IPS StructureDefinitions, because live serve mode publishes no StructureDefinitions and resolves none (R8): the IG is a vocabulary this document conforms to, not a dependency the generated country guide takes on.

Classes

IpsSection

Bases: BaseModel

One section of the summary: its LOINC code, its title, and the display that code carries.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
class IpsSection(BaseModel):
    """One section of the summary: its LOINC code, its title, and the display that code carries."""

    model_config = ConfigDict(frozen=True)

    code: str
    title: str
    display: str

    def concept(self) -> CodeableConcept:
        """The section code as the IG's own section table spells it."""
        return CodeableConcept(coding=[Coding(system=LOINC_SYSTEM, code=self.code, display=self.display)])
Methods:
concept()

The section code as the IG's own section table spells it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
def concept(self) -> CodeableConcept:
    """The section code as the IG's own section table spells it."""
    return CodeableConcept(coding=[Coding(system=LOINC_SYSTEM, code=self.code, display=self.display)])

RecordedDose

Bases: BaseModel

One dose one recorded event holds: which vaccine, when, and which dose of the series.

Read off the record the facade already serves rather than off DHIS2 a second way: the caller projects one tracked entity's events through the very machinery GET /facade/tracked-entities/{uid}/events answers with, and hands the doses in it here. display is the data element's name as the guide publishes it, and dose_number is the value DHIS2 stored where that value names a dose - Dose 2, IPT 1 - and nothing where the value is a plain statement that the dose was given.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
class RecordedDose(BaseModel):
    """One dose one recorded event holds: which vaccine, when, and which dose of the series.

    Read off the record the facade already serves rather than off DHIS2 a second way: the caller
    projects one tracked entity's events through the very machinery
    `GET /facade/tracked-entities/{uid}/events` answers with, and hands the doses in it here. `display` is
    the data element's name as the guide publishes it, and `dose_number` is the value DHIS2 stored
    where that value names a dose - `Dose 2`, `IPT 1` - and nothing where the value is a plain
    statement that the dose was given.
    """

    model_config = ConfigDict(frozen=True)

    event_uid: str
    data_element_uid: str
    display: str | None = None
    occurred_at: str | None = None
    """When the event occurred, as an R4 `dateTime`, or nothing where the instance dated it unreadably."""

    dose_number: str | None = None
Attributes
occurred_at = None class-attribute instance-attribute

When the event occurred, as an R4 dateTime, or nothing where the instance dated it unreadably.

AssembledSummary

Bases: BaseModel

One assembled summary: the document, and the honest statement that goes beside it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
class AssembledSummary(BaseModel):
    """One assembled summary: the document, and the honest statement that goes beside it."""

    model_config = ConfigDict(frozen=True)

    bundle: Bundle
    caveat: str
    """What this document is and is not, in one sentence - the same words `Composition.text` carries."""

    creator_conformant: bool = False
    """Whether the document claims the Creator (IPS) actor's obligations, which today it never does.

    The three obligated sections are unmapped in every project this toolchain serves, so the answer
    is false and the caveat says so. It is a field rather than a constant because R5 asks this
    project to state validity and conformance as two separate claims, and a caller reading one
    should not have to infer the other from a sentence.
    """
Attributes
caveat instance-attribute

What this document is and is not, in one sentence - the same words Composition.text carries.

creator_conformant = False class-attribute instance-attribute

Whether the document claims the Creator (IPS) actor's obligations, which today it never does.

The three obligated sections are unmapped in every project this toolchain serves, so the answer is false and the caveat says so. It is a field rather than a constant because R5 asks this project to state validity and conformance as two separate claims, and a caller reading one should not have to infer the other from a sentence.

Functions:

summary_caveat(*, mapped_sections, dose_count)

The one sentence a summary states about itself, in the document and beside the response.

Two situations and two sentences, because they are different facts and a reader has to be able to tell them apart: a summary with a mapped section carrying real content, and a summary whose clinical sections are all empty. Both are valid IPS documents and neither is conformance with the Creator (IPS) actor, which is what the closing clause states in both.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
def summary_caveat(*, mapped_sections: Sequence[str], dose_count: int) -> str:
    """The one sentence a summary states about itself, in the document and beside the response.

    Two situations and two sentences, because they are different facts and a reader has to be able
    to tell them apart: a summary with a mapped section carrying real content, and a summary whose
    clinical sections are all empty. Both are valid IPS documents and neither is conformance with
    the Creator (IPS) actor, which is what the closing clause states in both.
    """
    obligated = ", ".join(section.title for section in REQUIRED_SECTIONS)
    conformance = "This document is a valid IPS Bundle and does not claim the Creator (IPS) actor's obligations."
    if not mapped_sections:
        return (
            f"No clinical section of this summary is mapped. {obligated} are the three sections the IPS "
            "requires, and each states an empty reason rather than carrying content nobody nominated: "
            "DHIS2 marks no data element as a problem, an allergy, or a medication, and this project has "
            f"nominated none. {conformance}"
        )
    mapped = ", ".join(mapped_sections)
    return (
        f"{mapped} is mapped and carries {pluralize(dose_count, 'dose')} read from this "
        f"person's own record. {obligated} are the three sections the IPS requires, and each states an "
        "empty reason rather than carrying content nobody nominated: DHIS2 marks no data element as a "
        f"problem, an allergy, or a medication, and this project has nominated none. {conformance}"
    )

build_patient_summary(subject, doses, *, vaccine_code_system, assembled_at, author_display, immunizations_mapped=False, unpublished_stage_uids=())

Assemble one person's summary out of what the register and the record already say about them.

subject is the very resource GET /Patient/{uid} answers with, carried into the document unchanged, so the person a summary is about and the person the register serves can never disagree. doses is what the record projected; vaccine_code_system is the DHIS2 data-element identifier namespace their vaccine codes are stated under - the same namespace the published section map maps out of, so the document and the map name one thing one way.

immunizations_mapped is the project's own word rather than something inferred from doses being empty: a mapped section with no dose is a statement about this person, and an unmapped one is a statement about the project, and the summary has to make them differently.

unpublished_stage_uids names the mapped stages this guide publishes no form for, which the section states in its own narrative. A guide narrower than its mapping and a person who was never vaccinated produce the same count of doses and are not the same fact.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
def build_patient_summary(
    subject: RegisteredEntity,
    doses: Sequence[RecordedDose],
    *,
    vaccine_code_system: str,
    assembled_at: str,
    author_display: str,
    immunizations_mapped: bool = False,
    unpublished_stage_uids: Sequence[str] = (),
) -> AssembledSummary:
    """Assemble one person's summary out of what the register and the record already say about them.

    `subject` is the very resource `GET /Patient/{uid}` answers with, carried into the document
    unchanged, so the person a summary is about and the person the register serves can never
    disagree. `doses` is what the record projected; `vaccine_code_system` is the DHIS2 data-element
    identifier namespace their vaccine codes are stated under - the same namespace the published
    section map maps out of, so the document and the map name one thing one way.

    `immunizations_mapped` is the project's own word rather than something inferred from `doses`
    being empty: a mapped section with no dose is a statement about this person, and an unmapped one
    is a statement about the project, and the summary has to make them differently.

    `unpublished_stage_uids` names the mapped stages this guide publishes no form for, which the
    section states in its own narrative. A guide narrower than its mapping and a person who was
    never vaccinated produce the same count of doses and are not the same fact.
    """
    tracked_entity_uid = subject.id or ""
    subject_reference = Reference(reference=_urn(subject.resourceType, tracked_entity_uid))
    immunizations = [_immunization(dose, subject_reference, vaccine_code_system) for dose in doses]
    sections = [_empty_section(section) for section in REQUIRED_SECTIONS]
    if immunizations_mapped:
        sections.append(_immunizations_section(immunizations, unpublished_stage_uids))
    mapped_sections = [IMMUNIZATIONS_SECTION.title] if immunizations_mapped else []
    caveat = summary_caveat(mapped_sections=mapped_sections, dose_count=len(immunizations))
    composition = Composition(
        id=f"{tracked_entity_uid}-ips",
        text=_narrative(caveat),
        identifier=Identifier(system=URN_IDENTIFIER_SYSTEM, value=_urn("Composition", tracked_entity_uid)),
        status="final",
        type=CodeableConcept(
            coding=[
                Coding(
                    system=LOINC_SYSTEM,
                    code=IPS_COMPOSITION_TYPE_CODE,
                    display=IPS_COMPOSITION_TYPE_DISPLAY,
                )
            ]
        ),
        subject=subject_reference,
        date=assembled_at,
        author=[Reference(display=author_display)],
        title=IPS_DOCUMENT_TITLE,
        section=sections,
    )
    # Every entry of a document is addressed inside the document rather than at this server: an
    # `Immunization` is not a resource type this facade serves at a URL, and a `fullUrl` pointing at
    # one would be a link nobody can follow. `urn:uuid` is R4's own answer, and deriving it from the
    # DHIS2 identity is what makes two assemblies of an unchanged record name the same resources.
    # The Composition leads, which is what makes the Bundle a document rather than a collection.
    entries = [
        BundleEntry(fullUrl=_urn(composition.resourceType, composition.id or ""), resource=json_resource(composition)),
        BundleEntry(fullUrl=subject_reference.reference, resource=json_resource(subject)),
        *(
            BundleEntry(
                fullUrl=_urn(immunization.resourceType, immunization.id or ""),
                resource=json_resource(immunization),
            )
            for immunization in immunizations
        ),
    ]
    return AssembledSummary(
        bundle=Bundle(
            id=f"{tracked_entity_uid}-ips",
            type="document",
            identifier=Identifier(system=URN_IDENTIFIER_SYSTEM, value=_urn("Bundle", tracked_entity_uid)),
            timestamp=assembled_at,
            entry=entries,
        ),
        caveat=caveat,
    )

DHIS2 attribute values

The projection every generated resource carries its DHIS2 attribute values on, plus the uid -> code index a generate run resolves once against /api/attributes and every emitter joins against. DHIS2 sends an attribute value as an attribute UID and a string, so the code is a lookup rather than part of the value.

attributes

DHIS2 attribute values shared by every component: the projection plus the emit-time code index.

Classes

AttributeValueIn

Bases: BaseModel

One DHIS2 attribute value: the attribute it belongs to, and the value the instance holds.

DHIS2 sends {"attribute": {"id": "ihn1wb9eho8"}, "value": "KE03"} and nothing else - no code, no name, no value type - so the projection carries the UID and the value alone and the attribute's code is joined from AttributeCodeIndex at emit time. The value is a string whatever the attribute's declared DHIS2 value type; a GeoJSON-valued attribute arrives as the serialised document.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
class AttributeValueIn(BaseModel):
    """One DHIS2 attribute value: the attribute it belongs to, and the value the instance holds.

    DHIS2 sends `{"attribute": {"id": "ihn1wb9eho8"}, "value": "KE03"}` and nothing else - no
    code, no name, no value type - so the projection carries the UID and the value alone and
    the attribute's code is joined from `AttributeCodeIndex` at emit time. The value is a
    string whatever the attribute's declared DHIS2 value type; a GeoJSON-valued attribute
    arrives as the serialised document.
    """

    model_config = ConfigDict(frozen=True)

    attribute_uid: str
    value: str

AttributeCodeIndex

Bases: BaseModel

What one generate run knows about the instance's attributes: their codes, and which are unique.

An attribute DHIS2 left without a code is absent from the mapping rather than present with an empty one: most instances code few of their attributes, and some code none of them, so every consumer reads through code_for and decides what a missing code means for it.

unique_uids holds the attributes DHIS2 declares unique. A unique attribute value is a business identifier - a national registry number on a facility, a payer code on a data set - so it is emitted as an Identifier on the resource rather than as an annotation extension, and every consumer decides which of the two it writes by asking is_unique.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
class AttributeCodeIndex(BaseModel):
    """What one generate run knows about the instance's attributes: their codes, and which are unique.

    An attribute DHIS2 left without a code is absent from the mapping rather than present with
    an empty one: most instances code few of their attributes, and some code none of them, so
    every consumer reads through `code_for` and decides what a missing code means for it.

    `unique_uids` holds the attributes DHIS2 declares unique. A unique attribute value is a
    business identifier - a national registry number on a facility, a payer code on a data set -
    so it is emitted as an `Identifier` on the resource rather than as an annotation extension,
    and every consumer decides which of the two it writes by asking `is_unique`.
    """

    model_config = ConfigDict(frozen=True)

    codes: dict[str, str] = Field(default_factory=dict)
    unique_uids: frozenset[str] = Field(default_factory=frozenset)

    def code_for(self, attribute_uid: str) -> str | None:
        """The attribute's DHIS2 code, or None when the instance left it unset."""
        return self.codes.get(attribute_uid)

    def is_unique(self, attribute_uid: str) -> bool:
        """Whether DHIS2 declares the attribute unique - its values identify their object."""
        return attribute_uid in self.unique_uids
Methods:
code_for(attribute_uid)

The attribute's DHIS2 code, or None when the instance left it unset.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
def code_for(self, attribute_uid: str) -> str | None:
    """The attribute's DHIS2 code, or None when the instance left it unset."""
    return self.codes.get(attribute_uid)
is_unique(attribute_uid)

Whether DHIS2 declares the attribute unique - its values identify their object.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
def is_unique(self, attribute_uid: str) -> bool:
    """Whether DHIS2 declares the attribute unique - its values identify their object."""
    return attribute_uid in self.unique_uids

Generate notes

What a generate target has to say about a run, as a model rather than a sentence. A GenerateNote carries the kind of decision it records (GenerateNoteCategory) beside the human text, and echoes_validate says whether the kind only restates a finding d2w fhir validate reports on the instance - which is what lets a bare run count those apart from what generation itself found.

notes

Human-facing note formatting shared by every emitter and the service layer.

Classes

GenerateNoteCategory

Bases: StrEnum

The kind of decision one generate note records, which is what makes a note reasonable about.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
class GenerateNoteCategory(StrEnum):
    """The kind of decision one generate note records, which is what makes a note reasonable about."""

    #: A `[generate.*] include_ids` entry that matched nothing on the instance.
    SELECTION_MISMATCH = "selection-mismatch"

    #: An object the configured selection did not name, pulled in by the closure a form binds.
    SELECTION_CLOSURE = "selection-closure"

    #: The configured selection resolved to nothing, so the target emitted nothing.
    EMPTY_SELECTION = "empty-selection"

    #: A reference that leaves the selection, so the emitted resource points at something unpublished.
    SELECTION_GAP = "selection-gap"

    #: A whole form the emitter refused to publish rather than publish invalid.
    REFUSED_FORM = "refused-form"

    #: A form the emitter reshaped to fit FHIR, with every question kept.
    FORM_STRUCTURE = "form-structure"

    #: A question or captured value the emitter dropped or left unanswered.
    SKIPPED_QUESTION = "skipped-question"

    #: An answer emitted in a weaker shape than the question asked for, rather than dropped.
    ANSWER_FALLBACK = "answer-fallback"

    #: The instance holds no usable value where the target needed one, so the emitted resource is degraded.
    INSTANCE_DATA_GAP = "instance-data-gap"

    #: What the emitted volume costs the IG publisher's own build.
    BUILD_COST = "build-cost"

    #: The compiled guide removed, because this run rewrote the FSH sources SUSHI compiled it from.
    COMPILE_REMOVED = "compile-removed"

    #: The project's own files disagree: fhir.toml states an identity ig/sushi-config.yaml does not carry.
    SCAFFOLD_DRIFT = "scaffold-drift"

    #: A DHIS2 name the IG publisher's build cannot survive, published in rewritten wording.
    NAME_SUBSTITUTION = "name-substitution"

    #: A DHIS2 code carrying a space, published with the space hyphenated.
    CODE_SUBSTITUTION = "code-substitution"

    #: A DHIS2 code unusable as a concept code, so the UID stands in for it.
    CODE_FALLBACK = "code-fallback"

    #: A DHIS2 code claimed twice, so the loser takes the UID or receives no code at all.
    CODE_COLLISION = "code-collision"

    #: A DHIS2 code unusable as an identity stem, so the id stands in for it.
    STEM_FALLBACK = "stem-fallback"

GenerateNote

Bases: BaseModel

One note a generate target raised: the human text, plus the kind of decision it records.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
class GenerateNote(BaseModel):
    """One note a generate target raised: the human text, plus the kind of decision it records."""

    model_config = ConfigDict(frozen=True)

    category: GenerateNoteCategory
    message: str

    @computed_field  # type: ignore[prop-decorator]
    @property
    def echoes_validate(self) -> bool:
        """Whether the note only restates a finding `d2w fhir validate` reports on the instance better."""
        return self.category in VALIDATE_ECHO_CATEGORIES

    def __str__(self) -> str:
        """The human text alone - a note formats as the sentence it carries, kind and all."""
        return self.message
Attributes
echoes_validate property

Whether the note only restates a finding d2w fhir validate reports on the instance better.

Methods:
__str__()

The human text alone - a note formats as the sentence it carries, kind and all.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def __str__(self) -> str:
    """The human text alone - a note formats as the sentence it carries, kind and all."""
    return self.message

Functions:

pluralize(count, noun)

Render a count with its noun, singular at exactly one (1 error, 0 errors).

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def pluralize(count: int, noun: str) -> str:
    """Render a count with its noun, singular at exactly one (`1 error`, `0 errors`)."""
    return f"{count} {noun}" if count == 1 else f"{count} {noun}s"

verb_for_count(count, singular, plural)

The verb form a count takes, so a counted sentence agrees with itself at one and at many.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def verb_for_count(count: int, singular: str, plural: str) -> str:
    """The verb form a count takes, so a counted sentence agrees with itself at one and at many."""
    return singular if count == 1 else plural

aggregate_note(message, subjects, sample_size=5)

One loud note for many subjects: message, a capped sample, and the remainder count.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def aggregate_note(message: str, subjects: list[str], sample_size: int = 5) -> str:
    """One loud note for many subjects: message, a capped sample, and the remainder count."""
    sample = ", ".join(subjects[:sample_size])
    remainder = len(subjects) - sample_size
    return f"{message}: {sample}" + (f" and {remainder} more" if remainder > 0 else "")

generate_note(category, message)

Wrap one already-written message in the note kind it belongs to.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def generate_note(category: GenerateNoteCategory, message: str) -> GenerateNote:
    """Wrap one already-written message in the note kind it belongs to."""
    return GenerateNote(category=category, message=message)

aggregate_generate_note(category, message, subjects, sample_size=5)

One aggregate note for many subjects, carried as the note kind it belongs to.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
def aggregate_generate_note(
    category: GenerateNoteCategory, message: str, subjects: list[str], sample_size: int = 5
) -> GenerateNote:
    """One aggregate note for many subjects, carried as the note kind it belongs to."""
    return GenerateNote(category=category, message=aggregate_note(message, subjects, sample_size))

FHIR R4 resource schemas

dhis2w_fhir.r4 is the capture-facing surface over the R4 resource models, which dhis2w_fhir_engine owns and defines at dhis2w_fhir_engine.r4.resources. The engine is the FHIR foundation package, so one Patient and one Bundle serve the generator, the evaluator, and the server alike; this module re-exports that family under the path capture code imports it from, and every name below is the engine's own class object rather than a copy of it. import dhis2w_fhir.r4 keeps working exactly as written, and a model built through it is an instance of the engine's class.

That family covers the models every pre-built JSON document is serialised from - Organization and Location for the registry, CodeSystem and ValueSet for the option-set and category terminology, ConceptMap for both families' mappings back to DHIS2. Beside them are the resources a summary document is assembled out of - Composition and its flat CompositionSection, Patient, Condition, AllergyIntolerance, and Observation, with Bundle carrying the identifier and timestamp a document requires; see examples/fhir/client/ips_document.py and the IPS working paper. Every one is frozen, alias-aware, and closed to unknown keys, so Model.model_validate(payload).model_dump_json(exclude_none=True, by_alias=True) reproduces the input document key for key.

The R4 primitive checks under dhis2w_fhir.r4.primitives stay this package's own, and arrive under the same dhis2w_fhir.r4 name.

schemas

The capture-facing surface over the FHIR R4 resource models, which dhis2w_fhir_engine owns.

dhis2w_fhir_engine.r4.resources is where the models are defined: the engine is the FHIR foundation package, so one definition of Patient, Bundle, or QuestionnaireResponse serves the evaluator, the generator, and the server alike. This module re-exports that family under dhis2w_fhir.r4, which is the import path capture code reads them from - one import surface per package, so a caller working on DHIS2 capture never has to know which package below it declared the class.

Nothing is redefined here and nothing is renamed: every name below is the engine's own class object.

Classes

AllergyIntolerance

Bases: DomainResource

A FHIR R4 AllergyIntolerance - one allergy a summary names, or an assertion that none is known.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class AllergyIntolerance(DomainResource):
    """A FHIR R4 AllergyIntolerance - one allergy a summary names, or an assertion that none is known."""

    resourceType: Literal["AllergyIntolerance"] = "AllergyIntolerance"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    clinicalStatus: CodeableConcept | None = None
    verificationStatus: CodeableConcept | None = None
    type: Literal["allergy", "intolerance"] | None = None
    category: list[Literal["food", "medication", "environment", "biologic"]] | None = None
    criticality: Literal["low", "high", "unable-to-assess"] | None = None
    code: CodeableConcept | None = None
    patient: Reference | None = None
    recordedDate: str | None = None

Attachment

Bases: Element

Attached content - the base64 GeoJSON boundary a Location extension carries.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Attachment(Element):
    """Attached content - the base64 GeoJSON boundary a Location extension carries."""

    contentType: str | None = None
    data: str | None = None
    title: str | None = None
    size: int | None = None

BackboneElement

Bases: Element

BackboneElement - an element defined inside a resource rather than as a reusable datatype.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class BackboneElement(Element):
    """`BackboneElement` - an element defined inside a resource rather than as a reusable datatype."""

Bundle

Bases: Resource

A FHIR R4 Bundle - the container a search answers with, and the one a document is.

identifier and timestamp are optional on the base resource and required on a document: a document bundle states which document it is and the instant it was assembled.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Bundle(Resource):
    """A FHIR R4 Bundle - the container a search answers with, and the one a document is.

    `identifier` and `timestamp` are optional on the base resource and required on a document: a
    document bundle states which document it is and the instant it was assembled.
    """

    resourceType: Literal["Bundle"] = "Bundle"
    id: str | None = None
    identifier: Identifier | None = None
    timestamp: str | None = None
    type: (
        Literal[
            "searchset",
            "collection",
            "document",
            "message",
            "history",
            "transaction",
            "transaction-response",
            "batch",
            "batch-response",
        ]
        | None
    ) = None
    total: int | None = None
    link: list[BundleLink] | None = None
    entry: list[BundleEntry] | None = None

BundleEntry

Bases: BackboneElement

Bundle.entry - one resource in a bundle, at the URL it is served from.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class BundleEntry(BackboneElement):
    """`Bundle.entry` - one resource in a bundle, at the URL it is served from."""

    fullUrl: str | None = None
    resource: JsonResource | None = None
    search: BundleEntrySearch | None = None

BundleEntrySearch

Bases: BackboneElement

Bundle.entry.search - why an entry is in a search result set.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class BundleEntrySearch(BackboneElement):
    """`Bundle.entry.search` - why an entry is in a search result set."""

    mode: Literal["match", "include", "outcome"] | None = None

Bases: BackboneElement

Bundle.link - one relation of a search result set, such as self.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class BundleLink(BackboneElement):
    """`Bundle.link` - one relation of a search result set, such as `self`."""

    relation: str | None = None
    url: str | None = None

CapabilityStatement

Bases: DomainResource

A FHIR R4 CapabilityStatement - what a DHIS2 capture server accepts and serves.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatement(DomainResource):
    """A FHIR R4 CapabilityStatement - what a DHIS2 capture server accepts and serves."""

    resourceType: Literal["CapabilityStatement"] = "CapabilityStatement"
    id: str | None = None
    url: str | None = None
    name: str | None = None
    title: str | None = None
    status: Literal["draft", "active", "retired", "unknown"] | None = None
    experimental: bool | None = None
    date: str | None = None
    description: str | None = None
    kind: Literal["instance", "capability", "requirements"] | None = None
    instantiates: list[str] | None = None
    software: CapabilityStatementSoftware | None = None
    implementation: CapabilityStatementImplementation | None = None
    fhirVersion: str | None = None
    format: list[str] | None = None
    rest: list[CapabilityStatementRest] | None = None

CapabilityStatementImplementation

Bases: BackboneElement

CapabilityStatement.implementation - the specific installation the statement describes.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementImplementation(BackboneElement):
    """`CapabilityStatement.implementation` - the specific installation the statement describes."""

    description: str | None = None
    url: str | None = None

CapabilityStatementInteraction

Bases: BackboneElement

CapabilityStatement.rest.resource.interaction - one RESTful interaction supported on a resource type.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementInteraction(BackboneElement):
    """`CapabilityStatement.rest.resource.interaction` - one RESTful interaction supported on a resource type."""

    code: (
        Literal[
            "read", "vread", "update", "patch", "delete", "history-instance", "history-type", "create", "search-type"
        ]
        | None
    ) = None
    documentation: str | None = None

CapabilityStatementOperation

Bases: BackboneElement

CapabilityStatement.rest.operation and .rest.resource.operation - one operation the endpoint answers.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementOperation(BackboneElement):
    """`CapabilityStatement.rest.operation` and `.rest.resource.operation` - one operation the endpoint answers."""

    name: str | None = None
    definition: str | None = None
    documentation: str | None = None

CapabilityStatementResource

Bases: BackboneElement

CapabilityStatement.rest.resource - one resource type the endpoint serves, and how.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementResource(BackboneElement):
    """`CapabilityStatement.rest.resource` - one resource type the endpoint serves, and how."""

    type: str | None = None
    profile: str | None = None
    supportedProfile: list[str] | None = None
    documentation: str | None = None
    interaction: list[CapabilityStatementInteraction] | None = None
    searchParam: list[CapabilityStatementSearchParam] | None = None
    operation: list[CapabilityStatementOperation] | None = None

CapabilityStatementRest

Bases: BackboneElement

CapabilityStatement.rest - the RESTful behaviour of one end of the conversation.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementRest(BackboneElement):
    """`CapabilityStatement.rest` - the RESTful behaviour of one end of the conversation."""

    mode: Literal["client", "server"] | None = None
    documentation: str | None = None
    security: CapabilityStatementSecurity | None = None
    resource: list[CapabilityStatementResource] | None = None
    operation: list[CapabilityStatementOperation] | None = None

CapabilityStatementSearchParam

Bases: BackboneElement

CapabilityStatement.rest.resource.searchParam - one search parameter supported on a resource type.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementSearchParam(BackboneElement):
    """`CapabilityStatement.rest.resource.searchParam` - one search parameter supported on a resource type."""

    name: str | None = None
    definition: str | None = None
    type: (
        Literal["number", "date", "string", "token", "reference", "composite", "quantity", "uri", "special"] | None
    ) = None
    documentation: str | None = None

CapabilityStatementSecurity

Bases: BackboneElement

CapabilityStatement.rest.security - how the endpoint decides who is calling it.

service draws on R4's restful-security-service value set, which is extensible: a scheme the value set has no code for is stated as CodeableConcept.text, which is what the binding is for.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementSecurity(BackboneElement):
    """`CapabilityStatement.rest.security` - how the endpoint decides who is calling it.

    `service` draws on R4's `restful-security-service` value set, which is extensible: a scheme the
    value set has no code for is stated as `CodeableConcept.text`, which is what the binding is for.
    """

    cors: bool | None = None
    service: list[CodeableConcept] | None = None
    description: str | None = None

CapabilityStatementSoftware

Bases: BackboneElement

CapabilityStatement.software - the software the described endpoint runs.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CapabilityStatementSoftware(BackboneElement):
    """`CapabilityStatement.software` - the software the described endpoint runs."""

    name: str | None = None
    version: str | None = None

CodeableConcept

Bases: Element

A concept expressed as one or more codings, optionally with free text.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeableConcept(Element):
    """A concept expressed as one or more codings, optionally with free text."""

    coding: list[Coding] | None = None
    text: str | None = None

CodeSystem

Bases: DomainResource

A FHIR R4 CodeSystem as generated from one DHIS2 option set, one concept per option.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeSystem(DomainResource):
    """A FHIR R4 CodeSystem as generated from one DHIS2 option set, one concept per option."""

    resourceType: Literal["CodeSystem"] = "CodeSystem"
    id: str | None = None
    extension: list[Extension] | None = None
    url: str | None = None
    identifier: list[Identifier] | None = None
    name: str | None = None
    title: str | None = None
    title_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_title", "title_element"), serialization_alias="_title"
    )
    description: str | None = None
    status: Literal["draft", "active", "retired", "unknown"] | None = None
    experimental: bool | None = None
    caseSensitive: bool | None = None
    content: Literal["not-present", "example", "fragment", "complete", "supplement"] | None = None
    count: int | None = None
    valueSet: str | None = None
    property: list[CodeSystemProperty] | None = None
    concept: list[CodeSystemConcept] | None = None

CodeSystemConcept

Bases: BackboneElement

CodeSystem.concept - one DHIS2 option, keyed by its option UID.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeSystemConcept(BackboneElement):
    """`CodeSystem.concept` - one DHIS2 option, keyed by its option UID."""

    code: str | None = None
    display: str | None = None
    property: list[CodeSystemConceptProperty] | None = None
    designation: list[CodeSystemConceptDesignation] | None = None

CodeSystemConceptDesignation

Bases: BackboneElement

CodeSystem.concept.designation - the translation of a concept display into one locale.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeSystemConceptDesignation(BackboneElement):
    """`CodeSystem.concept.designation` - the translation of a concept display into one locale."""

    language: str | None = None
    value: str | None = None

CodeSystemConceptProperty

Bases: BackboneElement

CodeSystem.concept.property - one declared property carried by a single concept.

The value[x] choices are the ones the generated code systems declare a property type for: #string and #code on the DHIS2 code, domain, value type, and parent properties, #boolean on the uniqueness flag, #integer on the organisation-unit hierarchy level, and Coding on the category axes a category option combo concept decomposes over.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeSystemConceptProperty(BackboneElement):
    """`CodeSystem.concept.property` - one declared property carried by a single concept.

    The `value[x]` choices are the ones the generated code systems declare a property type for:
    `#string` and `#code` on the DHIS2 code, domain, value type, and parent properties,
    `#boolean` on the uniqueness flag, `#integer` on the organisation-unit hierarchy level, and
    `Coding` on the category axes a category option combo concept decomposes over.
    """

    code: str | None = None
    valueCode: str | None = None
    valueString: str | None = None
    valueBoolean: bool | None = None
    valueInteger: int | None = None
    valueCoding: Coding | None = None

CodeSystemProperty

Bases: BackboneElement

CodeSystem.property - the declaration of a property the concepts in the code system may carry.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CodeSystemProperty(BackboneElement):
    """`CodeSystem.property` - the declaration of a property the concepts in the code system may carry."""

    code: str | None = None
    uri: str | None = None
    description: str | None = None
    type: CodeSystemPropertyType | None = None

Coding

Bases: Element

One code drawn from a code system, with the display the system gives it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Coding(Element):
    """One code drawn from a code system, with the display the system gives it."""

    system: str | None = None
    code: str | None = None
    display: str | None = None

Composition

Bases: DomainResource

A FHIR R4 Composition - the first entry of a document bundle, and the index of everything in it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Composition(DomainResource):
    """A FHIR R4 Composition - the first entry of a document bundle, and the index of everything in it."""

    resourceType: Literal["Composition"] = "Composition"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: Identifier | None = None
    status: Literal["preliminary", "final", "amended", "entered-in-error"] | None = None
    type: CodeableConcept | None = None
    category: list[CodeableConcept] | None = None
    subject: Reference | None = None
    encounter: Reference | None = None
    date: str | None = None
    author: list[Reference] | None = None
    title: str | None = None
    custodian: Reference | None = None
    section: list[CompositionSection] | None = None

CompositionSection

Bases: BackboneElement

Composition.section - one section of a document: what it is, what is in it, or why nothing is.

entry names the resources the section carries and emptyReason says why it carries none, and a section states one or the other. No nested section: the International Patient Summary pins section.section to 0..0, so the section model a summary uses is flat.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class CompositionSection(BackboneElement):
    """`Composition.section` - one section of a document: what it is, what is in it, or why nothing is.

    `entry` names the resources the section carries and `emptyReason` says why it carries none, and a
    section states one or the other. No nested `section`: the International Patient Summary pins
    `section.section` to 0..0, so the section model a summary uses is flat.
    """

    title: str | None = None
    code: CodeableConcept | None = None
    text: Narrative | None = None
    author: list[Reference] | None = None
    focus: Reference | None = None
    entry: list[Reference] | None = None
    emptyReason: CodeableConcept | None = None

ConceptMap

Bases: DomainResource

A FHIR R4 ConceptMap taking one option set's concept codes back to the DHIS2 identifiers they stand for.

identifier is a single Identifier rather than a list: R4 gives ConceptMap 0..1 where it gives CodeSystem and ValueSet 0..*.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ConceptMap(DomainResource):
    """A FHIR R4 ConceptMap taking one option set's concept codes back to the DHIS2 identifiers they stand for.

    `identifier` is a single `Identifier` rather than a list: R4 gives ConceptMap `0..1` where it
    gives CodeSystem and ValueSet `0..*`.
    """

    resourceType: Literal["ConceptMap"] = "ConceptMap"
    id: str | None = None
    url: str | None = None
    identifier: Identifier | None = None
    name: str | None = None
    title: str | None = None
    title_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_title", "title_element"), serialization_alias="_title"
    )
    description: str | None = None
    status: Literal["draft", "active", "retired", "unknown"] | None = None
    experimental: bool | None = None
    sourceCanonical: str | None = None
    targetCanonical: str | None = None
    group: list[ConceptMapGroup] | None = None

ConceptMapGroup

Bases: BackboneElement

ConceptMap.group - the mappings from one source system into one target system.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ConceptMapGroup(BackboneElement):
    """`ConceptMap.group` - the mappings from one source system into one target system."""

    source: str | None = None
    target: str | None = None
    element: list[ConceptMapGroupElement] | None = None

ConceptMapGroupElement

Bases: BackboneElement

ConceptMap.group.element - one source concept and every target it maps onto.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ConceptMapGroupElement(BackboneElement):
    """`ConceptMap.group.element` - one source concept and every target it maps onto."""

    code: str | None = None
    display: str | None = None
    target: list[ConceptMapGroupElementTarget] | None = None

ConceptMapGroupElementTarget

Bases: BackboneElement

ConceptMap.group.element.target - the DHIS2 identifier one concept maps onto, and how closely.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ConceptMapGroupElementTarget(BackboneElement):
    """`ConceptMap.group.element.target` - the DHIS2 identifier one concept maps onto, and how closely."""

    code: str | None = None
    display: str | None = None
    equivalence: (
        Literal[
            "relatedto",
            "equivalent",
            "equal",
            "wider",
            "subsumes",
            "narrower",
            "specializes",
            "inexact",
            "unmatched",
            "disjoint",
        ]
        | None
    ) = None

Condition

Bases: DomainResource

A FHIR R4 Condition - one problem a summary's Problems section names, or an assertion of none.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Condition(DomainResource):
    """A FHIR R4 Condition - one problem a summary's Problems section names, or an assertion of none."""

    resourceType: Literal["Condition"] = "Condition"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    clinicalStatus: CodeableConcept | None = None
    verificationStatus: CodeableConcept | None = None
    category: list[CodeableConcept] | None = None
    code: CodeableConcept | None = None
    subject: Reference | None = None
    onsetDateTime: str | None = None
    recordedDate: str | None = None

ContactPoint

Bases: Element

A telecom contact point - the phone number or email address of an organisation unit.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ContactPoint(Element):
    """A telecom contact point - the phone number or email address of an organisation unit."""

    system: Literal["phone", "fax", "email", "pager", "url", "sms", "other"] | None = None
    value: str | None = None

DomainResource

Bases: Resource

DomainResource - a resource carrying narrative and extensions; every resource emitted here is one.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class DomainResource(Resource):
    """`DomainResource` - a resource carrying narrative and extensions; every resource emitted here is one."""

Element

Bases: FhirBase

Element - the R4 root for datatypes, and the _x sibling a primitive's extensions hang from.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Element(FhirBase):
    """`Element` - the R4 root for datatypes, and the `_x` sibling a primitive's extensions hang from."""

    extension: list[Extension] | None = None

Extension

Bases: Element

One extension: either a nested set of extensions or a single value[x] choice.

Only the choices this package emits or reads are modelled. valueDecimal is typed as int | float rather than float so a whole number survives the round trip: float would coerce the wire value 2896 to 2896.0 and change the document.

valueString_element carries the _valueString sibling the way name_element carries _name: a DHIS2 string an extension holds - a date label, a description - is translated in the instance, and its translations ride the standard R4 translation extension on the primitive.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Extension(Element):
    """One extension: either a nested set of extensions or a single `value[x]` choice.

    Only the choices this package emits or reads are modelled. `valueDecimal` is typed as
    `int | float` rather than `float` so a whole number survives the round trip: `float`
    would coerce the wire value `2896` to `2896.0` and change the document.

    `valueString_element` carries the `_valueString` sibling the way `name_element` carries
    `_name`: a DHIS2 string an extension holds - a date label, a description - is translated in
    the instance, and its translations ride the standard R4 translation extension on the primitive.
    """

    url: str
    extension: list[Extension] | None = None
    valueBoolean: bool | None = None
    valueCode: str | None = None
    valueId: str | None = None
    """A FHIR `id` - a DHIS2 UID is one, which is what a published program rule names itself by."""

    valueCanonical: str | None = None
    valueString: str | None = None
    valueString_element: Element | None = Field(
        default=None,
        validation_alias=AliasChoices("_valueString", "valueString_element"),
        serialization_alias="_valueString",
    )
    valueDate: str | None = None
    valueDateTime: str | None = None
    valueInteger: int | None = None
    valueDecimal: int | float | None = None
    valueAttachment: Attachment | None = None
    valueCodeableConcept: CodeableConcept | None = None
    valueCoding: Coding | None = None
    valueIdentifier: Identifier | None = None
    valueReference: Reference | None = None
    valuePeriod: Period | None = None
Attributes
valueId = None class-attribute instance-attribute

A FHIR id - a DHIS2 UID is one, which is what a published program rule names itself by.

FhirBase

Bases: BaseModel

Pydantic carrier for every schema here - frozen, alias-aware, closed to unknown keys. Not a FHIR type.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class FhirBase(BaseModel):
    """Pydantic carrier for every schema here - frozen, alias-aware, closed to unknown keys. Not a FHIR type."""

    model_config = ConfigDict(frozen=True, populate_by_name=True, extra="forbid")

HumanName

Bases: Element

A person's name; the generated contacts and a nominated DHIS2 attribute both carry it in text.

text alone satisfies the IPS invariant ips-pat-1, which asks for family, given, or text. family and given are here for a name a document was handed already split; nothing in this project splits one, because which half of a person's name an attribute holds is a fact DHIS2 does not state.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class HumanName(Element):
    """A person's name; the generated contacts and a nominated DHIS2 attribute both carry it in `text`.

    `text` alone satisfies the IPS invariant `ips-pat-1`, which asks for `family`, `given`, **or**
    `text`. `family` and `given` are here for a name a document was handed already split; nothing in
    this project splits one, because which half of a person's name an attribute holds is a fact
    DHIS2 does not state.
    """

    text: str | None = None
    family: str | None = None
    given: list[str] | None = None

Identifier

Bases: Element

A business identifier: the DHIS2 UID or code under its identifier system.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Identifier(Element):
    """A business identifier: the DHIS2 UID or code under its identifier system."""

    system: str | None = None
    value: str | None = None

Immunization

Bases: DomainResource

A FHIR R4 Immunization - one dose a summary's Immunizations section names.

The four elements the International Patient Summary asks a Creator to populate where they are known are status, vaccineCode, patient, and occurrence[x], and each of them is a fact DHIS2 states about a recorded dose. vaccineCode's binding in the IPS is preferred rather than required, so a dose recorded against a DHIS2 data element is published with the DHIS2 coding and violates no profile - which is what lets this section carry real content while an international vaccine vocabulary is still missing.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Immunization(DomainResource):
    """A FHIR R4 Immunization - one dose a summary's Immunizations section names.

    The four elements the International Patient Summary asks a Creator to populate where they are
    known are `status`, `vaccineCode`, `patient`, and `occurrence[x]`, and each of them is a fact
    DHIS2 states about a recorded dose. `vaccineCode`'s binding in the IPS is **preferred** rather
    than required, so a dose recorded against a DHIS2 data element is published with the DHIS2
    coding and violates no profile - which is what lets this section carry real content while an
    international vaccine vocabulary is still missing.
    """

    resourceType: Literal["Immunization"] = "Immunization"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    status: Literal["completed", "entered-in-error", "not-done"] | None = None
    vaccineCode: CodeableConcept | None = None
    patient: Reference | None = None
    encounter: Reference | None = None
    occurrenceDateTime: str | None = None
    occurrenceString: str | None = None
    location: Reference | None = None
    protocolApplied: list[ImmunizationProtocolApplied] | None = None

ImmunizationProtocolApplied

Bases: BackboneElement

Immunization.protocolApplied - which dose of a series one administration was.

doseNumberString rather than doseNumberPositiveInt because a DHIS2 option code naming a dose is a code and not always a number: Dose 0 and IPT 1 are both dose numbers an instance states, and reading either as an integer would either fail or invent one.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ImmunizationProtocolApplied(BackboneElement):
    """`Immunization.protocolApplied` - which dose of a series one administration was.

    `doseNumberString` rather than `doseNumberPositiveInt` because a DHIS2 option code naming a dose
    is a code and not always a number: `Dose 0` and `IPT 1` are both dose numbers an instance states,
    and reading either as an integer would either fail or invent one.
    """

    series: str | None = None
    doseNumberString: str | None = None

JsonResource

Bases: FhirBase

The one open model here: a wire document carried verbatim, keyed only by its resourceType.

A Bundle entry, a compiled-store body, and an operation parameter carrying a whole resource hold whatever resource the document happens to be, so modelling their contents would mean naming every resource type in advance. This is the typed wrapper the house style asks for over a genuinely dynamic wire shape: extra="allow" keeps every key the document carried, and resourceType is the one fact that is always there.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class JsonResource(FhirBase):
    """The one open model here: a wire document carried verbatim, keyed only by its `resourceType`.

    A Bundle entry, a compiled-store body, and an operation parameter carrying a whole resource hold
    whatever resource the document happens to be, so modelling their contents would mean naming every
    resource type in advance. This is the typed wrapper the house style asks for over a genuinely
    dynamic wire shape: `extra="allow"` keeps every key the document carried, and `resourceType` is
    the one fact that is always there.
    """

    model_config = ConfigDict(frozen=True, populate_by_name=True, extra="allow")

    resourceType: str

ListEntry

Bases: BackboneElement

List.entry - one resource the list names, as a reference.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ListEntry(BackboneElement):
    """`List.entry` - one resource the list names, as a reference."""

    item: Reference

Location

Bases: DomainResource

A FHIR R4 Location as generated from the physical place of one DHIS2 organisation unit.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Location(DomainResource):
    """A FHIR R4 Location as generated from the physical place of one DHIS2 organisation unit."""

    resourceType: Literal["Location"] = "Location"
    id: str | None = None
    meta: Meta | None = None
    identifier: list[Identifier] | None = None
    name: str | None = None
    name_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_name", "name_element"), serialization_alias="_name"
    )
    description: str | None = None
    status: Literal["active", "suspended", "inactive"] | None = None
    position: LocationPosition | None = None
    extension: list[Extension] | None = None
    managingOrganization: Reference | None = None
    partOf: Reference | None = None

LocationPosition

Bases: BackboneElement

Location.position - the WGS84 point of an organisation unit.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class LocationPosition(BackboneElement):
    """`Location.position` - the WGS84 point of an organisation unit."""

    longitude: float | None = None
    latitude: float | None = None

Meta

Bases: Element

Resource.meta - the profiles a generated instance claims conformance to, and the tags classifying it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Meta(Element):
    """`Resource.meta` - the profiles a generated instance claims conformance to, and the tags classifying it."""

    profile: list[str] | None = None
    tag: list[Coding] | None = None

Narrative

Bases: Element

DomainResource.text - the human-readable XHTML rendering of a resource.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Narrative(Element):
    """`DomainResource.text` - the human-readable XHTML rendering of a resource."""

    status: Literal["generated", "extensions", "additional", "empty"] | None = None
    div: str | None = None

Observation

Bases: DomainResource

A FHIR R4 Observation - one recorded value, coded as whatever code system stated the question.

value[x] here is the string and the concept: a DHIS2 data value arrives as the string DHIS2 sent, and a Quantity would need a unit and a unit system nobody has stated for a DHIS2 data element. dataAbsentReason is the element R4 gives an observation made with no value.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Observation(DomainResource):
    """A FHIR R4 Observation - one recorded value, coded as whatever code system stated the question.

    `value[x]` here is the string and the concept: a DHIS2 data value arrives as the string DHIS2
    sent, and a `Quantity` would need a unit and a unit system nobody has stated for a DHIS2 data
    element. `dataAbsentReason` is the element R4 gives an observation made with no value.
    """

    resourceType: Literal["Observation"] = "Observation"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    status: (
        Literal[
            "registered",
            "preliminary",
            "final",
            "amended",
            "corrected",
            "cancelled",
            "entered-in-error",
            "unknown",
        ]
        | None
    ) = None
    category: list[CodeableConcept] | None = None
    code: CodeableConcept | None = None
    subject: Reference | None = None
    effectiveDateTime: str | None = None
    issued: str | None = None
    performer: list[Reference] | None = None
    valueString: str | None = None
    valueBoolean: bool | None = None
    valueInteger: int | None = None
    valueCodeableConcept: CodeableConcept | None = None
    dataAbsentReason: CodeableConcept | None = None

OperationOutcome

Bases: DomainResource

A FHIR R4 OperationOutcome - the error body every failed interaction answers with.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class OperationOutcome(DomainResource):
    """A FHIR R4 OperationOutcome - the error body every failed interaction answers with."""

    resourceType: Literal["OperationOutcome"] = "OperationOutcome"
    id: str | None = None
    issue: list[OperationOutcomeIssue] | None = None

OperationOutcomeIssue

Bases: BackboneElement

OperationOutcome.issue - one thing that went wrong, at the severity and issue type R4 names for it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class OperationOutcomeIssue(BackboneElement):
    """`OperationOutcome.issue` - one thing that went wrong, at the severity and issue type R4 names for it."""

    severity: Literal["fatal", "error", "warning", "information"] | None = None
    code: (
        Literal[
            "invalid",
            "structure",
            "required",
            "value",
            "invariant",
            "security",
            "login",
            "unknown",
            "expired",
            "forbidden",
            "suppressed",
            "processing",
            "not-supported",
            "duplicate",
            "multiple-matches",
            "not-found",
            "deleted",
            "too-long",
            "code-invalid",
            "extension",
            "too-costly",
            "business-rule",
            "conflict",
            "transient",
            "lock-error",
            "no-store",
            "exception",
            "timeout",
            "incomplete",
            "throttled",
            "informational",
        ]
        | None
    ) = None
    details: CodeableConcept | None = None
    diagnostics: str | None = None
    expression: list[str] | None = None

Organization

Bases: DomainResource

A FHIR R4 Organization as generated from one DHIS2 organisation unit.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Organization(DomainResource):
    """A FHIR R4 Organization as generated from one DHIS2 organisation unit."""

    resourceType: Literal["Organization"] = "Organization"
    id: str | None = None
    meta: Meta | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    name: str | None = None
    name_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_name", "name_element"), serialization_alias="_name"
    )
    alias: list[str] | None = None
    type: list[CodeableConcept] | None = None
    partOf: Reference | None = None
    telecom: list[ContactPoint] | None = None
    contact: list[OrganizationContact] | None = None
    active: bool | None = None

OrganizationContact

Bases: BackboneElement

Organization.contact - a contact party for the organisation unit.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class OrganizationContact(BackboneElement):
    """`Organization.contact` - a contact party for the organisation unit."""

    name: HumanName | None = None
    telecom: list[ContactPoint] | None = None

Parameters

Bases: Resource

A FHIR R4 Parameters - the body an operation answers with; a Resource, so it carries no narrative.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Parameters(Resource):
    """A FHIR R4 Parameters - the body an operation answers with; a `Resource`, so it carries no narrative."""

    resourceType: Literal["Parameters"] = "Parameters"
    id: str | None = None
    parameter: list[ParametersParameter] | None = None

ParametersParameter

Bases: BackboneElement

Parameters.parameter - one named input or output of an operation, valued or nested in part.

resource is how a parameter carries a whole resource rather than a datatype - an operation handed a document to work over, and an operation answering an OperationOutcome about one of its own parts. It is JsonResource for the reason BundleEntry.resource is: which resource type it holds is the caller's to decide, one request at a time.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ParametersParameter(BackboneElement):
    """`Parameters.parameter` - one named input or output of an operation, valued or nested in `part`.

    `resource` is how a parameter carries a whole resource rather than a datatype - an operation
    handed a document to work over, and an operation answering an `OperationOutcome` about one of its
    own parts. It is `JsonResource` for the reason `BundleEntry.resource` is: which resource type it
    holds is the caller's to decide, one request at a time.
    """

    name: str | None = None
    valueBoolean: bool | None = None
    valueCode: str | None = None
    valueDecimal: float | None = None
    valueInteger: int | None = None
    valueString: str | None = None
    valueUri: str | None = None
    valueCoding: Coding | None = None
    resource: JsonResource | None = None
    part: list[ParametersParameter] | None = None

Patient

Bases: DomainResource

A FHIR R4 Patient - the person a summary document is about, with the demographics somebody stated.

RegisteredEntity is the register's projection of a tracked entity and carries identity alone; this is the fuller resource a document assembles, so it names the elements a nomination fills. birthDate_element carries the _birthDate sibling the way name_element carries _name: a person the instance holds no birth date for keeps the required element and states its absence on the data-absent-reason extension, which is what DATA_ABSENT_REASON_EXTENSION_URL is for.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Patient(DomainResource):
    """A FHIR R4 Patient - the person a summary document is about, with the demographics somebody stated.

    `RegisteredEntity` is the register's projection of a tracked entity and carries identity alone;
    this is the fuller resource a document assembles, so it names the elements a nomination fills.
    `birthDate_element` carries the `_birthDate` sibling the way `name_element` carries `_name`: a
    person the instance holds no birth date for keeps the required element and states its absence on
    the data-absent-reason extension, which is what `DATA_ABSENT_REASON_EXTENSION_URL` is for.
    """

    resourceType: Literal["Patient"] = "Patient"
    id: str | None = None
    meta: Meta | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    active: bool | None = None
    name: list[HumanName] | None = None
    gender: Literal["male", "female", "other", "unknown"] | None = None
    birthDate: str | None = None
    birthDate_element: Element | None = Field(
        default=None,
        validation_alias=AliasChoices("_birthDate", "birthDate_element"),
        serialization_alias="_birthDate",
    )
    managingOrganization: Reference | None = None

Period

Bases: Element

A time range with inclusive bounds - the reporting period a captured data value set covers.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Period(Element):
    """A time range with inclusive bounds - the reporting period a captured data value set covers."""

    start: str | None = None
    end: str | None = None

Questionnaire

Bases: DomainResource

A FHIR R4 Questionnaire as generated from one DHIS2 data set, event program, or program stage.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Questionnaire(DomainResource):
    """A FHIR R4 Questionnaire as generated from one DHIS2 data set, event program, or program stage."""

    resourceType: Literal["Questionnaire"] = "Questionnaire"
    id: str | None = None
    url: str | None = None
    title: str | None = None
    title_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_title", "title_element"), serialization_alias="_title"
    )
    description: str | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    name: str | None = None
    status: Literal["draft", "active", "retired", "unknown"] | None = None
    experimental: bool | None = None
    subjectType: list[str] | None = None
    code: list[Coding] | None = None
    item: list[QuestionnaireItem] | None = None

QuestionnaireItem

Bases: BackboneElement

Questionnaire.item - one question, or a group nesting the questions of a section or a disaggregation.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class QuestionnaireItem(BackboneElement):
    """`Questionnaire.item` - one question, or a group nesting the questions of a section or a disaggregation."""

    linkId: str | None = None
    code: list[Coding] | None = None
    text: str | None = None
    text_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_text", "text_element"), serialization_alias="_text"
    )
    type: (
        Literal[
            "group",
            "display",
            "boolean",
            "decimal",
            "integer",
            "date",
            "dateTime",
            "time",
            "string",
            "text",
            "url",
            "choice",
            "open-choice",
            "attachment",
            "reference",
            "quantity",
        ]
        | None
    ) = None
    answerValueSet: str | None = None
    required: bool | None = None
    repeats: bool | None = None
    readOnly: bool | None = None
    """True when DHIS2 owns the value - a generated tracked entity attribute, minted by the instance on import."""

    enableWhen: list[QuestionnaireItemEnableWhen] | None = None
    """The conditions under which the form asks this item; absent on an item the form always asks."""

    enableBehavior: Literal["all", "any"] | None = None
    """How several conditions combine. R4 requires it past one condition and admits it at one."""

    extension: list[Extension] | None = None
    item: list[QuestionnaireItem] | None = None
Attributes
readOnly = None class-attribute instance-attribute

True when DHIS2 owns the value - a generated tracked entity attribute, minted by the instance on import.

enableWhen = None class-attribute instance-attribute

The conditions under which the form asks this item; absent on an item the form always asks.

enableBehavior = None class-attribute instance-attribute

How several conditions combine. R4 requires it past one condition and admits it at one.

QuestionnaireItemEnableWhen

Bases: BackboneElement

Questionnaire.item.enableWhen - one condition under which the form asks the item carrying it.

The answer[x] choices are the ones a DHIS2 program rule condition compiles into: a coded answer, a tick, a number, and the three temporal primitives. operator is the R4 code, and exists states its sense on answerBoolean rather than on any of the others.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class QuestionnaireItemEnableWhen(BackboneElement):
    """`Questionnaire.item.enableWhen` - one condition under which the form asks the item carrying it.

    The `answer[x]` choices are the ones a DHIS2 program rule condition compiles into: a coded
    answer, a tick, a number, and the three temporal primitives. `operator` is the R4 code, and
    `exists` states its sense on `answerBoolean` rather than on any of the others.
    """

    question: str | None = None
    operator: Literal["exists", "=", "!=", ">", "<", ">=", "<="] | None = None
    answerBoolean: bool | None = None
    answerDecimal: int | float | None = None
    answerInteger: int | None = None
    answerDate: str | None = None
    answerDateTime: str | None = None
    answerTime: str | None = None
    answerString: str | None = None
    answerCoding: Coding | None = None

QuestionnaireResponse

Bases: DomainResource

A FHIR R4 QuestionnaireResponse - one captured DHIS2 data value set, event, or tracker event.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class QuestionnaireResponse(DomainResource):
    """A FHIR R4 QuestionnaireResponse - one captured DHIS2 data value set, event, or tracker event."""

    resourceType: Literal["QuestionnaireResponse"] = "QuestionnaireResponse"
    id: str | None = None
    meta: Meta | None = None
    language: str | None = None
    text: Narrative | None = None
    extension: list[Extension] | None = None
    identifier: Identifier | None = None
    basedOn: list[Reference] | None = None
    partOf: list[Reference] | None = None
    questionnaire: str | None = None
    status: Literal["in-progress", "completed", "amended", "entered-in-error", "stopped"] | None = None
    subject: Reference | None = None
    encounter: Reference | None = None
    authored: str | None = None
    author: Reference | None = None
    source: Reference | None = None
    item: list[QuestionnaireResponseItem] | None = None

QuestionnaireResponseAnswer

Bases: BackboneElement

QuestionnaireResponse.item.answer - one captured value on the value[x] element its type asks for.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class QuestionnaireResponseAnswer(BackboneElement):
    """`QuestionnaireResponse.item.answer` - one captured value on the `value[x]` element its type asks for."""

    valueBoolean: bool | None = None
    valueDecimal: int | float | None = None
    valueInteger: int | None = None
    valueDate: str | None = None
    valueDateTime: str | None = None
    valueTime: str | None = None
    valueString: str | None = None
    valueUri: str | None = None
    valueAttachment: Attachment | None = None
    valueCoding: Coding | None = None
    valueReference: Reference | None = None
    item: list[QuestionnaireResponseItem] | None = None

QuestionnaireResponseItem

Bases: BackboneElement

QuestionnaireResponse.item - one answered question, or a group mirroring the questionnaire's tree.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class QuestionnaireResponseItem(BackboneElement):
    """`QuestionnaireResponse.item` - one answered question, or a group mirroring the questionnaire's tree."""

    linkId: str | None = None
    definition: str | None = None
    text: str | None = None
    answer: list[QuestionnaireResponseAnswer] | None = None
    item: list[QuestionnaireResponseItem] | None = None

Reference

Bases: Element

A reference to another resource - a literal Organization/mOsABqg3Cqw, or a business identifier.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Reference(Element):
    """A reference to another resource - a literal `Organization/mOsABqg3Cqw`, or a business identifier."""

    reference: str | None = None
    type: str | None = None
    identifier: Identifier | None = None
    display: str | None = None

RegisteredEntity

Bases: DomainResource

One DHIS2 tracked entity as the FHIR resource its type is published as - identity only, no domain claims.

resourceType is a plain string rather than a literal because the resource a tracked entity is served as is what the published D2TET_CM maps its type onto: a Patient for the people, a Specimen for the samples, whatever a project states. The elements are the four every R4 resource in that map carries and DHIS2 states without interpretation: identifier for the tracked entity UID and the values of the attributes DHIS2 declares unique, meta.tag for the tracked entity type, and extension for every other attribute value the entity holds.

The three demographic elements below are filled from a nomination and from nothing else. DHIS2 has no name field, no sex field, and no date-of-birth field, so which attribute means which of them is stated in [ips.identity] per instance or it is not stated at all; a project that nominates nothing serves the four elements above and no others. They sit at the end of the model rather than in R4's own element order so that a served resource is byte-identical to what this register answered before the nomination existed. birthDate_element carries the _birthDate sibling the way Patient does, because a nominated birth date the instance holds no readable value for states its absence there - see dhis2w_fhir.ips and dhis2w_fhir_serve.register.projection.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class RegisteredEntity(DomainResource):
    """One DHIS2 tracked entity as the FHIR resource its type is published as - identity only, no domain claims.

    `resourceType` is a plain string rather than a literal because the resource a tracked entity is
    served as is what the published `D2TET_CM` maps its type onto: a Patient for the people, a
    Specimen for the samples, whatever a project states. The elements are the four every R4 resource
    in that map carries and DHIS2 states without interpretation: `identifier` for the tracked entity
    UID and the values of the attributes DHIS2 declares unique, `meta.tag` for the tracked entity
    type, and `extension` for every other attribute value the entity holds.

    The three demographic elements below are filled from a nomination and from nothing else. DHIS2
    has no name field, no sex field, and no date-of-birth field, so which attribute means which of
    them is stated in `[ips.identity]` per instance or it is not stated at all; a project that
    nominates nothing serves the four elements above and no others. They sit at the end of the model
    rather than in R4's own element order so that a served resource is byte-identical to what this
    register answered before the nomination existed. `birthDate_element` carries the `_birthDate`
    sibling the way `Patient` does, because a nominated birth date the instance holds no readable
    value for states its absence there - see `dhis2w_fhir.ips` and
    `dhis2w_fhir_serve.register.projection`.
    """

    resourceType: str
    id: str | None = None
    meta: Meta | None = None
    identifier: list[Identifier] | None = None
    extension: list[Extension] | None = None
    name: list[HumanName] | None = None
    gender: Literal["male", "female", "other", "unknown"] | None = None
    birthDate: str | None = None
    birthDate_element: Element | None = Field(
        default=None,
        validation_alias=AliasChoices("_birthDate", "birthDate_element"),
        serialization_alias="_birthDate",
    )

Resource

Bases: FhirBase

Resource - the R4 root for resources, a sibling of Element rather than a subtype of it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class Resource(FhirBase):
    """`Resource` - the R4 root for resources, a sibling of `Element` rather than a subtype of it."""

ResourceList

Bases: DomainResource

A FHIR R4 List, which is what carries a DHIS2 organisation-unit assignment.

R4 binds Group.member.entity to Patient, Practitioner, PractitionerRole, Device, Medication, Substance, and Group, so a Location cannot be a Group member; List.entry.item is Reference(Resource) and takes one. Named ResourceList because List is a built-in.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ResourceList(DomainResource):
    """A FHIR R4 List, which is what carries a DHIS2 organisation-unit assignment.

    R4 binds `Group.member.entity` to Patient, Practitioner, PractitionerRole, Device,
    Medication, Substance, and Group, so a Location cannot be a Group member; `List.entry.item`
    is `Reference(Resource)` and takes one. Named `ResourceList` because `List` is a built-in.
    """

    resourceType: Literal["List"] = "List"
    id: str | None = None
    meta: Meta | None = None
    extension: list[Extension] | None = None
    identifier: list[Identifier] | None = None
    status: Literal["current", "retired", "entered-in-error"] = "current"
    mode: Literal["working", "snapshot", "changes"] = "snapshot"
    title: str | None = None
    code: CodeableConcept | None = None
    entry: list[ListEntry] | None = None

ValueSet

Bases: DomainResource

A FHIR R4 ValueSet as generated from one DHIS2 option set, composing the whole matching CodeSystem.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ValueSet(DomainResource):
    """A FHIR R4 ValueSet as generated from one DHIS2 option set, composing the whole matching CodeSystem."""

    resourceType: Literal["ValueSet"] = "ValueSet"
    id: str | None = None
    extension: list[Extension] | None = None
    url: str | None = None
    identifier: list[Identifier] | None = None
    name: str | None = None
    title: str | None = None
    title_element: Element | None = Field(
        default=None, validation_alias=AliasChoices("_title", "title_element"), serialization_alias="_title"
    )
    description: str | None = None
    status: Literal["draft", "active", "retired", "unknown"] | None = None
    experimental: bool | None = None
    compose: ValueSetCompose | None = None

ValueSetCompose

Bases: BackboneElement

ValueSet.compose - the content logic that builds the expansion of the value set.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ValueSetCompose(BackboneElement):
    """`ValueSet.compose` - the content logic that builds the expansion of the value set."""

    include: list[ValueSetInclude] | None = None

ValueSetInclude

Bases: BackboneElement

ValueSet.compose.include - one code system the value set draws its codes from.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
class ValueSetInclude(BackboneElement):
    """`ValueSet.compose.include` - one code system the value set draws its codes from."""

    system: str | None = None

Functions:

json_resource(resource)

Carry a typed resource as a JsonResource, exactly as the emitter would have written it.

Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
def json_resource(resource: FhirBase) -> JsonResource:
    """Carry a typed resource as a `JsonResource`, exactly as the emitter would have written it."""
    return JsonResource.model_validate(json.loads(resource.model_dump_json(exclude_none=True, by_alias=True)))

Conversion: QuestionnaireResponse to DHIS2

The inverse of the emitters, and the reference implementation docs/fhir/design/conversion.md holds the later published StructureMaps against. A caller assembles a ConversionContext once from the compiled IG artifacts - the served Questionnaires, the option-set CodeSystems and their ConceptMaps, the ValueSets binding the two, and the published Locations - and then translates each captured response into the DHIS2 import payload its form kind reports: a DataValueSet for a data set, a TrackerEvent for an event program or a tracker program stage. A response the translator cannot read whole answers with typed refusals naming the link id and the reason, never with a partial payload.

A result carries exactly one payload and its target_kind names which, so ConversionResult.payload reads the document off the kind and payload_of narrows it to one wire shape. The batch form is ConversionReport.payloads_of, whose four named properties - data_value_sets, events, tracked_entities, enrollments - are the order a drain posts them in.

conversion.artifacts is where a project's own files become those models: it reads the compiled ig/fsh-generated/resources merged with the predefined ig/input/resources tree - the same two trees d2w fhir serve serves - and build_project_context assembles the context from them plus the project's [generate] naming, identifier base, and timezone.

Every public name below is importable from dhis2w_fhir itself, which is the package's one stable import surface; the dhis2w_fhir.conversion path answers the same objects.

schemas

Conversion schemas: the translation context, the outcome taxonomy, and the typed DHIS2 payloads.

Every type the QR -> DHIS2 translator reads or writes lives here. The context side (ConversionNaming, OptionTable, QuestionSpec, FormSpec, ConversionContext) is what a caller assembles once from the compiled IG artifacts; the outcome side (ConversionNote, ConversionRefusal, ConversionResult, ConversionReport) is what one translated response answers with.

The DHIS2 payloads themselves are the generated OpenAPI models: DataValueSet / DataValue for the aggregate envelope, TrackerEvent / TrackerDataValue for both event kinds, and TrackerTrackedEntity / TrackerEnrollment / TrackerAttribute for the registration one, and TrackerEnrollment on its own for the registration that enrols a person the instance already holds. Nothing is hand-rolled - those schemas carry every field the import endpoints read, and every wire value they carry is a string, so a lexical decimal and a DHIS2 option code survive untouched.

Attributes

CONCEPT_CODE_TIER = 'concept-code' module-attribute

The spelling the contract asks for: the concept code the served CodeSystem publishes.

OPTION_UID_TIER = 'option-uid' module-attribute

The first lenient fall-back: the DHIS2 UID of the option, sent against a code-mode CodeSystem.

OPTION_CODE_TIER = 'option-code' module-attribute

The second lenient fall-back: the DHIS2 code of the option, sent against an id-mode CodeSystem.

Classes

ConversionTargetKind

Bases: StrEnum

Which DHIS2 import payload one translated response becomes.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionTargetKind(StrEnum):
    """Which DHIS2 import payload one translated response becomes."""

    #: The `/api/dataValueSets` envelope a data set's response reports as.
    DATA_VALUE_SET = "data-value-set"

    #: The `/api/tracker` event an event program's response reports as.
    EVENT = "event"

    #: The `/api/tracker` tracked entity a person-only response creates, with no enrollment on it.
    TRACKED_ENTITY = "tracked-entity"

    #: The `/api/tracker` tracked entity and enrollment a tracker registration response creates.
    TRACKER = "tracker"

    #: The `/api/tracker` enrollment alone a registration naming a person the instance already holds creates.
    TRACKER_ENROLLMENT = "tracker-enrollment"

    #: The `/api/tracker` event a tracker program stage's response reports as, on its enrollment.
    TRACKER_EVENT = "tracker-event"

CodedAnswerMode

Bases: StrEnum

How exactly a coded answer has to name its concept before the translator accepts it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class CodedAnswerMode(StrEnum):
    """How exactly a coded answer has to name its concept before the translator accepts it."""

    #: Only the concept code the served CodeSystem publishes resolves; anything else is refused.
    STRICT = "strict"

    #: The concept code resolves first, then the DHIS2 option UID, then the DHIS2 option code, each noted.
    LENIENT = "lenient"

WireValueKind

Bases: StrEnum

How one question's answers are serialised onto the DHIS2 wire.

Derived once, at context build, from the question's R4 item type, whether it repeats, whether it binds terminology, and - when the caller supplied one - the DHIS2 value type behind it. Every serialisation branch dispatches on this and on nothing else.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class WireValueKind(StrEnum):
    """How one question's answers are serialised onto the DHIS2 wire.

    Derived once, at context build, from the question's R4 item type, whether it repeats, whether
    it binds terminology, and - when the caller supplied one - the DHIS2 value type behind it.
    Every serialisation branch dispatches on this and on nothing else.
    """

    INTEGER = "integer"
    DECIMAL = "decimal"
    BOOLEAN = "boolean"
    TRUE_ONLY = "true-only"
    TEXT = "text"
    CODED = "coded"
    MULTI_TEXT = "multi-text"
    DATE = "date"
    DATE_TIME = "date-time"
    TIME = "time"
    URI = "uri"
    ORGANISATION_UNIT = "organisation-unit"
    ATTACHMENT = "attachment"

ConversionNoteCategory

Bases: StrEnum

The kind of interpretation one conversion note records, which is what makes a note reasonable about.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionNoteCategory(StrEnum):
    """The kind of interpretation one conversion note records, which is what makes a note reasonable about."""

    #: A coded answer resolved on a spelling the contract does not ask for (lenient mode only).
    CODED_ANSWER_FALLBACK = "coded-answer-fallback"

    #: A coded answer's terminology is not in the context, so its code went to DHIS2 unchecked.
    CODED_ANSWER_UNCHECKED = "coded-answer-unchecked"

    #: A code-mode concept whose code fell back to the UID, so the DHIS2 option code is unrecoverable.
    OPTION_CODE_UNRECOVERABLE = "option-code-unrecoverable"

    #: A `TRUE_ONLY` question answered `false`, which DHIS2 spells as no data value at all.
    TRUE_ONLY_FALSE_DROPPED = "true-only-false-dropped"

    #: The response status maps onto a DHIS2 event status that several statuses map forward onto.
    STATUS_COLLAPSED = "status-collapsed"

    #: A zoned R4 timestamp read back to the zone-less wall clock DHIS2 stores (BUGS.md #62).
    WALL_CLOCK_DERIVED = "wall-clock-derived"

    #: A timestamp carrying no offset, taken as already being the wall clock DHIS2 stores.
    TIMESTAMP_UNZONED = "timestamp-unzoned"

    #: The D2Period date range disagrees with the ISO period it rides beside; the ISO period wins.
    PERIOD_RANGE_IGNORED = "period-range-ignored"

    #: The context carries no Location table, so a `Location/<id>` reference is read as a DHIS2 UID.
    ORGANISATION_UNIT_ASSUMED = "organisation-unit-assumed"

    #: The context knows no DHIS2 value type for a boolean question, so it is read as `BOOLEAN`.
    BOOLEAN_VALUE_TYPE_ASSUMED = "boolean-value-type-assumed"

    #: The response reports itself `completed`, so the tuple it imports is registered complete.
    COMPLETENESS_CLAIMED = "completeness-claimed"

    #: The response reports itself `in-progress`, so its values import and no completeness is claimed.
    COMPLETENESS_NOT_CLAIMED = "completeness-not-claimed"

    #: A tracker response carries a subject reference, which its tracked-entity identifier supersedes.
    SUBJECT_REFERENCE_IGNORED = "subject-reference-ignored"

    #: An item answering a group's link id, which carries no data value of its own.
    GROUP_ITEM_IGNORED = "group-item-ignored"

    #: A response names an attribute option combo its form declares no vocabulary for, so it is not written.
    ATTRIBUTE_OPTION_COMBO_IGNORED = "attribute-option-combo-ignored"

ConversionRefusalCategory

Bases: StrEnum

The kind of refusal one untranslatable response raised, named so a caller can route it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionRefusalCategory(StrEnum):
    """The kind of refusal one untranslatable response raised, named so a caller can route it."""

    #: The response declares no DHIS2 form type, so there is no contract to translate it under.
    NO_FORM_TYPE = "no-form-type"

    #: The response answers a questionnaire canonical the context holds no form for.
    UNKNOWN_FORM = "unknown-form"

    #: The response declares one form kind and answers a form of another.
    FORM_KIND_MISMATCH = "form-kind-mismatch"

    #: The form's Questionnaire carries no DHIS2 identifier for the object it was generated from.
    MISSING_TARGET_IDENTIFIER = "missing-target-identifier"

    #: An answered item names a link id the form does not ask.
    UNKNOWN_LINK_ID = "unknown-link-id"

    #: An answer carries a `value[x]` element the question does not answer on.
    ANSWER_ELEMENT_MISMATCH = "answer-element-mismatch"

    #: An answered item carries no value at all.
    MISSING_ANSWER_VALUE = "missing-answer-value"

    #: A question storing one DHIS2 data value was answered more than once.
    REPEATED_ANSWER = "repeated-answer"

    #: An answer carries a value DHIS2 has no wire spelling for.
    UNSUPPORTED_ANSWER_VALUE = "unsupported-answer-value"

    #: An aggregate response carries no D2Period extension.
    MISSING_PERIOD = "missing-period"

    #: The form declares an attribute-option-combo vocabulary and the response names no concept of it.
    MISSING_ATTRIBUTE_OPTION_COMBO = "missing-attribute-option-combo"

    #: The response names an attribute option combo the declared vocabulary holds no concept for.
    UNRESOLVABLE_ATTRIBUTE_OPTION_COMBO = "unresolvable-attribute-option-combo"

    #: An aggregate response's ISO period does not read as a DHIS2 period.
    MALFORMED_PERIOD = "malformed-period"

    #: The response names no organisation unit where its kind requires one.
    MISSING_ORGANISATION_UNIT = "missing-organisation-unit"

    #: The response names a Location the context resolves to no DHIS2 organisation unit.
    UNRESOLVABLE_ORGANISATION_UNIT = "unresolvable-organisation-unit"

    #: A coded answer carries no code.
    MISSING_CODING = "missing-coding"

    #: A coded answer names a code the question's terminology does not hold.
    UNRESOLVABLE_CODING = "unresolvable-coding"

    #: A coded answer names a code more than one option of the terminology carries.
    AMBIGUOUS_CODING = "ambiguous-coding"

    #: A tracker response names no tracked entity.
    MISSING_SUBJECT = "missing-subject"

    #: A tracker response names no enrollment.
    MISSING_ENROLLMENT = "missing-enrollment"

    #: A registration form's Questionnaire names no tracked entity type, so there is nothing to enrol a person as.
    MISSING_TRACKED_ENTITY_TYPE = "missing-tracked-entity-type"

    #: A registration response states no enrollment date, which DHIS2 requires of every enrollment.
    MISSING_ENROLLMENT_DATE = "missing-enrollment-date"

    #: A registration naming a person the instance already holds answers a question of that person's own record.
    ENTITY_LEVEL_ANSWER_ON_EXISTING_SUBJECT = "entity-level-answer-on-existing-subject"

    #: A registration response's enrollment or incident date does not read as an instant.
    MALFORMED_ENROLLMENT_DATE = "malformed-enrollment-date"

    #: An event response records no `authored` instant, which is the moment the event occurred.
    MISSING_OCCURRENCE = "missing-occurrence"

    #: The response status names something DHIS2 has no event status for.
    UNMAPPABLE_STATUS = "unmappable-status"

    #: The response reports itself `entered-in-error`: a withdrawal of something already recorded,
    #: which is a deletion rather than an import. Its own category because it is the one refusal no
    #: change to the guide and no change to the data can ever resolve - see
    #: `docs/fhir/design/data-lifecycle.md` - so the forwarder files it instead of retrying it.
    ENTERED_IN_ERROR_IS_A_DELETION = "entered-in-error-is-a-deletion"

ConversionNote

Bases: BaseModel

One thing the translator had to interpret, recorded rather than left silent.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionNote(BaseModel):
    """One thing the translator had to interpret, recorded rather than left silent."""

    model_config = ConfigDict(frozen=True)

    category: ConversionNoteCategory
    message: str
    link_id: str | None = None
    """The question the note is about, or None when it is about the response as a whole."""
Attributes

The question the note is about, or None when it is about the response as a whole.

ConversionRefusal

Bases: BaseModel

One reason a response was not translated, naming the element it stumbled on.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionRefusal(BaseModel):
    """One reason a response was not translated, naming the element it stumbled on."""

    model_config = ConfigDict(frozen=True)

    category: ConversionRefusalCategory
    reason: str
    link_id: str | None = None
    """The question the refusal is about, or None when it is about the response as a whole."""

    element: str | None = None
    """The FHIR element or DHIS2 field the refusal names, when one is narrower than the link id."""
Attributes

The question the refusal is about, or None when it is about the response as a whole.

element = None class-attribute instance-attribute

The FHIR element or DHIS2 field the refusal names, when one is narrower than the link id.

ConversionNaming

Bases: BaseModel

The extension urls and identifier systems one project's responses and forms are written in.

Every name is derived from fhir.toml - the IG canonical for the extensions the response profiles pin, and [generate] identifier_system_base for the DHIS2 identifier systems a response names its tracked entity, its enrollment, its organisation unit, and its form under.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionNaming(BaseModel):
    """The extension urls and identifier systems one project's responses and forms are written in.

    Every name is derived from `fhir.toml` - the IG canonical for the extensions the response
    profiles pin, and `[generate] identifier_system_base` for the DHIS2 identifier systems a
    response names its tracked entity, its enrollment, its organisation unit, and its form under.
    """

    model_config = ConfigDict(frozen=True)

    form_type_url: str
    period_url: str
    organisation_unit_url: str
    tracker_enrollment_url: str
    enrolled_at_url: str
    """Canonical of the extension a registration response dates the enrollment it mints from."""

    incident_at_url: str
    """Canonical of the extension a registration response dates the incident that enrollment follows."""

    entity_level_url: str
    """Canonical of the item extension a registration question states which DHIS2 level it is imported at on."""

    subject_exists_url: str
    """Canonical of the extension a registration response states that its subject is already held on."""

    program_rule_url: str
    """Canonical of the extension a form publishes the DHIS2 program rules it does not itself express on."""

    attribute_option_combos_url: str
    """Canonical of the Questionnaire extension a form declares its attribute-option-combo ValueSet on."""

    attribute_option_combo_url: str
    """Canonical of the QuestionnaireResponse extension one response names its attribute option combo on."""

    organisation_unit_system: str
    data_set_system: str
    program_system: str
    program_stage_system: str
    tracked_entity_type_system: str
    """The DHIS2 identifier system a registration form names the type it enrols a person as under."""

    tracked_entity_system: str
    tracker_enrollment_system: str
    option_code_system: str
    """The DHIS2 identifier namespace an option-set ConceptMap carries its option codes onto."""

    option_uid_system: str
    """The DHIS2 identifier namespace an option-set ConceptMap carries its option UIDs onto."""

    attribute_option_combo_uid_system: str
    """The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo UIDs onto."""

    attribute_option_combo_code_system: str
    """The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo codes onto."""

    @classmethod
    def from_config(cls, config: GenerateConfig, canonical: str) -> ConversionNaming:
        """Derive every conversion name from the IG canonical plus the `[generate]` naming tokens and base."""
        names = FoundationNaming.from_naming(config.naming)
        base = config.identifier_system_base
        return cls(
            form_type_url=_definition_url(canonical, names.form_type_extension_id),
            period_url=_definition_url(canonical, names.period_extension_id),
            organisation_unit_url=_definition_url(canonical, names.organisation_unit_extension_id),
            tracker_enrollment_url=_definition_url(canonical, names.tracker_enrollment_extension_id),
            enrolled_at_url=_definition_url(canonical, names.enrolled_at_extension_id),
            incident_at_url=_definition_url(canonical, names.incident_at_extension_id),
            entity_level_url=_definition_url(canonical, names.entity_level_extension_id),
            subject_exists_url=_definition_url(canonical, names.subject_exists_extension_id),
            program_rule_url=_definition_url(canonical, names.program_rule_extension_id),
            attribute_option_combos_url=_definition_url(canonical, names.attribute_option_combos_extension_id),
            attribute_option_combo_url=_definition_url(canonical, names.attribute_option_combo_extension_id),
            organisation_unit_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["OrgUnit"]),
            data_set_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["DataSet"]),
            program_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["Program"]),
            program_stage_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["ProgramStage"]),
            tracked_entity_type_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackedEntityType"]),
            tracked_entity_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackedEntity"]),
            tracker_enrollment_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackerEnrollment"]),
            option_code_system=_identifier_system(base, _OPTION_CODE_SEGMENT),
            option_uid_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["Option"]),
            attribute_option_combo_uid_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["CategoryOptionCombo"]),
            attribute_option_combo_code_system=_identifier_system(
                base, f"{_SEGMENTS_BY_TOKEN['CategoryOptionCombo']}{_CODE_SEGMENT_SUFFIX}"
            ),
        )

    def target_identifier_system(self, form_kind: FormKind) -> str:
        """The DHIS2 identifier system one form kind's Questionnaire names the object it was generated from under."""
        if form_kind == "aggregate":
            return self.data_set_system
        if form_kind == "tracker-event":
            return self.program_stage_system
        if form_kind == "tracked-entity":
            return self.tracked_entity_type_system
        return self.program_system
Attributes
enrolled_at_url instance-attribute

Canonical of the extension a registration response dates the enrollment it mints from.

incident_at_url instance-attribute

Canonical of the extension a registration response dates the incident that enrollment follows.

entity_level_url instance-attribute

Canonical of the item extension a registration question states which DHIS2 level it is imported at on.

subject_exists_url instance-attribute

Canonical of the extension a registration response states that its subject is already held on.

program_rule_url instance-attribute

Canonical of the extension a form publishes the DHIS2 program rules it does not itself express on.

attribute_option_combos_url instance-attribute

Canonical of the Questionnaire extension a form declares its attribute-option-combo ValueSet on.

attribute_option_combo_url instance-attribute

Canonical of the QuestionnaireResponse extension one response names its attribute option combo on.

tracked_entity_type_system instance-attribute

The DHIS2 identifier system a registration form names the type it enrols a person as under.

option_code_system instance-attribute

The DHIS2 identifier namespace an option-set ConceptMap carries its option codes onto.

option_uid_system instance-attribute

The DHIS2 identifier namespace an option-set ConceptMap carries its option UIDs onto.

attribute_option_combo_uid_system instance-attribute

The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo UIDs onto.

attribute_option_combo_code_system instance-attribute

The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo codes onto.

Methods:
from_config(config, canonical) classmethod

Derive every conversion name from the IG canonical plus the [generate] naming tokens and base.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
@classmethod
def from_config(cls, config: GenerateConfig, canonical: str) -> ConversionNaming:
    """Derive every conversion name from the IG canonical plus the `[generate]` naming tokens and base."""
    names = FoundationNaming.from_naming(config.naming)
    base = config.identifier_system_base
    return cls(
        form_type_url=_definition_url(canonical, names.form_type_extension_id),
        period_url=_definition_url(canonical, names.period_extension_id),
        organisation_unit_url=_definition_url(canonical, names.organisation_unit_extension_id),
        tracker_enrollment_url=_definition_url(canonical, names.tracker_enrollment_extension_id),
        enrolled_at_url=_definition_url(canonical, names.enrolled_at_extension_id),
        incident_at_url=_definition_url(canonical, names.incident_at_extension_id),
        entity_level_url=_definition_url(canonical, names.entity_level_extension_id),
        subject_exists_url=_definition_url(canonical, names.subject_exists_extension_id),
        program_rule_url=_definition_url(canonical, names.program_rule_extension_id),
        attribute_option_combos_url=_definition_url(canonical, names.attribute_option_combos_extension_id),
        attribute_option_combo_url=_definition_url(canonical, names.attribute_option_combo_extension_id),
        organisation_unit_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["OrgUnit"]),
        data_set_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["DataSet"]),
        program_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["Program"]),
        program_stage_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["ProgramStage"]),
        tracked_entity_type_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackedEntityType"]),
        tracked_entity_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackedEntity"]),
        tracker_enrollment_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["TrackerEnrollment"]),
        option_code_system=_identifier_system(base, _OPTION_CODE_SEGMENT),
        option_uid_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["Option"]),
        attribute_option_combo_uid_system=_identifier_system(base, _SEGMENTS_BY_TOKEN["CategoryOptionCombo"]),
        attribute_option_combo_code_system=_identifier_system(
            base, f"{_SEGMENTS_BY_TOKEN['CategoryOptionCombo']}{_CODE_SEGMENT_SUFFIX}"
        ),
    )
target_identifier_system(form_kind)

The DHIS2 identifier system one form kind's Questionnaire names the object it was generated from under.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
def target_identifier_system(self, form_kind: FormKind) -> str:
    """The DHIS2 identifier system one form kind's Questionnaire names the object it was generated from under."""
    if form_kind == "aggregate":
        return self.data_set_system
    if form_kind == "tracker-event":
        return self.program_stage_system
    if form_kind == "tracked-entity":
        return self.tracked_entity_type_system
    return self.program_system

OptionEntry

Bases: BaseModel

One DHIS2 option of a served CodeSystem, in every spelling a coded answer may name it by.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class OptionEntry(BaseModel):
    """One DHIS2 option of a served CodeSystem, in every spelling a coded answer may name it by."""

    model_config = ConfigDict(frozen=True)

    concept_code: str
    """The code the served CodeSystem publishes the option under - what the contract asks for."""

    option_uid: str
    option_code: str | None = None
    """The DHIS2 option code, when the artifacts carry it; None when only the UID is recoverable."""

    @property
    def wire_value(self) -> str:
        """What a DHIS2 data value stores for this option: its code, falling back to its UID."""
        return self.option_code or self.option_uid
Attributes
concept_code instance-attribute

The code the served CodeSystem publishes the option under - what the contract asks for.

option_code = None class-attribute instance-attribute

The DHIS2 option code, when the artifacts carry it; None when only the UID is recoverable.

wire_value property

What a DHIS2 data value stores for this option: its code, falling back to its UID.

ResolvedOption

Bases: BaseModel

One option a received code resolved to, and which spelling of it matched.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ResolvedOption(BaseModel):
    """One option a received code resolved to, and which spelling of it matched."""

    model_config = ConfigDict(frozen=True)

    entry: OptionEntry
    matched_by: str
    """`concept-code`, `option-uid`, or `option-code` - which of the three tiers the code hit."""

    @property
    def matched_contract_spelling(self) -> bool:
        """Whether the received code was the concept code the contract asks for, rather than a fall-back."""
        return self.matched_by == CONCEPT_CODE_TIER
Attributes
matched_by instance-attribute

concept-code, option-uid, or option-code - which of the three tiers the code hit.

matched_contract_spelling property

Whether the received code was the concept code the contract asks for, rather than a fall-back.

OptionLookup

Bases: BaseModel

The outcome of resolving one received code against a served option table.

Exactly one of the three states holds: option set is a resolution, ambiguous_option_uids non-empty is a code the table carries more than once at one tier, and both empty is a code the table does not carry at all.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class OptionLookup(BaseModel):
    """The outcome of resolving one received code against a served option table.

    Exactly one of the three states holds: `option` set is a resolution, `ambiguous_option_uids`
    non-empty is a code the table carries more than once at one tier, and both empty is a code
    the table does not carry at all.
    """

    model_config = ConfigDict(frozen=True)

    option: ResolvedOption | None = None
    ambiguous_option_uids: tuple[str, ...] = ()

OptionTable

Bases: BaseModel

Every option of one served CodeSystem, resolvable by each of the three spellings it can arrive as.

The table is the inverse of what the terminology target emitted. Under concept_code_source = "id" the concept code is the option UID and the dhis2-code property carries the DHIS2 code; under "code" the concept code is the DHIS2 code and the dhis2-id property carries the UID. Both spellings identify the same option, which is why a lenient resolution accepts either and notes what it did.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class OptionTable(BaseModel):
    """Every option of one served CodeSystem, resolvable by each of the three spellings it can arrive as.

    The table is the inverse of what the terminology target emitted. Under
    `concept_code_source = "id"` the concept code is the option UID and the `dhis2-code` property
    carries the DHIS2 code; under `"code"` the concept code is the DHIS2 code and the `dhis2-id`
    property carries the UID. Both spellings identify the same option, which is why a lenient
    resolution accepts either and notes what it did.
    """

    model_config = ConfigDict(frozen=True)

    system: str
    entries: tuple[OptionEntry, ...] = ()

QuestionSpec

Bases: BaseModel

One answerable question of a served form, as the translator writes its DHIS2 data value.

data_element_uid and category_option_combo_uid come straight from the link-id grammar: a plain question's link id is the data element UID, and a disaggregated aggregate cell's is <dataElement>.<categoryOptionCombo> - the very key a DHIS2 data value carries. A tracker registration form asks tracked entity attributes rather than data elements, and its link ids are attribute UIDs, so data_element_uid is the attribute a TrackerAttribute is keyed by.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class QuestionSpec(BaseModel):
    """One answerable question of a served form, as the translator writes its DHIS2 data value.

    `data_element_uid` and `category_option_combo_uid` come straight from the link-id grammar:
    a plain question's link id is the data element UID, and a disaggregated aggregate cell's is
    `<dataElement>.<categoryOptionCombo>` - the very key a DHIS2 data value carries. A tracker
    registration form asks tracked entity attributes rather than data elements, and its link ids
    are attribute UIDs, so `data_element_uid` is the attribute a `TrackerAttribute` is keyed by.
    """

    model_config = ConfigDict(frozen=True)

    link_id: str
    data_element_uid: str
    category_option_combo_uid: str | None = None
    item_type: str
    answer_element: str
    wire_kind: WireValueKind
    repeats: bool = False
    option_system: str | None = None
    """Canonical of the CodeSystem a coded answer resolves against, or None when the binding is open."""

    value_type: str | None = None
    """The DHIS2 value type behind the question, when the caller supplied one; the compiled IG publishes none."""

    entity_level: bool | None = None
    """Which DHIS2 level a registration answer is imported at, off the question's D2EntityLevel extension.

    True is a tracked entity attribute of the program's tracked entity type, whose value is stated
    on the tracked entity; False is an attribute only the program asks, whose value is stated on
    the enrollment the registration creates. None is a form that states no level - every other form
    kind, and a registration form published before the guide carried the extension - and the
    translator then writes the answer on the tracked entity.
    """

    required: bool = False
    """Whether the form makes the question mandatory - `Questionnaire.item.required`.

    On a registration form this is the program's own join saying so: the emitter writes
    `required` from `programTrackedEntityAttributes.mandatory`, which is the grain DHIS2 answers
    `E1018` on. That makes it the one fact that decides whether an entity-level answer may ride
    the enrollment of a person the instance already holds.
    """
Attributes
option_system = None class-attribute instance-attribute

Canonical of the CodeSystem a coded answer resolves against, or None when the binding is open.

value_type = None class-attribute instance-attribute

The DHIS2 value type behind the question, when the caller supplied one; the compiled IG publishes none.

entity_level = None class-attribute instance-attribute

Which DHIS2 level a registration answer is imported at, off the question's D2EntityLevel extension.

True is a tracked entity attribute of the program's tracked entity type, whose value is stated on the tracked entity; False is an attribute only the program asks, whose value is stated on the enrollment the registration creates. None is a form that states no level - every other form kind, and a registration form published before the guide carried the extension - and the translator then writes the answer on the tracked entity.

required = False class-attribute instance-attribute

Whether the form makes the question mandatory - Questionnaire.item.required.

On a registration form this is the program's own join saying so: the emitter writes required from programTrackedEntityAttributes.mandatory, which is the grain DHIS2 answers E1018 on. That makes it the one fact that decides whether an entity-level answer may ride the enrollment of a person the instance already holds.

FormSpec

Bases: BaseModel

One served Questionnaire flattened into what a response answering it translates through.

attribute_option_combo_value_set is the vocabulary the form's D2AttributeOptionCombos extension declares, and its presence is what makes the response-side extension required: a data set on the default category combo declares none, and its values are keyed under the one attribute option combo it has.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class FormSpec(BaseModel):
    """One served Questionnaire flattened into what a response answering it translates through.

    `attribute_option_combo_value_set` is the vocabulary the form's `D2AttributeOptionCombos`
    extension declares, and its presence is what makes the response-side extension required: a
    data set on the default category combo declares none, and its values are keyed under the one
    attribute option combo it has.
    """

    model_config = ConfigDict(frozen=True)

    canonical: str
    form_kind: FormKind
    target_kind: ConversionTargetKind
    data_set_uid: str | None = None
    program_uid: str | None = None
    program_stage_uid: str | None = None
    tracked_entity_type_uid: str | None = None
    """The DHIS2 type a registration form enrols a person as, off the form's `$DHIS2-TET` identifier."""

    questions: dict[str, QuestionSpec] = Field(default_factory=dict)
    group_link_ids: frozenset[str] = frozenset()
    attribute_option_combo_value_set: str | None = None
    """Canonical of the ValueSet the form declares its responses key their values from, or None."""

    attribute_option_combo_system: str | None = None
    """Canonical of the CodeSystem behind that ValueSet, or None when the context does not carry it."""
Attributes
tracked_entity_type_uid = None class-attribute instance-attribute

The DHIS2 type a registration form enrols a person as, off the form's $DHIS2-TET identifier.

attribute_option_combo_value_set = None class-attribute instance-attribute

Canonical of the ValueSet the form declares its responses key their values from, or None.

attribute_option_combo_system = None class-attribute instance-attribute

Canonical of the CodeSystem behind that ValueSet, or None when the context does not carry it.

ConversionContext

Bases: BaseModel

Everything the QR -> DHIS2 translator reads besides the response itself.

Assembled once from the compiled IG artifacts - the served Questionnaires, the option-set CodeSystems (and their ConceptMaps, where a code-mode guide needs them), the ValueSets that bind the two, and the published Locations - plus the two dials a run chooses: how exactly a coded answer has to name its concept, and which IANA zone DHIS2's zone-less timestamps are wall-clock readings in.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionContext(BaseModel):
    """Everything the QR -> DHIS2 translator reads besides the response itself.

    Assembled once from the compiled IG artifacts - the served Questionnaires, the option-set
    CodeSystems (and their ConceptMaps, where a code-mode guide needs them), the ValueSets that
    bind the two, and the published Locations - plus the two dials a run chooses: how exactly a
    coded answer has to name its concept, and which IANA zone DHIS2's zone-less timestamps are
    wall-clock readings in.
    """

    model_config = ConfigDict(frozen=True)

    naming: ConversionNaming
    forms: dict[str, FormSpec] = Field(default_factory=dict)
    """Every served form, keyed by the questionnaire canonical a response names it under."""

    option_tables: dict[str, OptionTable] = Field(default_factory=dict)
    """Every served option terminology, keyed by its CodeSystem canonical."""

    organisation_unit_uids_by_location_id: dict[str, str] = Field(default_factory=dict)
    """The DHIS2 UID behind every published Location id - a code stem under code-or-id naming."""

    coded_answer_mode: CodedAnswerMode = CodedAnswerMode.LENIENT
    timezone: str | None = None

    @property
    def resolves_organisation_units(self) -> bool:
        """Whether the context was given a Location table to resolve organisation-unit references through."""
        return bool(self.organisation_unit_uids_by_location_id)

    def form_for(self, reference: str | None) -> FormSpec | None:
        """The served form one reference names: the questionnaire canonical, or the bare form id.

        `forms` is keyed by the canonical a response carries, and a canonical ends in the form's
        id - so a caller holding the id alone (the segment a UI routes on) resolves here too,
        rather than building a second index. None for nothing named and for nothing served under
        the name, which are the same answer to a translator: no form to read the response against.
        """
        if not reference:
            return None
        direct = self.forms.get(reference)
        if direct is not None:
            return direct
        return next((form for key, form in self.forms.items() if key.rsplit("/", 1)[-1] == reference), None)
Attributes
forms = Field(default_factory=dict) class-attribute instance-attribute

Every served form, keyed by the questionnaire canonical a response names it under.

option_tables = Field(default_factory=dict) class-attribute instance-attribute

Every served option terminology, keyed by its CodeSystem canonical.

organisation_unit_uids_by_location_id = Field(default_factory=dict) class-attribute instance-attribute

The DHIS2 UID behind every published Location id - a code stem under code-or-id naming.

resolves_organisation_units property

Whether the context was given a Location table to resolve organisation-unit references through.

Methods:
form_for(reference)

The served form one reference names: the questionnaire canonical, or the bare form id.

forms is keyed by the canonical a response carries, and a canonical ends in the form's id - so a caller holding the id alone (the segment a UI routes on) resolves here too, rather than building a second index. None for nothing named and for nothing served under the name, which are the same answer to a translator: no form to read the response against.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
def form_for(self, reference: str | None) -> FormSpec | None:
    """The served form one reference names: the questionnaire canonical, or the bare form id.

    `forms` is keyed by the canonical a response carries, and a canonical ends in the form's
    id - so a caller holding the id alone (the segment a UI routes on) resolves here too,
    rather than building a second index. None for nothing named and for nothing served under
    the name, which are the same answer to a translator: no form to read the response against.
    """
    if not reference:
        return None
    direct = self.forms.get(reference)
    if direct is not None:
        return direct
    return next((form for key, form in self.forms.items() if key.rsplit("/", 1)[-1] == reference), None)

ConversionResult

Bases: BaseModel

What one QuestionnaireResponse translated into: a payload, or the reasons there is none.

Exactly one of data_value_set, event, tracked_entity, and enrollment is set when refusals is empty, and target_kind names which. A refused response carries none of them: a response the translator cannot read whole produces a named refusal rather than a partial payload that would import the half of itself that happened to parse.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionResult(BaseModel):
    """What one QuestionnaireResponse translated into: a payload, or the reasons there is none.

    Exactly one of `data_value_set`, `event`, `tracked_entity`, and `enrollment` is set when
    `refusals` is empty, and `target_kind` names which. A refused response carries none of them: a
    response the translator cannot read whole produces a named refusal rather than a partial payload
    that would import the half of itself that happened to parse.
    """

    model_config = ConfigDict(frozen=True)

    response_id: str | None = None
    questionnaire: str | None = None
    target_kind: ConversionTargetKind | None = None
    data_value_set: DataValueSet | None = None
    event: TrackerEvent | None = None
    tracked_entity: TrackerTrackedEntity | None = None
    """The person and the enrollment a registration response creates, carried whole for one `/api/tracker` post."""

    enrollment: TrackerEnrollment | None = None
    """The enrollment alone a registration whose subject the instance already holds creates.

    Set instead of `tracked_entity`, and posted as a top-level `enrollments` array: an enrollment
    that rides inside a `trackedEntities` wrapper rewrites the person's owning organisation unit
    (BUGS.md 73), and a person this response did not create is not this response's to move.
    """

    completeness: CompleteDataSetRegistration | None = None
    """The completeness the aggregate response claims, set only where its `status` is `completed`.

    Carried beside the data value set rather than inside it, because it is a second write to a second
    resource and it only happens once DHIS2 has taken the values. A response reporting itself
    `in-progress` translates its values and carries nothing here.
    """

    notes: tuple[ConversionNote, ...] = ()
    refusals: tuple[ConversionRefusal, ...] = ()

    @property
    def is_refused(self) -> bool:
        """Whether the response produced a refusal instead of a payload."""
        return bool(self.refusals)

    @property
    def payload(self) -> ConversionPayload | None:
        """The payload `target_kind` names, or None when the response was refused.

        The four payload fields are mutually exclusive - the invariant the class docstring states -
        and the target kind is what says which one carries the document, so this is the one place
        that invariant is written down rather than re-derived at every call site. A registration
        chooses between two kinds for that reason: `TRACKER` for the person this response mints,
        with the enrollment nested inside it, and `TRACKER_ENROLLMENT` for the enrollment alone on a
        person the instance already holds. The typed fields stay for the caller wanting one shape.
        """
        match self.target_kind:
            case ConversionTargetKind.DATA_VALUE_SET:
                return self.data_value_set
            case ConversionTargetKind.EVENT | ConversionTargetKind.TRACKER_EVENT:
                return self.event
            case ConversionTargetKind.TRACKED_ENTITY | ConversionTargetKind.TRACKER:
                return self.tracked_entity
            case ConversionTargetKind.TRACKER_ENROLLMENT:
                return self.enrollment
            case None:
                return None

    def payload_of[PayloadT: ConversionPayload](self, wire_shape: type[PayloadT]) -> PayloadT | None:
        """This result's payload when its target kind produced the wire shape asked for, else None.

        The narrowing accessor beside `payload`: a caller that only acts on aggregate envelopes asks
        for `DataValueSet` and reads a typed answer, rather than testing a field that a tracker
        result leaves unset for reasons of its own.
        """
        payload = self.payload
        return payload if isinstance(payload, wire_shape) else None
Attributes
tracked_entity = None class-attribute instance-attribute

The person and the enrollment a registration response creates, carried whole for one /api/tracker post.

enrollment = None class-attribute instance-attribute

The enrollment alone a registration whose subject the instance already holds creates.

Set instead of tracked_entity, and posted as a top-level enrollments array: an enrollment that rides inside a trackedEntities wrapper rewrites the person's owning organisation unit (BUGS.md 73), and a person this response did not create is not this response's to move.

completeness = None class-attribute instance-attribute

The completeness the aggregate response claims, set only where its status is completed.

Carried beside the data value set rather than inside it, because it is a second write to a second resource and it only happens once DHIS2 has taken the values. A response reporting itself in-progress translates its values and carries nothing here.

is_refused property

Whether the response produced a refusal instead of a payload.

payload property

The payload target_kind names, or None when the response was refused.

The four payload fields are mutually exclusive - the invariant the class docstring states - and the target kind is what says which one carries the document, so this is the one place that invariant is written down rather than re-derived at every call site. A registration chooses between two kinds for that reason: TRACKER for the person this response mints, with the enrollment nested inside it, and TRACKER_ENROLLMENT for the enrollment alone on a person the instance already holds. The typed fields stay for the caller wanting one shape.

Methods:
payload_of(wire_shape)

This result's payload when its target kind produced the wire shape asked for, else None.

The narrowing accessor beside payload: a caller that only acts on aggregate envelopes asks for DataValueSet and reads a typed answer, rather than testing a field that a tracker result leaves unset for reasons of its own.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
def payload_of[PayloadT: ConversionPayload](self, wire_shape: type[PayloadT]) -> PayloadT | None:
    """This result's payload when its target kind produced the wire shape asked for, else None.

    The narrowing accessor beside `payload`: a caller that only acts on aggregate envelopes asks
    for `DataValueSet` and reads a typed answer, rather than testing a field that a tracker
    result leaves unset for reasons of its own.
    """
    payload = self.payload
    return payload if isinstance(payload, wire_shape) else None

ConversionReport

Bases: BaseModel

The outcome of translating a batch of spooled responses, in the order they were drained.

The order here is the spool's, not the posting order: a caller draining into DHIS2 posts FORWARD_TARGET_ORDER so a registration lands before the stage events of the same drain.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
class ConversionReport(BaseModel):
    """The outcome of translating a batch of spooled responses, in the order they were drained.

    The order here is the spool's, not the posting order: a caller draining into DHIS2 posts
    `FORWARD_TARGET_ORDER` so a registration lands before the stage events of the same drain.
    """

    model_config = ConfigDict(frozen=True)

    results: tuple[ConversionResult, ...] = ()

    @property
    def translated(self) -> tuple[ConversionResult, ...]:
        """Every response that produced a payload."""
        return tuple(result for result in self.results if not result.is_refused)

    @property
    def refused(self) -> tuple[ConversionResult, ...]:
        """Every response that produced a refusal."""
        return tuple(result for result in self.results if result.is_refused)

    @property
    def data_value_sets(self) -> tuple[DataValueSet, ...]:
        """Every aggregate envelope the batch produced, ready to post to `/api/dataValueSets`."""
        return self.payloads_of(DataValueSet)

    @property
    def events(self) -> tuple[TrackerEvent, ...]:
        """Every event the batch produced, ready to post to `/api/tracker`."""
        return self.payloads_of(TrackerEvent)

    @property
    def tracked_entities(self) -> tuple[TrackerTrackedEntity, ...]:
        """Every registration the batch produced, ready to post to `/api/tracker` before its events."""
        return self.payloads_of(TrackerTrackedEntity)

    @property
    def enrollments(self) -> tuple[TrackerEnrollment, ...]:
        """Every enrollment-only payload the batch produced, for the people the instance already holds."""
        return self.payloads_of(TrackerEnrollment)

    def payloads_of[PayloadT: ConversionPayload](self, wire_shape: type[PayloadT]) -> tuple[PayloadT, ...]:
        """Every payload of one wire shape the batch produced, in the order the responses were drained.

        The four properties above are this method under the names a caller posts them by; a refused
        response contributes nothing, because a refusal is the absence of a payload.
        """
        return tuple(payload for result in self.results if (payload := result.payload_of(wire_shape)) is not None)
Attributes
translated property

Every response that produced a payload.

refused property

Every response that produced a refusal.

data_value_sets property

Every aggregate envelope the batch produced, ready to post to /api/dataValueSets.

events property

Every event the batch produced, ready to post to /api/tracker.

tracked_entities property

Every registration the batch produced, ready to post to /api/tracker before its events.

enrollments property

Every enrollment-only payload the batch produced, for the people the instance already holds.

Methods:
payloads_of(wire_shape)

Every payload of one wire shape the batch produced, in the order the responses were drained.

The four properties above are this method under the names a caller posts them by; a refused response contributes nothing, because a refusal is the absence of a payload.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
def payloads_of[PayloadT: ConversionPayload](self, wire_shape: type[PayloadT]) -> tuple[PayloadT, ...]:
    """Every payload of one wire shape the batch produced, in the order the responses were drained.

    The four properties above are this method under the names a caller posts them by; a refused
    response contributes nothing, because a refusal is the absence of a payload.
    """
    return tuple(payload for result in self.results if (payload := result.payload_of(wire_shape)) is not None)

context

Assembling the translation context from the compiled IG artifacts a project publishes.

The translator reads one thing besides the response: a ConversionContext. This module builds it from the very documents d2w fhir serve holds - the served Questionnaires, the option-set CodeSystems, the ValueSets binding a question to its terminology, the ConceptMaps taking a concept code back to its DHIS2 option code, and the published Locations whose ids are identity stems rather than DHIS2 UIDs.

Two things the compiled IG deliberately does not publish are threaded in by the caller. The DHIS2 value type behind a question is one: a Questionnaire states an R4 item type, and BOOLEAN and TRUE_ONLY share #boolean, so a caller holding the instance's metadata passes value_types_by_data_element and the translator writes TRUE_ONLY values the way DHIS2 stores them. Without it a boolean question is read as BOOLEAN and the response carries a note saying so. The other is the project timezone, which is what a zoned R4 timestamp is read back through (BUGS.md #62).

A tracker registration form's questions are the program's tracked entity attributes, and they are read through the very same walk: an attribute has the DHIS2 value types a data element has, binds option sets the same way, and lands on the same QuestionSpec. value_types_by_data_element is keyed by whichever object the link ids name, so a caller wanting TRUE_ONLY right on a registration form passes the attributes' value types in the same table.

One thing a registration question does carry that a data element's cannot: the D2EntityLevel extension, which says whether DHIS2 imports its answer onto the tracked entity or onto the enrollment. It is read off the item here, so the translator splits the answers by what the guide published rather than by anything the caller has to supply.

Classes

ConversionContextError

Bases: ValueError

Raised when a compiled artifact cannot be read into the context the translator needs.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
class ConversionContextError(ValueError):
    """Raised when a compiled artifact cannot be read into the context the translator needs."""

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)
        self.diagnostics = diagnostics

Functions:

build_conversion_context(naming, questionnaires, *, code_systems=(), value_sets=(), concept_maps=(), locations=(), value_types_by_data_element=None, coded_answer_mode=CodedAnswerMode.LENIENT, timezone=None)

Read one project's compiled artifacts into the context every response is translated through.

questionnaires decides which responses can be translated at all - a canonical absent here is a refusal, not a guess. value_sets binds a #choice question to the CodeSystem behind its answerValueSet, and code_systems (refined by concept_maps where a code-mode guide needs the DHIS2 option code back) is what a coded answer resolves against. locations is what turns a Location/<id> reference into the DHIS2 organisation unit UID it stands for, which under code-or-id naming is not the id itself.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
def build_conversion_context(
    naming: ConversionNaming,
    questionnaires: Sequence[Questionnaire],
    *,
    code_systems: Sequence[CodeSystem] = (),
    value_sets: Sequence[ValueSet] = (),
    concept_maps: Sequence[ConceptMap] = (),
    locations: Sequence[Location] = (),
    value_types_by_data_element: dict[str, str] | None = None,
    coded_answer_mode: CodedAnswerMode = CodedAnswerMode.LENIENT,
    timezone: str | None = None,
) -> ConversionContext:
    """Read one project's compiled artifacts into the context every response is translated through.

    `questionnaires` decides which responses can be translated at all - a canonical absent here is
    a refusal, not a guess. `value_sets` binds a `#choice` question to the CodeSystem behind its
    `answerValueSet`, and `code_systems` (refined by `concept_maps` where a code-mode guide needs
    the DHIS2 option code back) is what a coded answer resolves against. `locations` is what turns
    a `Location/<id>` reference into the DHIS2 organisation unit UID it stands for, which under
    code-or-id naming is not the id itself.
    """
    code_system_urls = _code_system_urls_by_value_set(value_sets)
    tables = {
        table.system: table
        for table in (build_option_table(code_system, naming, concept_maps) for code_system in code_systems)
    }
    forms = {}
    for questionnaire in questionnaires:
        form = build_form_spec(questionnaire, naming, code_system_urls, value_types_by_data_element or {})
        forms[form.canonical] = form
    return ConversionContext(
        naming=naming,
        forms=forms,
        option_tables=tables,
        organisation_unit_uids_by_location_id=_organisation_unit_uids(locations, naming),
        coded_answer_mode=coded_answer_mode,
        timezone=timezone,
    )

build_form_spec(questionnaire, naming, code_system_urls_by_value_set, value_types_by_data_element)

Flatten one served Questionnaire into the form spec a response answering it translates through.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
def build_form_spec(
    questionnaire: Questionnaire,
    naming: ConversionNaming,
    code_system_urls_by_value_set: dict[str, str],
    value_types_by_data_element: dict[str, str],
) -> FormSpec:
    """Flatten one served Questionnaire into the form spec a response answering it translates through."""
    canonical = questionnaire.url
    if not canonical:
        raise ConversionContextError(f"the served {_QUESTIONNAIRE_RESOURCE_TYPE} carries no canonical url")
    form_kind = _form_kind(questionnaire, naming, canonical)
    questions: dict[str, QuestionSpec] = {}
    group_link_ids: set[str] = set()
    _walk(
        questionnaire.item or [],
        naming,
        code_system_urls_by_value_set,
        value_types_by_data_element,
        questions,
        group_link_ids,
    )
    target_uid = _identifier_value(questionnaire, naming.target_identifier_system(form_kind))
    value_set = _attribute_option_combo_value_set(questionnaire, naming)
    return FormSpec(
        canonical=canonical,
        form_kind=form_kind,
        target_kind=TARGET_KINDS_BY_FORM_KIND[form_kind],
        data_set_uid=target_uid if form_kind == "aggregate" else None,
        program_uid=target_uid if form_kind == "event" else _identifier_value(questionnaire, naming.program_system),
        program_stage_uid=target_uid if form_kind == "tracker-event" else None,
        tracked_entity_type_uid=_identifier_value(questionnaire, naming.tracked_entity_type_system),
        questions=questions,
        group_link_ids=frozenset(group_link_ids),
        attribute_option_combo_value_set=value_set,
        attribute_option_combo_system=code_system_urls_by_value_set.get(value_set or ""),
    )

build_option_table(code_system, naming, concept_maps=())

Read one served CodeSystem into the option table a coded answer resolves against.

The concept properties carry the complementary DHIS2 identifier both ways round, so the table is complete under concept_code_source = "id" from the CodeSystem alone. Under "code" an option whose DHIS2 code was not a usable concept code took the UID instead, and only the set's ConceptMap still knows the code - which is why the maps refine the table where they are given.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
def build_option_table(
    code_system: CodeSystem,
    naming: ConversionNaming,
    concept_maps: Sequence[ConceptMap] = (),
) -> OptionTable:
    """Read one served CodeSystem into the option table a coded answer resolves against.

    The concept properties carry the complementary DHIS2 identifier both ways round, so the table
    is complete under `concept_code_source = "id"` from the CodeSystem alone. Under `"code"` an
    option whose DHIS2 code was not a usable concept code took the UID instead, and only the set's
    ConceptMap still knows the code - which is why the maps refine the table where they are given.
    """
    system = code_system.url
    if not system:
        raise ConversionContextError("a served CodeSystem carries no canonical url")
    read = (_option_entry(concept) for concept in code_system.concept or [])
    entries = [entry for entry in read if entry is not None]
    return OptionTable(system=system, entries=_mapped_entries(entries, system, naming, concept_maps))

artifacts

Reading one project's published IG into the translation context a forward run drains through.

build_conversion_context takes plain R4 models; this module is where a project's guide becomes them. collect_artifacts is the one place documents are sorted into the five resource types the translator reads - Questionnaire, CodeSystem, ValueSet, ConceptMap, and Location - and everything else the guide publishes is passed over, because nothing in the QR -> DHIS2 direction consults it.

Two ways a guide reaches it. load_compiled_artifacts reads the two trees d2w fhir serve serves as one: ig/fsh-generated/resources, which SUSHI compiled from the emitted FSH, and ig/input/resources, the predefined registry, terminology, and ConceptMap tree the generate targets wrote as JSON. A project that has never run SUSHI has neither, and dhis2w_fhir.service.fetch_live_artifacts builds the same documents off the instance instead - the same builders d2w fhir serve --live answers reads from - so a capture UI that needed no build step drains through a forward that needs none either.

How an unreadable document is handled depends on what it is. A Questionnaire the R4 models cannot read fails the collection naming its source: a form quietly skipped turns every response answering it into an unknown-form refusal, which reads as a problem with the data rather than with the guide. The other four cost only what they carry - a coded answer resolved unchecked, an organisation-unit reference read as a UID - so an unreadable one is left out and named on unreadable_resources, which the caller reports. A guide is free to hand-write terminology this package has no model for, and one such document is not worth refusing a whole spool over.

Classes

CompiledIgMissingError

Bases: LookupError

Raised when a project has no compiled IG to translate captured responses against.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class CompiledIgMissingError(LookupError):
    """Raised when a project has no compiled IG to translate captured responses against."""

    def __init__(self, directory: Path) -> None:
        """Carry the refusal naming the missing directory and the two commands that fill it."""
        super().__init__(
            f"no compiled IG at {directory} - run `d2w fhir generate`, then `make sushi` in the project, "
            "and forward again."
        )
Methods:
__init__(directory)

Carry the refusal naming the missing directory and the two commands that fill it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def __init__(self, directory: Path) -> None:
    """Carry the refusal naming the missing directory and the two commands that fill it."""
    super().__init__(
        f"no compiled IG at {directory} - run `d2w fhir generate`, then `make sushi` in the project, "
        "and forward again."
    )

CompiledArtifactReadError

Bases: ValueError

Raised when a published resource the translator reads cannot be parsed as its R4 model.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class CompiledArtifactReadError(ValueError):
    """Raised when a published resource the translator reads cannot be parsed as its R4 model."""

CompiledArtifacts

Bases: BaseModel

Everything one published guide holds that the QR -> DHIS2 translator reads.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class CompiledArtifacts(BaseModel):
    """Everything one published guide holds that the QR -> DHIS2 translator reads."""

    model_config = ConfigDict(frozen=True)

    questionnaires: tuple[Questionnaire, ...] = ()
    code_systems: tuple[CodeSystem, ...] = ()
    value_sets: tuple[ValueSet, ...] = ()
    concept_maps: tuple[ConceptMap, ...] = ()
    locations: tuple[Location, ...] = ()
    unreadable_resources: tuple[str, ...] = ()
    """One line per non-form document left out because the R4 models could not read it, naming the file."""

    @property
    def resource_count(self) -> int:
        """How many published resources were kept, across the five types."""
        return (
            len(self.questionnaires)
            + len(self.code_systems)
            + len(self.value_sets)
            + len(self.concept_maps)
            + len(self.locations)
        )
Attributes
unreadable_resources = () class-attribute instance-attribute

One line per non-form document left out because the R4 models could not read it, naming the file.

resource_count property

How many published resources were kept, across the five types.

SourcedDocument

Bases: BaseModel

One wire document paired with whatever names it in a diagnostic - a file path, or a builder.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class SourcedDocument(BaseModel):
    """One wire document paired with whatever names it in a diagnostic - a file path, or a builder."""

    model_config = ConfigDict(frozen=True)

    source: str
    body: dict[str, Any]

BoundQuestionUids

Bases: BaseModel

The DHIS2 objects one published guide asks questions from, split by the endpoint that types them.

Both halves feed the same value_types_by_data_element table the context takes, because a link id names one object whichever kind of form carries it - the split exists only because a data element's value type is read from /api/dataElements and an attribute's from /api/trackedEntityAttributes.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class BoundQuestionUids(BaseModel):
    """The DHIS2 objects one published guide asks questions from, split by the endpoint that types them.

    Both halves feed the same `value_types_by_data_element` table the context takes, because a link
    id names one object whichever kind of form carries it - the split exists only because a data
    element's value type is read from `/api/dataElements` and an attribute's from
    `/api/trackedEntityAttributes`.
    """

    model_config = ConfigDict(frozen=True)

    data_element_uids: tuple[str, ...] = ()
    tracked_entity_attribute_uids: tuple[str, ...] = ()

    @property
    def total(self) -> int:
        """How many DHIS2 objects the published forms ask a question from, across both kinds."""
        return len(self.data_element_uids) + len(self.tracked_entity_attribute_uids)
Attributes
total property

How many DHIS2 objects the published forms ask a question from, across both kinds.

ProgramRuleNames

Bases: BaseModel

The DHIS2 program rules a published guide names, keyed by UID, for reading a refusal back.

DHIS2 refuses a tracker import a rule rejected with E1300 and names the rule by UID alone. The guide published that UID beside the rule's name, so the drain reads the refusal as the name an administrator knows the rule by, while the UID stays on the response's own report.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
class ProgramRuleNames(BaseModel):
    """The DHIS2 program rules a published guide names, keyed by UID, for reading a refusal back.

    DHIS2 refuses a tracker import a rule rejected with `E1300` and names the rule by UID alone. The
    guide published that UID beside the rule's name, so the drain reads the refusal as the name an
    administrator knows the rule by, while the UID stays on the response's own report.
    """

    model_config = ConfigDict(frozen=True)

    names_by_uid: dict[str, str] = Field(default_factory=dict)

    def name_for(self, uid: str) -> str | None:
        """The name one rule is published under, or None for a UID this guide publishes no rule for."""
        return self.names_by_uid.get(uid)
Methods:
name_for(uid)

The name one rule is published under, or None for a UID this guide publishes no rule for.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def name_for(self, uid: str) -> str | None:
    """The name one rule is published under, or None for a UID this guide publishes no rule for."""
    return self.names_by_uid.get(uid)

Functions:

load_compiled_artifacts(project)

Read one project's compiled IG plus its predefined resource tree into the artifacts the translator reads.

Compiled resources are read first and the predefined tree second, which is the order the facade loads them in, so a project reading its own guide two ways sees one collection.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def load_compiled_artifacts(project: FhirProject) -> CompiledArtifacts:
    """Read one project's compiled IG plus its predefined resource tree into the artifacts the translator reads.

    Compiled resources are read first and the predefined tree second, which is the order the facade
    loads them in, so a project reading its own guide two ways sees one collection.
    """
    compiled_directory = project.ig_directory / COMPILED_RESOURCES_RELATIVE_PATH
    compiled_paths = sorted(compiled_directory.glob("*.json")) if compiled_directory.is_dir() else []
    if not compiled_paths:
        raise CompiledIgMissingError(compiled_directory)
    predefined_directory = project.resources_directory
    predefined_paths = sorted(predefined_directory.rglob("*.json")) if predefined_directory.is_dir() else []
    return collect_artifacts(
        SourcedDocument(source=str(path), body=_read_resource(path)) for path in [*compiled_paths, *predefined_paths]
    )

collect_artifacts(documents)

Sort wire documents into the five resource types the QR -> DHIS2 translator reads.

One collection point for both ways a project's guide reaches the translator: read off disk from a compiled build, or built in memory from the instance a --live run points at. Anything outside TRANSLATED_RESOURCE_TYPES is passed over, because nothing in the response direction consults it.

How an unreadable document is handled depends on what it is. A Questionnaire the R4 models cannot read fails the collection naming its source: a form quietly skipped turns every response answering it into an unknown-form refusal, which reads as a problem with the data rather than with the guide. The other four cost only what they carry - a coded answer resolved unchecked, an organisation-unit reference read as a UID - so an unreadable one is left out and named on unreadable_resources, which the caller reports.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def collect_artifacts(documents: Iterable[SourcedDocument]) -> CompiledArtifacts:
    """Sort wire documents into the five resource types the QR -> DHIS2 translator reads.

    One collection point for both ways a project's guide reaches the translator: read off disk from
    a compiled build, or built in memory from the instance a `--live` run points at. Anything
    outside `TRANSLATED_RESOURCE_TYPES` is passed over, because nothing in the response direction
    consults it.

    How an unreadable document is handled depends on what it is. A **Questionnaire** the R4 models
    cannot read fails the collection naming its source: a form quietly skipped turns every response
    answering it into an `unknown-form` refusal, which reads as a problem with the data rather than
    with the guide. The other four cost only what they carry - a coded answer resolved unchecked, an
    organisation-unit reference read as a UID - so an unreadable one is left out and named on
    `unreadable_resources`, which the caller reports.
    """
    questionnaires: list[Questionnaire] = []
    code_systems: list[CodeSystem] = []
    value_sets: list[ValueSet] = []
    concept_maps: list[ConceptMap] = []
    locations: list[Location] = []
    unreadable: list[str] = []
    collections: dict[str, tuple[type[BaseModel], list[Any]]] = {
        _FORM_RESOURCE_TYPE: (Questionnaire, questionnaires),
        "CodeSystem": (CodeSystem, code_systems),
        "ValueSet": (ValueSet, value_sets),
        "ConceptMap": (ConceptMap, concept_maps),
        "Location": (Location, locations),
    }
    for document in documents:
        resource_type = str(document.body.get("resourceType"))
        kept = collections.get(resource_type)
        if kept is None:
            continue
        model, collected = kept
        try:
            collected.append(_parse(model, document.body, document.source))
        except CompiledArtifactReadError as error:
            if resource_type == _FORM_RESOURCE_TYPE:
                raise
            unreadable.append(str(error))
    return CompiledArtifacts(
        questionnaires=tuple(questionnaires),
        code_systems=tuple(code_systems),
        value_sets=tuple(value_sets),
        concept_maps=tuple(concept_maps),
        locations=tuple(locations),
        unreadable_resources=tuple(unreadable),
    )

bound_question_uids(artifacts, naming)

Every DHIS2 object the published forms ask a question from, sorted, read through the link-id grammar.

The forms are flattened with the very call the context builds them with, so the UIDs asked of the instance are exactly the ones a translated answer will be keyed by - a disaggregated cell's <dataElement>.<categoryOptionCombo> link id contributing its data element once. A tracker registration form's link ids are tracked entity attribute UIDs, so they are collected apart: they are the same question grammar keyed to a different DHIS2 object, and a different endpoint types them.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def bound_question_uids(artifacts: CompiledArtifacts, naming: ConversionNaming) -> BoundQuestionUids:
    """Every DHIS2 object the published forms ask a question from, sorted, read through the link-id grammar.

    The forms are flattened with the very call the context builds them with, so the UIDs asked of the
    instance are exactly the ones a translated answer will be keyed by - a disaggregated cell's
    `<dataElement>.<categoryOptionCombo>` link id contributing its data element once. A tracker
    registration form's link ids are tracked entity attribute UIDs, so they are collected apart:
    they are the same question grammar keyed to a different DHIS2 object, and a different endpoint
    types them.
    """
    data_elements: set[str] = set()
    attributes: set[str] = set()
    for questionnaire in artifacts.questionnaires:
        form = build_form_spec(questionnaire, naming, {}, {})
        collected = attributes if form.form_kind == _REGISTRATION_FORM_KIND else data_elements
        collected.update(question.data_element_uid for question in form.questions.values())
    return BoundQuestionUids(
        data_element_uids=tuple(sorted(data_elements)),
        tracked_entity_attribute_uids=tuple(sorted(attributes)),
    )

program_rule_names(artifacts, naming)

Every program rule the published forms carry, read off their D2ProgramRule extensions.

Only the rules a form could not express are published this way, which is exactly the set a refusal can name: a rule the form did express refuses nothing the form admits.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def program_rule_names(artifacts: CompiledArtifacts, naming: ConversionNaming) -> ProgramRuleNames:
    """Every program rule the published forms carry, read off their `D2ProgramRule` extensions.

    Only the rules a form could not express are published this way, which is exactly the set a
    refusal can name: a rule the form did express refuses nothing the form admits.
    """
    names: dict[str, str] = {}
    for questionnaire in artifacts.questionnaires:
        for extension in questionnaire.extension or []:
            if extension.url != naming.program_rule_url:
                continue
            carried = {sub.url: sub for sub in extension.extension or []}
            uid = carried[PROGRAM_RULE_UID_SUB_EXTENSION].valueId if PROGRAM_RULE_UID_SUB_EXTENSION in carried else None
            name = (
                carried[PROGRAM_RULE_NAME_SUB_EXTENSION].valueString
                if PROGRAM_RULE_NAME_SUB_EXTENSION in carried
                else None
            )
            if uid and name:
                names.setdefault(uid, name)
    return ProgramRuleNames(names_by_uid=names)

build_project_context(project, artifacts, *, value_types_by_data_element=None, coded_answer_mode=CodedAnswerMode.LENIENT)

Assemble the translation context one project's captured responses are read through.

Every name comes from fhir.toml - the IG canonical the extensions are pinned to and the [generate] naming tokens and identifier base - and the project timezone is what a zoned R4 timestamp is read back through into the wall clock DHIS2 stores (BUGS.md #62).

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
def build_project_context(
    project: FhirProject,
    artifacts: CompiledArtifacts,
    *,
    value_types_by_data_element: dict[str, str] | None = None,
    coded_answer_mode: CodedAnswerMode = CodedAnswerMode.LENIENT,
) -> ConversionContext:
    """Assemble the translation context one project's captured responses are read through.

    Every name comes from `fhir.toml` - the IG canonical the extensions are pinned to and the
    `[generate]` naming tokens and identifier base - and the project timezone is what a zoned R4
    timestamp is read back through into the wall clock DHIS2 stores (BUGS.md #62).
    """
    generate = project.config.generate
    naming = ConversionNaming.from_config(generate, project.config.ig.canonical)
    return build_conversion_context(
        naming,
        artifacts.questionnaires,
        code_systems=artifacts.code_systems,
        value_sets=artifacts.value_sets,
        concept_maps=artifacts.concept_maps,
        locations=artifacts.locations,
        value_types_by_data_element=value_types_by_data_element,
        coded_answer_mode=coded_answer_mode,
        timezone=generate.timezone,
    )

values

Serialising one question's answers into the DHIS2 wire value, exhaustively per value type.

This is the inverse of what the examples emitter wrote forward, and it is written as one branch per WireValueKind so the two directions can be read side by side:

TRUE_ONLY          `"true"`, or no data value at all - DHIS2 never stores `"false"` here.
BOOLEAN            `"true"` / `"false"`.
INTEGER family     the integer verbatim; the form's own `minValue` / `maxValue` are DHIS2's to enforce.
NUMBER family      the lexical decimal the R4 primitive carries - a whole number stays whole.
MULTI_TEXT         every selected option's DHIS2 code, joined by the separator DHIS2 splits on.
option-coded       the resolved option's DHIS2 code, falling back to its UID.
DATE / TIME        the R4 primitive verbatim; both are already what DHIS2 stores.
DATETIME           the zone-less wall clock behind the zoned R4 instant (BUGS.md #62).
URL / text         the string verbatim, which is where `COORDINATE` lands too.
ORGANISATION_UNIT  the DHIS2 UID behind the answered Location reference.
attachment         refused - DHIS2's file-resource wire is a separate upload, not a data value.

Nothing here reaches for a default. A question the translator cannot serialise answers with a typed refusal naming the link id, so a caller never posts a payload with a value quietly missing from the middle of it.

Attributes

Classes

WireValue

Bases: BaseModel

One DHIS2 data value the translator produced, and what producing it had to interpret or refuse.

value is None on two very different outcomes, which refusals separates: a TRUE_ONLY question answered false writes no data value because that is how DHIS2 spells false, while a refused answer writes none because the response is not translatable at all.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
class WireValue(BaseModel):
    """One DHIS2 data value the translator produced, and what producing it had to interpret or refuse.

    `value` is None on two very different outcomes, which `refusals` separates: a `TRUE_ONLY`
    question answered `false` writes no data value *because that is how DHIS2 spells false*, while
    a refused answer writes none because the response is not translatable at all.
    """

    model_config = ConfigDict(frozen=True)

    value: str | None = None
    notes: tuple[ConversionNote, ...] = ()
    refusals: tuple[ConversionRefusal, ...] = ()

WallClockReading

Bases: BaseModel

One R4 timestamp read back to the zone-less wall clock DHIS2 stores, and what reading it took.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
class WallClockReading(BaseModel):
    """One R4 timestamp read back to the zone-less wall clock DHIS2 stores, and what reading it took."""

    model_config = ConfigDict(frozen=True)

    value: str
    derived: bool = False
    """Whether an offset was present and read back through the project's zone."""

    unzoned: bool = False
    """Whether no offset was present, so the timestamp was taken as already being the wall clock."""

    moment: datetime.datetime | None = None
    """The same wall clock as a zone-less `datetime`, which is what `TrackerEvent.occurredAt` takes."""
Attributes
derived = False class-attribute instance-attribute

Whether an offset was present and read back through the project's zone.

unzoned = False class-attribute instance-attribute

Whether no offset was present, so the timestamp was taken as already being the wall clock.

moment = None class-attribute instance-attribute

The same wall clock as a zone-less datetime, which is what TrackerEvent.occurredAt takes.

OrganisationUnitResolution

Bases: BaseModel

The DHIS2 organisation unit one Location/<id> reference names, and what resolving it took.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
class OrganisationUnitResolution(BaseModel):
    """The DHIS2 organisation unit one `Location/<id>` reference names, and what resolving it took."""

    model_config = ConfigDict(frozen=True)

    uid: str | None = None
    note: ConversionNote | None = None

Functions:

answer_wire_value(question, answers, context)

Serialise every answer to one question into the single data value DHIS2 stores for it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def answer_wire_value(
    question: QuestionSpec,
    answers: Sequence[QuestionnaireResponseAnswer],
    context: ConversionContext,
) -> WireValue:
    """Serialise every answer to one question into the single data value DHIS2 stores for it."""
    if not answers:
        return _refuse(
            question,
            ConversionRefusalCategory.MISSING_ANSWER_VALUE,
            f"`{question.link_id}` carries no answer",
        )
    mismatch = _element_refusal(question, answers)
    if mismatch is not None:
        return WireValue(refusals=(mismatch,))
    if question.wire_kind == WireValueKind.MULTI_TEXT:
        return _multi_text_value(question, answers, context)
    if len(answers) > 1:
        return _refuse(
            question,
            ConversionRefusalCategory.REPEATED_ANSWER,
            f"`{question.link_id}` stores one DHIS2 data value, and {len(answers)} answers were sent",
        )
    return _single_value(question, answers[0], context)

resolve_option(table, code, mode)

Resolve one received code to the option it names, as strictly as the run's dial asks.

Strict accepts the concept code the served CodeSystem publishes and nothing else. Lenient then tries the DHIS2 option UID and the DHIS2 option code, because the generated CodeSystem carries every option under both spellings and a client that sent the other one still named exactly one option. Two matches inside one tier is an ambiguity leniency cannot paper over.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def resolve_option(table: OptionTable, code: str, mode: CodedAnswerMode) -> OptionLookup:
    """Resolve one received code to the option it names, as strictly as the run's dial asks.

    Strict accepts the concept code the served CodeSystem publishes and nothing else. Lenient
    then tries the DHIS2 option UID and the DHIS2 option code, because the generated CodeSystem
    carries every option under both spellings and a client that sent the other one still named
    exactly one option. Two matches inside one tier is an ambiguity leniency cannot paper over.
    """
    matched = _tier(table, code, CONCEPT_CODE_TIER)
    if matched.option is not None or matched.ambiguous_option_uids:
        return matched
    if mode == CodedAnswerMode.STRICT:
        return OptionLookup()
    by_uid = _tier(table, code, OPTION_UID_TIER)
    if by_uid.option is not None or by_uid.ambiguous_option_uids:
        return by_uid
    return _tier(table, code, OPTION_CODE_TIER)

wall_clock_reading(value, timezone)

Read one R4 timestamp back to the zone-less wall clock DHIS2 stores - the inverse of zoned_date_time.

DHIS2 serves and accepts occurredAt and DATETIME data values as zone-less local timestamps under fields its OpenAPI types as Instant (BUGS.md #62), so the offset an R4 dateTime requires is exactly what has to come back off. timezone is the IANA zone those wall-clock readings are taken in, which the project states as [generate] timezone; naming none reads the clock in UTC, which is the same guess the forward direction makes.

A date-only value carries no clock and passes through, and so does a timestamp already written without an offset - the caller is told which through unzoned rather than left to assume.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def wall_clock_reading(value: str, timezone: str | None) -> WallClockReading:
    """Read one R4 timestamp back to the zone-less wall clock DHIS2 stores - the inverse of `zoned_date_time`.

    DHIS2 serves and accepts `occurredAt` and `DATETIME` data values as zone-less local timestamps
    under fields its OpenAPI types as `Instant` (BUGS.md #62), so the offset an R4 `dateTime`
    requires is exactly what has to come back off. `timezone` is the IANA zone those wall-clock
    readings are taken in, which the project states as `[generate] timezone`; naming none reads
    the clock in UTC, which is the same guess the forward direction makes.

    A date-only value carries no clock and passes through, and so does a timestamp already written
    without an offset - the caller is told which through `unzoned` rather than left to assume.
    """
    if "T" not in value:
        return WallClockReading(value=value, moment=_moment(value))
    try:
        parsed = datetime.datetime.fromisoformat(value)
    except ValueError:
        return WallClockReading(value=value, unzoned=True)
    if parsed.tzinfo is None:
        return WallClockReading(value=value, unzoned=True, moment=parsed)
    local = parsed.astimezone(_zone(timezone)).replace(tzinfo=None)
    return WallClockReading(value=local.isoformat(), derived=True, moment=local)

decimal_wire_value(value)

Write one R4 decimal answer as the DHIS2 wire value it is, without a float round trip.

A whole number stays whole - the R4 model types valueDecimal as int | float for exactly that reason, so 2896 never becomes 2896.0. A fractional number is written as the shortest decimal that reads back as the same number, and never in exponent notation, which DHIS2's numeric value types do not accept.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def decimal_wire_value(value: int | float) -> str:
    """Write one R4 decimal answer as the DHIS2 wire value it is, without a float round trip.

    A whole number stays whole - the R4 model types `valueDecimal` as `int | float` for exactly
    that reason, so `2896` never becomes `2896.0`. A fractional number is written as the shortest
    decimal that reads back as the same number, and never in exponent notation, which DHIS2's
    numeric value types do not accept.
    """
    if isinstance(value, int):
        return str(value)
    text = repr(value)
    if "e" in text or "E" in text:
        return format(Decimal(text), "f")
    return text

resolve_organisation_unit(reference, context)

Resolve a Location/<id> reference to the DHIS2 organisation unit UID the published Location identifies.

A Location id is an identity stem, and under naming.source = "code" that stem is the unit's DHIS2 code rather than its UID - so the resolution goes through the org-unit identifier the registry wrote and never assumes the id is the UID. A context given no Location table has nothing to go through and falls back to reading the id as a UID, which it says out loud.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def resolve_organisation_unit(reference: str, context: ConversionContext) -> OrganisationUnitResolution:
    """Resolve a `Location/<id>` reference to the DHIS2 organisation unit UID the published Location identifies.

    A Location id is an identity stem, and under `naming.source = "code"` that stem is the unit's
    DHIS2 code rather than its UID - so the resolution goes through the org-unit identifier the
    registry wrote and never assumes the id is the UID. A context given no Location table has
    nothing to go through and falls back to reading the id as a UID, which it says out loud.
    """
    location_id = reference.removeprefix(LOCATION_REFERENCE_PREFIX)
    if not location_id:
        return OrganisationUnitResolution()
    if not context.resolves_organisation_units:
        return OrganisationUnitResolution(
            uid=location_id,
            note=ConversionNote(
                category=ConversionNoteCategory.ORGANISATION_UNIT_ASSUMED,
                message=f"the context carries no published Location, so `{reference}` is read as the DHIS2 "
                f"organisation unit UID `{location_id}`",
            ),
        )
    return OrganisationUnitResolution(uid=context.organisation_unit_uids_by_location_id.get(location_id))

wall_clock_notes(reading, context, *, link_id)

The note one wall-clock reading is worth, so a stripped offset is never silent.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
def wall_clock_notes(
    reading: WallClockReading, context: ConversionContext, *, link_id: str | None
) -> tuple[ConversionNote, ...]:
    """The note one wall-clock reading is worth, so a stripped offset is never silent."""
    if reading.derived:
        zone = context.timezone or "UTC"
        return (
            ConversionNote(
                category=ConversionNoteCategory.WALL_CLOCK_DERIVED,
                link_id=link_id,
                message=f"the zoned timestamp was read in `{zone}` and written as the zone-less "
                f"wall clock `{reading.value}` DHIS2 stores",
            ),
        )
    if reading.unzoned:
        return (
            ConversionNote(
                category=ConversionNoteCategory.TIMESTAMP_UNZONED,
                link_id=link_id,
                message=f"`{reading.value}` carries no UTC offset, so it is taken as already being the "
                f"wall clock DHIS2 stores",
            ),
        )
    return ()

payloads

The five payload translators: one per DHIS2 form kind, each writing the import shape DHIS2 reads.

aggregate      -> a `/api/dataValueSets` envelope: data set, ISO period, organisation unit, the
                  attribute option combo the whole report is filed under where the form declares
                  a vocabulary for one, and one data value per answered cell, each carrying its
                  category option combo.
event          -> one `/api/tracker` event of an event program: the UID derived from the
                  receipt's own logical id, program, organisation unit, occurrence, status,
                  and one data value per answered question.
tracker        -> one `/api/tracker` tracked entity: its client-minted UID, the tracked entity
                  type the form names, the organisation unit that owns it, one attribute per
                  answered entity-level question, and the single enrollment the response
                  creates - minted UID, program, organisation unit, enrolment date, incident
                  date where one was stated, `ACTIVE` status, and one attribute per answered
                  program-only question. A response stating `D2SubjectExists` names a person
                  the instance already holds, and produces that enrollment alone - naming the
                  existing tracked entity, with no tracked entity beside it to rewrite.
tracker-event  -> the same event as `event`, plus the program stage it belongs to, the tracked
                  entity it was captured for, and the enrollment it sits on.

Every DHIS2 object a tracker payload creates is named before it is posted. A registration reads its tracked entity and enrollment UIDs off the response, where the client that filled the form minted them; both event kinds derive theirs from the receipt's own logical id, so one receipt always names one event. A dry run and the import behind it therefore report the same UID, and forwarding a receipt twice is refused by the instance as an object it already holds rather than filing a second copy of one visit.

Every fact a payload carries is read out of the response through an identifier or an extension, never out of a URL. A Questionnaire's canonical segment is an identity stem, and a Location's id is one too, so a data set UID comes off the form's .../id/data-set identifier and an organisation unit UID off the registry's .../id/org-unit slice - which under code-or-id naming are not what the ids spell.

A response the translator cannot read whole produces refusals and no payload at all. There is no partial envelope: a data value set missing the third of its forty cells would import as a complete report of a period, which is worse than not importing.

Classes

TranslatedAnswer

Bases: BaseModel

One answered question and the DHIS2 wire value it produced.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
class TranslatedAnswer(BaseModel):
    """One answered question and the DHIS2 wire value it produced."""

    model_config = ConfigDict(frozen=True)

    question: QuestionSpec
    value: str

TranslatedAnswers

Bases: BaseModel

Every data value one response's item tree produced, in the order the response carries them.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
class TranslatedAnswers(BaseModel):
    """Every data value one response's item tree produced, in the order the response carries them."""

    model_config = ConfigDict(frozen=True)

    answers: tuple[TranslatedAnswer, ...] = ()
    notes: tuple[ConversionNote, ...] = ()
    refusals: tuple[ConversionRefusal, ...] = ()

AggregateTranslation

Bases: _Outcome

One aggregate response translated: its data value set, or the reasons it produced none.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
class AggregateTranslation(_Outcome):
    """One aggregate response translated: its data value set, or the reasons it produced none."""

    data_value_set: DataValueSet | None = None
    completeness: CompleteDataSetRegistration | None = None
    """What the response claims about completeness, set only where it reports itself `completed`."""
Attributes
completeness = None class-attribute instance-attribute

What the response claims about completeness, set only where it reports itself completed.

EventTranslation

Bases: _Outcome

One event or tracker-event response translated: its event, or the reasons it produced none.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
class EventTranslation(_Outcome):
    """One event or tracker-event response translated: its event, or the reasons it produced none."""

    event: TrackerEvent | None = None
    target_kind: ConversionTargetKind = ConversionTargetKind.EVENT

RegistrationTranslation

Bases: _Outcome

One registration response translated: what it creates in DHIS2, or the reasons it produced none.

tracked_entity and enrollment are alternatives, and target_kind says which one the response produced: a registration creating the person it enrols carries the tracked entity with the enrollment nested inside it, and one whose subject the instance already holds carries the enrollment alone.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
class RegistrationTranslation(_Outcome):
    """One registration response translated: what it creates in DHIS2, or the reasons it produced none.

    `tracked_entity` and `enrollment` are alternatives, and `target_kind` says which one the
    response produced: a registration creating the person it enrols carries the tracked entity with
    the enrollment nested inside it, and one whose subject the instance already holds carries the
    enrollment alone.
    """

    tracked_entity: TrackerTrackedEntity | None = None
    enrollment: TrackerEnrollment | None = None
    target_kind: ConversionTargetKind = ConversionTargetKind.TRACKER

Functions:

translate_aggregate_response(response, form, context)

Translate one aggregate response into the /api/dataValueSets envelope DHIS2 imports it as.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_aggregate_response(
    response: QuestionnaireResponse, form: FormSpec, context: ConversionContext
) -> AggregateTranslation:
    """Translate one aggregate response into the `/api/dataValueSets` envelope DHIS2 imports it as."""
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    data_set = form.data_set_uid
    if data_set is None:
        refusals.append(
            ConversionRefusal(
                category=ConversionRefusalCategory.MISSING_TARGET_IDENTIFIER,
                element="Questionnaire.identifier",
                reason=f"`{form.canonical}` carries no `{context.naming.data_set_system}` identifier, so the "
                f"data set its responses report for is unknown",
            )
        )
    period = _period(response, context, notes, refusals)
    organisation_unit = _subject_organisation_unit(response, context, notes, refusals)
    attribute_option_combo = _attribute_option_combo(response, form, context, notes, refusals)
    translated = translate_answers(response, form, context)
    notes.extend(translated.notes)
    refusals.extend(translated.refusals)
    if refusals or data_set is None or period is None or organisation_unit is None:
        return AggregateTranslation(notes=tuple(notes), refusals=tuple(refusals))
    completeness = _completeness(
        response,
        context,
        notes,
        data_set=data_set,
        period=period,
        organisation_unit=organisation_unit,
        attribute_option_combo=attribute_option_combo,
    )
    return AggregateTranslation(
        notes=tuple(notes),
        completeness=completeness,
        data_value_set=DataValueSet(
            dataSet=data_set,
            period=period,
            orgUnit=organisation_unit,
            attributeOptionCombo=attribute_option_combo,
            dataValues=[
                DataValue(
                    dataElement=answer.question.data_element_uid,
                    categoryOptionCombo=answer.question.category_option_combo_uid,
                    value=answer.value,
                )
                for answer in translated.answers
            ],
        ),
    )

receipt_event_uid(response_id)

The DHIS2 UID one receipt's event is imported under, derived from the receipt's own logical id.

The same receipt names the same event UID - on the dry run and on the import that follows it, on this machine and on the next. That property is the whole point: an event travels to /api/tracker under importStrategy=CREATE, so forwarding a receipt DHIS2 already holds the event of is refused as an object that exists, rather than filing a second copy of one visit. It is the identity a registration already has - the tracked entity UID its subject identifier carries - given to the one payload kind that carried none, and it is what lets a dry run's diagnostics be read against the objects the import then creates, because both name this UID.

The material is <response id>:event:0, hashed with SHA-256 - never Python's per-process salted hash - and shaped into [A-Za-z][A-Za-z0-9]{10} by the same drawer the synthesis path mints tracked entity and enrollment UIDs with. The trailing ordinal is the discriminator a receipt naming more than one event would move; a receipt reports exactly one.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def receipt_event_uid(response_id: str) -> str:
    """The DHIS2 UID one receipt's event is imported under, derived from the receipt's own logical id.

    The same receipt names the same event UID - on the dry run and on the import that follows it,
    on this machine and on the next. That property is the whole point: an event travels to
    `/api/tracker` under `importStrategy=CREATE`, so forwarding a receipt DHIS2 already holds the
    event of is refused as an object that exists, rather than filing a second copy of one visit.
    It is the identity a registration already has - the tracked entity UID its subject identifier
    carries - given to the one payload kind that carried none, and it is what lets a dry run's
    diagnostics be read against the objects the import then creates, because both name this UID.

    The material is `<response id>:event:0`, hashed with SHA-256 - never Python's per-process
    salted `hash` - and shaped into `[A-Za-z][A-Za-z0-9]{10}` by the same drawer the synthesis
    path mints tracked entity and enrollment UIDs with. The trailing ordinal is the discriminator
    a receipt naming more than one event would move; a receipt reports exactly one.
    """
    material = f"{response_id}:{_EVENT_IDENTITY_TOKEN}"
    generator = random.Random(derived_seed(material, _SOLE_EVENT_ORDINAL))  # noqa: S311 - an identity, not a secret
    return synthetic_uid(generator)

translate_event_response(response, form, context)

Translate one event-program response into the single /api/tracker event DHIS2 imports it as.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_event_response(
    response: QuestionnaireResponse, form: FormSpec, context: ConversionContext
) -> EventTranslation:
    """Translate one event-program response into the single `/api/tracker` event DHIS2 imports it as."""
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    program = _program(form, context, refusals)
    organisation_unit = _subject_organisation_unit(response, context, notes, refusals)
    occurred_at = _occurred_at(response, context, notes, refusals)
    status = _event_status(response, notes, refusals)
    translated = translate_answers(response, form, context)
    notes.extend(translated.notes)
    refusals.extend(translated.refusals)
    if refusals or program is None or organisation_unit is None or occurred_at is None or status is None:
        return EventTranslation(notes=tuple(notes), refusals=tuple(refusals))
    return EventTranslation(
        notes=tuple(notes),
        event=TrackerEvent(
            event=_event_identity(response),
            program=program,
            programStage=form.program_stage_uid,
            orgUnit=organisation_unit,
            occurredAt=occurred_at,
            status=status,
            dataValues=_tracker_data_values(translated),
        ),
    )

translate_tracker_event_response(response, form, context)

Translate one tracker program stage response into the enrolled /api/tracker event it reports.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_tracker_event_response(
    response: QuestionnaireResponse, form: FormSpec, context: ConversionContext
) -> EventTranslation:
    """Translate one tracker program stage response into the enrolled `/api/tracker` event it reports."""
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    program = _program(form, context, refusals)
    stage = _program_stage(form, context, refusals)
    organisation_unit = _extension_organisation_unit(response, context, notes, refusals)
    occurred_at = _occurred_at(response, context, notes, refusals)
    status = _event_status(response, notes, refusals)
    tracked_entity = _tracked_entity(response, context, notes, refusals)
    enrollment = _enrollment(response, context, refusals)
    translated = translate_answers(response, form, context)
    notes.extend(translated.notes)
    refusals.extend(translated.refusals)
    if refusals or program is None or stage is None or organisation_unit is None or occurred_at is None:
        return EventTranslation(
            notes=tuple(notes), refusals=tuple(refusals), target_kind=ConversionTargetKind.TRACKER_EVENT
        )
    if status is None or tracked_entity is None or enrollment is None:
        return EventTranslation(
            notes=tuple(notes), refusals=tuple(refusals), target_kind=ConversionTargetKind.TRACKER_EVENT
        )
    return EventTranslation(
        notes=tuple(notes),
        target_kind=ConversionTargetKind.TRACKER_EVENT,
        event=TrackerEvent(
            event=_event_identity(response),
            program=program,
            programStage=stage,
            orgUnit=organisation_unit,
            trackedEntity=tracked_entity,
            enrollment=enrollment,
            occurredAt=occurred_at,
            status=status,
            dataValues=_tracker_data_values(translated),
        ),
    )

translate_tracker_registration_response(response, form, context)

Translate one registration response into the /api/tracker tracked entity and enrollment it creates.

Both DHIS2 identities are the client's: the tracked entity UID the subject identifier carries and the enrollment UID the D2TrackerEnrollment extension carries are minted by whoever filled the form, and they travel to DHIS2 as sent. That is what lets the stage events of the same enrollment be captured before either identity exists, and it is why a registration is posted before them.

The attributes are the form's own answers, serialised through the value-type machinery a data element's answers go through - a tracked entity attribute has the same DHIS2 value types, binds option sets the same way, and its coded answers resolve against the published ValueSets on the same strict/lenient dial.

DHIS2 imports those answers at two levels, and the form says which is which: a question whose D2EntityLevel extension is true asks a tracked entity attribute of the program's tracked entity type, and its value is stated on the tracked entity; a question stating false asks an attribute only the program collects, and its value is stated on the enrollment. A question stating no level at all - a guide compiled before the extension was published - is written on the tracked entity, which is where every registration answer went before the split.

A response carrying D2SubjectExists as true states that its subject identifier names a person the instance already holds, and what it creates is then the enrollment alone - a top-level enrollments array naming that existing tracked entity, posted under the same plain importStrategy=CREATE every other payload goes under. No trackedEntities wrapper goes round it: an enrollment that rides inside one has to be posted CREATE_AND_UPDATE, which silently rewrites the person's owning organisation unit (BUGS.md 73), and the person is not this response's to move. The program's own attributes ride the enrollment, because DHIS2 answers E1018 to a mandatory program attribute that arrives on nothing.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_tracker_registration_response(
    response: QuestionnaireResponse, form: FormSpec, context: ConversionContext
) -> RegistrationTranslation:
    """Translate one registration response into the `/api/tracker` tracked entity and enrollment it creates.

    Both DHIS2 identities are the client's: the tracked entity UID the subject identifier carries
    and the enrollment UID the `D2TrackerEnrollment` extension carries are minted by whoever filled
    the form, and they travel to DHIS2 as sent. That is what lets the stage events of the same
    enrollment be captured before either identity exists, and it is why a registration is posted
    before them.

    The attributes are the form's own answers, serialised through the value-type machinery a data
    element's answers go through - a tracked entity attribute has the same DHIS2 value types, binds
    option sets the same way, and its coded answers resolve against the published ValueSets on the
    same strict/lenient dial.

    DHIS2 imports those answers at two levels, and the form says which is which: a question whose
    `D2EntityLevel` extension is true asks a tracked entity attribute of the program's tracked
    entity type, and its value is stated on the tracked entity; a question stating false asks an
    attribute only the program collects, and its value is stated on the enrollment. A question
    stating no level at all - a guide compiled before the extension was published - is written on
    the tracked entity, which is where every registration answer went before the split.

    A response carrying `D2SubjectExists` as true states that its subject identifier names a person
    the instance already holds, and what it creates is then the enrollment alone - a top-level
    `enrollments` array naming that existing tracked entity, posted under the same plain
    `importStrategy=CREATE` every other payload goes under. No `trackedEntities` wrapper goes round
    it: an enrollment that rides inside one has to be posted `CREATE_AND_UPDATE`, which silently
    rewrites the person's owning organisation unit (BUGS.md 73), and the person is not this
    response's to move. The program's own attributes ride the enrollment, because DHIS2 answers
    `E1018` to a mandatory program attribute that arrives on nothing.
    """
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    program = _program(form, context, refusals)
    tracked_entity_type = _tracked_entity_type(form, context, refusals)
    organisation_unit = _extension_organisation_unit(response, context, notes, refusals)
    tracked_entity = _tracked_entity(response, context, notes, refusals)
    enrollment = _enrollment(response, context, refusals)
    enrolled_at = _enrollment_date(response, context.naming.enrolled_at_url, context, notes, refusals, required=True)
    incident_at = _enrollment_date(response, context.naming.incident_at_url, context, notes, refusals, required=False)
    subject_exists = _subject_exists(response, context)
    translated = translate_answers(response, form, context)
    notes.extend(translated.notes)
    refusals.extend(translated.refusals)
    if subject_exists:
        refusals.extend(_existing_subject_refusals(translated, form))
    target_kind = ConversionTargetKind.TRACKER_ENROLLMENT if subject_exists else ConversionTargetKind.TRACKER
    if refusals or program is None or tracked_entity_type is None or organisation_unit is None:
        return RegistrationTranslation(notes=tuple(notes), refusals=tuple(refusals), target_kind=target_kind)
    if tracked_entity is None or enrollment is None or enrolled_at is None:
        return RegistrationTranslation(notes=tuple(notes), refusals=tuple(refusals), target_kind=target_kind)
    created = TrackerEnrollment(
        enrollment=enrollment,
        trackedEntity=tracked_entity if subject_exists else None,
        program=program,
        orgUnit=organisation_unit,
        enrolledAt=enrolled_at,
        occurredAt=incident_at,
        status=REGISTERED_ENROLLMENT_STATUS,
        attributes=_enrollment_attributes(translated, subject_exists=subject_exists) or None,
    )
    if subject_exists:
        return RegistrationTranslation(notes=tuple(notes), enrollment=created, target_kind=target_kind)
    return RegistrationTranslation(
        notes=tuple(notes),
        target_kind=target_kind,
        tracked_entity=TrackerTrackedEntity(
            trackedEntity=tracked_entity,
            trackedEntityType=tracked_entity_type,
            orgUnit=organisation_unit,
            attributes=_tracked_entity_attributes(translated),
            enrollments=[created],
        ),
    )

translate_tracked_entity_response(response, form, context)

Translate one person-only response into the /api/tracker tracked entity it creates.

The registration translator without its enrollment half. DHIS2 accepts a bare trackedEntities import under plain CREATE, so the payload is one tracked entity carrying the type off $DHIS2-TET, the organisation unit it is owned at, and its answers - every one of them on the entity, because the form asks only attributes the type itself collects and there is no enrollment for an answer to land on.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_tracked_entity_response(
    response: QuestionnaireResponse, form: FormSpec, context: ConversionContext
) -> RegistrationTranslation:
    """Translate one person-only response into the `/api/tracker` tracked entity it creates.

    The registration translator without its enrollment half. DHIS2 accepts a bare `trackedEntities`
    import under plain CREATE, so the payload is one tracked entity carrying the type off
    `$DHIS2-TET`, the organisation unit it is owned at, and its answers - every one of them on the
    entity, because the form asks only attributes the type itself collects and there is no
    enrollment for an answer to land on.
    """
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    tracked_entity_type = _tracked_entity_type(form, context, refusals)
    organisation_unit = _extension_organisation_unit(response, context, notes, refusals)
    tracked_entity = _tracked_entity(response, context, notes, refusals)
    translated = translate_answers(response, form, context)
    notes.extend(translated.notes)
    refusals.extend(translated.refusals)
    if refusals or tracked_entity_type is None or organisation_unit is None or tracked_entity is None:
        return RegistrationTranslation(
            notes=tuple(notes), refusals=tuple(refusals), target_kind=ConversionTargetKind.TRACKED_ENTITY
        )
    return RegistrationTranslation(
        notes=tuple(notes),
        target_kind=ConversionTargetKind.TRACKED_ENTITY,
        tracked_entity=TrackerTrackedEntity(
            trackedEntity=tracked_entity,
            trackedEntityType=tracked_entity_type,
            orgUnit=organisation_unit,
            attributes=[
                TrackerAttribute(attribute=answer.question.data_element_uid, value=answer.value)
                for answer in translated.answers
            ],
        ),
    )

translate_answers(response, form, context)

Walk one response's item tree and serialise every answered question, in document order.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
def translate_answers(response: QuestionnaireResponse, form: FormSpec, context: ConversionContext) -> TranslatedAnswers:
    """Walk one response's item tree and serialise every answered question, in document order."""
    answers: list[TranslatedAnswer] = []
    notes: list[ConversionNote] = []
    refusals: list[ConversionRefusal] = []
    _walk(response.item or [], form, context, answers, notes, refusals)
    return TranslatedAnswers(answers=tuple(answers), notes=tuple(notes), refusals=tuple(refusals))

translator

The entry point: one QuestionnaireResponse in, one typed DHIS2 payload or a named refusal out.

Translation is three questions asked in order, and the first that cannot be answered ends the response: which DHIS2 form kind the submission claims to be, which served form it answers, and whether the two agree. Only then does the form kind's payload translator run.

The batch form drains a spool the same way one response at a time, so a ConversionReport is one result per submission in the order they were drained - a refusal never stops the responses behind it, and a caller posts report.tracked_entities, report.enrollments, report.data_value_sets, and report.events while routing report.refused back to whoever sent them. People first, then the payloads that create an enrollment: a registration naming an existing person answers against one a person-only response of the same drain creates, and a stage event answers against an enrollment the registration is what creates.

Classes

Functions:

translate_response(response, context)

Translate one captured response into the DHIS2 import payload its form kind reports as.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/translator.py
def translate_response(response: QuestionnaireResponse, context: ConversionContext) -> ConversionResult:
    """Translate one captured response into the DHIS2 import payload its form kind reports as."""
    declared = _declared_form_kind(response, context)
    if declared is None:
        return _refused(
            response,
            ConversionRefusalCategory.NO_FORM_TYPE,
            "QuestionnaireResponse.extension",
            f"the response declares no DHIS2 form kind under `{context.naming.form_type_url}`",
        )
    form = context.form_for(response.questionnaire)
    if form is None:
        return _refused(
            response,
            ConversionRefusalCategory.UNKNOWN_FORM,
            "QuestionnaireResponse.questionnaire",
            f"`{response.questionnaire}` is no form this context carries",
        )
    if form.form_kind != declared:
        return _refused(
            response,
            ConversionRefusalCategory.FORM_KIND_MISMATCH,
            "QuestionnaireResponse.extension",
            f"the response declares form kind `{declared}`, and `{form.canonical}` is a `{form.form_kind}` form",
        )
    if form.form_kind == "aggregate":
        return _aggregate_result(response, form, context)
    if form.form_kind in _REGISTRATION_FORM_KINDS:
        return _registration_result(response, form, context)
    return _event_result(response, form, context)

translate_responses(responses, context)

Translate a batch of captured responses, one result each, in the order they were drained.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/translator.py
def translate_responses(responses: Sequence[QuestionnaireResponse], context: ConversionContext) -> ConversionReport:
    """Translate a batch of captured responses, one result each, in the order they were drained."""
    return ConversionReport(results=tuple(translate_response(response, context) for response in responses))

The conformance runner

What d2w fhir doctor concluded about one instance, as models rather than as terminal output. A DoctorReport carries one DoctorPhaseResult per phase - the outcome, the one line it is read by, the reason a phase that did not run gives, and every DoctorFinding it raised with the field path a mismatch was found at. The graders are the pure half of the runner: grade_forward, grade_capture, grade_oracle, and grade_drift turn a phase's own report into the verdict it is recorded under, over ForwardReport, CaptureOutcome, FamilyOutcome, and DriftReport, and render_doctor_markdown turns the whole run into the report file a handover is read from.

run_doctor is the other half, and what it does to the machine is part of its contract: it mints a workspace directory (or writes into the one DoctorOptions.workspace names) and removes a minted one unless keep is set, runs sushi or docker run where a compiler is on the machine, writes compiled resources under ig/fsh-generated/resources, runs an ASGI application in process, and posts a synthetic corpus at the instance under validate-only mode. A caller that cannot afford that wants the graders rather than the run.

doctor

The instance conformance runner behind d2w fhir doctor: run the whole toolchain, report what breaks.

Doctor scaffolds a throwaway project against the ambient DHIS2 profile, generates the IG source from that instance, compiles it when a compiler is on the machine, serves it in process, captures a synthetic corpus through the served endpoint, drains that corpus at the instance under validate-only mode, lets the instance judge the served resources object by object, and closes by asking whether the guide already on disk still describes the instance it was generated from. Every phase reports one typed result, and a failure never stops a phase that does not depend on it.

The direction of judgement is the whole point of the oracle phase: the DHIS2 instance is the authority and the served output is what is on trial, never the reverse. A mismatch is a fact about this toolchain against this instance, stated with the field path it was found on.

No MCP tool, the way d2w profile has none. A run writes a project tree, shells out to a compiler, posts a corpus through an in-process server, and reads a whole instance - a write-heavy orchestration with no read-only shape an MCP tool could honestly advertise.

run_doctor is a library call all the same, and what it does to the machine is part of its contract rather than a surprise: it mints a workspace directory (or writes into the one DoctorOptions.workspace names) and removes a minted one unless keep is set, runs sushi or docker run when a compiler is on the machine, writes compiled resources under ig/fsh-generated/resources, runs an ASGI application in process, and posts a synthetic corpus at the instance under validate-only mode. A caller that cannot afford any of that wants the phases it grades rather than the run: grade, grade_capture, grade_forward, grade_oracle, and grade_drift are pure over DoctorFinding, CaptureOutcome, FamilyOutcome, ForwardReport, and DriftReport.

The drift phase is the one phase whose subject is not the throwaway project. It reads the published guide the working directory sits in, so resolve_published_project runs before anything is written and returns nothing for a working directory inside the workspace: a project this run generated seconds ago can only agree with the instance, and a phase that always passes says nothing.

Classes

DoctorPhase

Bases: StrEnum

The phases of one doctor run, in the order they execute.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorPhase(StrEnum):
    """The phases of one doctor run, in the order they execute."""

    CONNECT = "connect"
    SCAFFOLD = "scaffold"
    GENERATE = "generate"
    COMPILE = "compile"
    VALIDATE = "validate"
    SERVE = "serve"
    CAPTURE = "capture"
    FORWARD = "forward"
    ORACLE = "oracle"
    DRIFT = "drift"

DoctorOutcome

Bases: StrEnum

What one phase concluded, which is what the verdict and the exit code are read off.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorOutcome(StrEnum):
    """What one phase concluded, which is what the verdict and the exit code are read off."""

    #: The phase ran and found nothing worth acting on.
    PASSED = "pass"

    #: The phase ran and found something that degrades the result without breaking it.
    WARNED = "warn"

    #: The phase ran and found something broken. A run holding one of these exits 1.
    FAILED = "fail"

    #: The phase did not run because this machine or this invocation does not offer what it needs.
    SKIPPED = "skipped"

    #: The phase did not run because an earlier phase it is built on did not produce its input.
    BLOCKED = "blocked"

DoctorFinding

Bases: BaseModel

One thing a phase found: what object, what is wrong with it, and where on it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorFinding(BaseModel):
    """One thing a phase found: what object, what is wrong with it, and where on it."""

    model_config = ConfigDict(frozen=True)

    phase: DoctorPhase
    severity: Literal["error", "warning"]
    subject: str
    detail: str
    field_path: str | None = None
    """The path on the served resource a mismatch was found at, for the findings that have one."""
Attributes
field_path = None class-attribute instance-attribute

The path on the served resource a mismatch was found at, for the findings that have one.

DoctorPhaseResult

Bases: BaseModel

The outcome of one phase: the verdict, the one line it is read by, and what it found.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorPhaseResult(BaseModel):
    """The outcome of one phase: the verdict, the one line it is read by, and what it found."""

    model_config = ConfigDict(frozen=True)

    phase: DoctorPhase
    outcome: DoctorOutcome
    evidence: str
    reason: str | None = None
    """Why a phase did not run, stated on every SKIPPED and BLOCKED result."""

    findings: tuple[DoctorFinding, ...] = ()
    elapsed_seconds: float = 0.0

    @property
    def error_count(self) -> int:
        """How many of this phase's findings are broken rather than merely degraded."""
        return sum(1 for finding in self.findings if finding.severity == "error")
Attributes
reason = None class-attribute instance-attribute

Why a phase did not run, stated on every SKIPPED and BLOCKED result.

error_count property

How many of this phase's findings are broken rather than merely degraded.

DoctorOptions

Bases: BaseModel

The dials one doctor run is invoked with.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorOptions(BaseModel):
    """The dials one doctor run is invoked with."""

    model_config = ConfigDict(frozen=True)

    workspace: Path | None = None
    """Where the throwaway project is written. None mints a temporary directory."""

    keep: bool = False
    """Keep a minted temporary workspace instead of removing it when the run ends."""

    all_targets: bool = False
    """Scaffold empty selection tables, which selects every data set and every program."""

    live: bool = False
    """Run the oracle phase, where the instance judges the served resources object by object."""

    samples: int = DEFAULT_ORACLE_SAMPLES
    """How many resources per family the oracle deep-compares against the instance."""
Attributes
workspace = None class-attribute instance-attribute

Where the throwaway project is written. None mints a temporary directory.

keep = False class-attribute instance-attribute

Keep a minted temporary workspace instead of removing it when the run ends.

all_targets = False class-attribute instance-attribute

Scaffold empty selection tables, which selects every data set and every program.

live = False class-attribute instance-attribute

Run the oracle phase, where the instance judges the served resources object by object.

samples = DEFAULT_ORACLE_SAMPLES class-attribute instance-attribute

How many resources per family the oracle deep-compares against the instance.

DoctorReport

Bases: BaseModel

Everything one doctor run concluded: what it ran against, where it ran, and what each phase found.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class DoctorReport(BaseModel):
    """Everything one doctor run concluded: what it ran against, where it ran, and what each phase found."""

    model_config = ConfigDict(frozen=True)

    profile_name: str
    profile_origin: str
    base_url: str
    dhis2_version: str | None = None
    version_tree: str | None = None
    """The `v41` / `v42` / `v43` plugin tree the client bound after detecting the instance's version."""

    workspace: Path
    workspace_kept: bool
    generated_at: datetime
    options: DoctorOptions
    phases: tuple[DoctorPhaseResult, ...] = ()

    @property
    def failed_phases(self) -> tuple[DoctorPhaseResult, ...]:
        """Every phase that concluded the toolchain is broken against this instance."""
        return tuple(phase for phase in self.phases if phase.outcome is DoctorOutcome.FAILED)

    @property
    def findings(self) -> tuple[DoctorFinding, ...]:
        """Every finding of the run, in phase order."""
        return tuple(finding for phase in self.phases for finding in phase.findings)

    @property
    def counts_by_outcome(self) -> dict[str, int]:
        """How many phases reached each outcome - the shape the verdict line is written from."""
        counted = Counter(phase.outcome.value for phase in self.phases)
        return {outcome.value: counted.get(outcome.value, 0) for outcome in DoctorOutcome}

    @property
    def verdict_line(self) -> str:
        """The whole run in one line: the verdict word, then how many phases reached each outcome."""
        counts = self.counts_by_outcome
        tail = ", ".join(f"{counts[outcome.value]} {outcome.value}" for outcome in DoctorOutcome)
        verdict = "BROKEN" if self.failed_phases else "USABLE"
        return f"{verdict}: {tail}"
Attributes
version_tree = None class-attribute instance-attribute

The v41 / v42 / v43 plugin tree the client bound after detecting the instance's version.

failed_phases property

Every phase that concluded the toolchain is broken against this instance.

findings property

Every finding of the run, in phase order.

counts_by_outcome property

How many phases reached each outcome - the shape the verdict line is written from.

verdict_line property

The whole run in one line: the verdict word, then how many phases reached each outcome.

PhaseOutcome

Bases: BaseModel

One phase's verdict before it is recorded: what it concluded, the line it says so on, its findings.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class PhaseOutcome(BaseModel):
    """One phase's verdict before it is recorded: what it concluded, the line it says so on, its findings."""

    model_config = ConfigDict(frozen=True)

    outcome: DoctorOutcome
    evidence: str
    findings: tuple[DoctorFinding, ...] = ()

CaptureOutcome

Bases: BaseModel

What one form contributed to the capture phase: whether it generated, whether it was accepted, and why not.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class CaptureOutcome(BaseModel):
    """What one form contributed to the capture phase: whether it generated, whether it was accepted, and why not."""

    model_config = ConfigDict(frozen=True)

    generated: bool
    accepted: bool
    findings: tuple[DoctorFinding, ...] = ()

FamilyOutcome

Bases: BaseModel

What one oracle family concluded: the line it is summarised by, and every mismatch it found.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
class FamilyOutcome(BaseModel):
    """What one oracle family concluded: the line it is summarised by, and every mismatch it found."""

    model_config = ConfigDict(frozen=True)

    summary: str
    findings: tuple[DoctorFinding, ...] = ()

Functions:

grade(phase, evidence, findings)

Grade one phase off what it found: an error breaks it, a warning degrades it, nothing passes it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def grade(phase: DoctorPhase, evidence: str, findings: Sequence[DoctorFinding]) -> PhaseOutcome:
    """Grade one phase off what it found: an error breaks it, a warning degrades it, nothing passes it."""
    if any(finding.severity == "error" for finding in findings):
        outcome = DoctorOutcome.FAILED
    elif findings:
        outcome = DoctorOutcome.WARNED
    else:
        outcome = DoctorOutcome.PASSED
    return PhaseOutcome(outcome=outcome, evidence=evidence, findings=_capped_findings(phase, findings))

generate_findings(notes)

Every note one generate run raised, as the findings the generate phase reports.

A note is a decision the emitters took against this instance's metadata - a form reshaped, a code fallen back to a UID, several tracked entity types left to the default resource - so the phase reports all of them and none of them breaks the run. The projection lives here rather than inline so a note's wording is what a test of the finding reads.

One note is one finding, however many targets raised it. Several targets read the same source notes - a form-structure note reaches the questionnaires, the examples, and the pages alike - and a table stating one fact about the instance three times says nothing the first row did not, while pushing other findings behind the cap that keeps it readable. GenerateNote is frozen, so this dedupes by value and keeps the order the targets raised them in.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def generate_findings(notes: Sequence[GenerateNote]) -> list[DoctorFinding]:
    """Every note one generate run raised, as the findings the generate phase reports.

    A note is a decision the emitters took against this instance's metadata - a form reshaped, a
    code fallen back to a UID, several tracked entity types left to the default resource - so the
    phase reports all of them and none of them breaks the run. The projection lives here rather
    than inline so a note's wording is what a test of the finding reads.

    One note is one finding, however many targets raised it. Several targets read the same source
    notes - a form-structure note reaches the questionnaires, the examples, and the pages alike -
    and a table stating one fact about the instance three times says nothing the first row did not,
    while pushing other findings behind the cap that keeps it readable. `GenerateNote` is frozen,
    so this dedupes by value and keeps the order the targets raised them in.
    """
    return [
        DoctorFinding(
            phase=DoctorPhase.GENERATE,
            severity="warning",
            subject=note.category.value,
            detail=note.message,
        )
        for note in dict.fromkeys(notes)
    ]

grade_capture(captured)

Grade the capture phase: an endpoint refusing what it generated is broken, a form it cannot fill is not.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def grade_capture(captured: Sequence[CaptureOutcome]) -> PhaseOutcome:
    """Grade the capture phase: an endpoint refusing what it generated is broken, a form it cannot fill is not."""
    findings = [finding for outcome in captured for finding in outcome.findings]
    generated = sum(1 for outcome in captured if outcome.generated)
    accepted = sum(1 for outcome in captured if outcome.accepted)
    evidence = f"{len(captured):,} form(s), {generated:,} generated, {accepted:,} accepted as 201"
    return grade(DoctorPhase.CAPTURE, evidence, findings)

grade_forward(report)

Grade the forward phase: a DHIS2 rejection is broken, a translator refusal is a note.

Rejections roll up by cause rather than by response, because DHIS2 names one row per broken rule and two hundred responses breaking the same rule are one thing to fix. What a dry run could not check is left out of the findings entirely: an event whose enrollment this very run would have created is unverifiable rather than refused, and an import is what settles it.

A drain that stopped early is an error of its own. It says the instance stopped answering part-way through, which is a fact about the instance this run is judging rather than about any payload, and a phase that passed on the strength of the half it managed would be reporting the quiet failure as a clean bill.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def grade_forward(report: ForwardReport) -> PhaseOutcome:
    """Grade the forward phase: a DHIS2 rejection is broken, a translator refusal is a note.

    Rejections roll up by cause rather than by response, because DHIS2 names one row per broken rule
    and two hundred responses breaking the same rule are one thing to fix. What a dry run could not
    check is left out of the findings entirely: an event whose enrollment this very run would have
    created is unverifiable rather than refused, and an import is what settles it.

    A drain that stopped early is an error of its own. It says the instance stopped answering
    part-way through, which is a fact about the instance this run is judging rather than about any
    payload, and a phase that passed on the strength of the half it managed would be reporting the
    quiet failure as a clean bill.
    """
    findings = [
        DoctorFinding(
            phase=DoctorPhase.FORWARD,
            severity="error",
            subject=reason.error_code or "no DHIS2 error code",
            detail=f"{reason.responses} response(s): {reason.reason}",
        )
        for reason in report.rejection_reasons
    ]
    if report.stopped is not None:
        findings.append(
            DoctorFinding(
                phase=DoctorPhase.FORWARD,
                severity="error",
                subject=report.stopped.response_id,
                detail=(
                    f"the drain stopped here and left {len(report.not_posted)} response(s) unposted: "
                    f"{report.stopped.reason}"
                ),
            )
        )
    findings.extend(
        DoctorFinding(
            phase=DoctorPhase.FORWARD,
            severity="warning",
            subject=outcome.response_id,
            detail="; ".join(f"{refusal.category}: {refusal.reason}" for refusal in outcome.refusals)
            or "the translator would not read this response whole",
        )
        for outcome in report.refused
    )
    return grade(DoctorPhase.FORWARD, report.counts_line, findings)

grade_oracle(families)

Grade the oracle phase off what the instance said about each family it judged.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def grade_oracle(families: Sequence[FamilyOutcome]) -> PhaseOutcome:
    """Grade the oracle phase off what the instance said about each family it judged."""
    findings = [finding for family in families for finding in family.findings]
    return grade(DoctorPhase.ORACLE, "; ".join(family.summary for family in families), findings)

drift_findings(report)

Every object that moved since the guide was published, as the findings the drift phase reports.

Drift is warning-class throughout, which is the line doctor already draws: a failure means the toolchain does not work against this instance, and a guide describing the instance as it stood last month still serves, still captures, and still forwards. It is out of date, not broken, and the exit code says so - only a fail exits 1.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def drift_findings(report: DriftReport) -> list[DoctorFinding]:
    """Every object that moved since the guide was published, as the findings the drift phase reports.

    Drift is warning-class throughout, which is the line doctor already draws: a failure means the
    toolchain does not work against this instance, and a guide describing the instance as it stood
    last month still serves, still captures, and still forwards. It is out of date, not broken, and
    the exit code says so - only a `fail` exits 1.
    """
    return [
        DoctorFinding(
            phase=DoctorPhase.DRIFT,
            severity="warning",
            subject=finding.title,
            detail=finding.detail,
            field_path=finding.kind.value,
        )
        for finding in report.findings
    ]

grade_drift(report)

Grade the drift phase: a moved object is a note about the guide, never a break in the toolchain.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def grade_drift(report: DriftReport) -> PhaseOutcome:
    """Grade the drift phase: a moved object is a note about the guide, never a break in the toolchain."""
    return grade(DoctorPhase.DRIFT, report.evidence, drift_findings(report))

resolve_published_project(workspace)

The published project this run checks for drift: the one the working directory sits in, or none.

Doctor's own workspace is not it. The run scaffolds and generates that project seconds before the drift phase reads it, so it can only ever agree with the instance - and a phase that always passes says nothing. A published guide is one somebody generated and compiled at some point in the past, which is exactly what a fhir.toml found by walking up from the working directory is.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def resolve_published_project(workspace: Path) -> FhirProject | None:
    """The published project this run checks for drift: the one the working directory sits in, or none.

    Doctor's own workspace is not it. The run scaffolds and generates that project seconds before the
    drift phase reads it, so it can only ever agree with the instance - and a phase that always
    passes says nothing. A published guide is one somebody generated and compiled at some point in
    the past, which is exactly what a `fhir.toml` found by walking up from the working directory is.
    """
    try:
        project = load_project()
    except (NoFhirProjectError, MalformedFhirConfigError, UnknownFhirConfigKeyError):
        return None
    return None if _is_inside(project.project_root, workspace) else project

resolve_doctor_profile()

The instance doctor runs against: d2w -p, then DHIS2_PROFILE, then a nearby project, then the default.

The same resolution d2w fhir validate uses, and for the same reason: doctor's subject is an instance rather than a project, and it scaffolds the project it works in, so a fhir.toml in the working directory is a source of the profile name and nothing else.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def resolve_doctor_profile() -> GenerationProfile:
    """The instance doctor runs against: `d2w -p`, then `DHIS2_PROFILE`, then a nearby project, then the default.

    The same resolution `d2w fhir validate` uses, and for the same reason: doctor's subject is an
    instance rather than a project, and it scaffolds the project it works in, so a `fhir.toml` in
    the working directory is a source of the profile name and nothing else.
    """
    return service.resolve_validation_context().generation

run_doctor(generation, options, *, reporter=None, client=None) async

Run every phase against the instance the profile names and report what each one concluded.

client is a connection the caller already holds open. Handed one, the connect phase reads the instance version off it and opens nothing, and the client is left open when the run ends - its lifetime belongs to whoever entered it. With none, the profile opens one for the run and closes it on every exit path.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
async def run_doctor(
    generation: GenerationProfile,
    options: DoctorOptions,
    *,
    reporter: ProgressReporter | None = None,
    client: Dhis2Client | None = None,
) -> DoctorReport:
    """Run every phase against the instance the profile names and report what each one concluded.

    `client` is a connection the caller already holds open. Handed one, the connect phase reads the
    instance version off it and opens nothing, and the client is left open when the run ends - its
    lifetime belongs to whoever entered it. With none, the profile opens one for the run and closes
    it on every exit path.
    """
    return await _DoctorRun(generation, options, reporter, client).execute()

render_doctor_markdown(report)

Render one run as the markdown report a handover is read from, phase table first, findings after.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def render_doctor_markdown(report: DoctorReport) -> str:
    """Render one run as the markdown report a handover is read from, phase table first, findings after."""
    lines = [
        "# fhir doctor report",
        "",
        f"- Profile: {report.profile_name} ({report.profile_origin})",
        f"- Instance: {report.base_url}",
        f"- DHIS2 version: {report.dhis2_version or 'not detected'} (plugin tree {report.version_tree or 'not bound'})",
        f"- Workspace: {report.workspace}{'' if report.workspace_kept else ' (removed when the run ended)'}",
        f"- Ran: {report.generated_at.isoformat(timespec='seconds')}",
        f"- Verdict: {report.verdict_line}",
        "",
        "## Phases",
        "",
        "| Phase | Outcome | Evidence |",
        "| --- | --- | --- |",
    ]
    lines.extend(
        f"| {phase.phase.value} | {phase.outcome.value} | {_markdown_cell(phase_evidence(phase))} |"
        for phase in report.phases
    )
    lines.append("")
    findings = report.findings
    if findings:
        lines.extend(
            [
                "## Findings",
                "",
                "| Phase | Severity | Subject | Where | What |",
                "| --- | --- | --- | --- | --- |",
            ]
        )
        lines.extend(
            f"| {finding.phase.value} | {finding.severity} | {_markdown_cell(finding.subject)} "
            f"| {_markdown_cell(finding.field_path or '')} | {_markdown_cell(finding.detail)} |"
            for finding in findings
        )
        lines.append("")
    else:
        lines.extend(["## Findings", "", "No phase found anything to report against this instance.", ""])
    return "\n".join(lines)

phase_evidence(phase)

The one line a phase is read by: its evidence, with the reason folded in when it did not run.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
def phase_evidence(phase: DoctorPhaseResult) -> str:
    """The one line a phase is read by: its evidence, with the reason folded in when it did not run."""
    if phase.reason is None:
        return phase.evidence
    return f"{phase.evidence} ({phase.reason})" if phase.evidence else phase.reason

Drift between a published guide and the instance

Whether the guide already on disk still describes the instance it was generated from. detect_drift reads the instance for everything one project publishes and names every organisation unit, option, question, and program stage that moved inside that project's own selection scope; read_published_guide is its offline half, projecting the published artifacts onto the objects the comparison is about. The three comparators - compare_organisation_units, compare_option_set, and compare_form - are pure over a PublishedGuide side and an instance side, so a caller can grade a snapshot it holds without opening a connection. Every finding is a DriftFinding naming what moved, which direction it moved in, and what each side says about it.

drift

What the instance holds that the published guide does not: the drift report behind doctor's last phase.

A guide is a photograph. d2w fhir generate reads the instance once, the compiler turns that reading into artifacts, and from then on the artifacts say what the instance said on the day they were written. DHIS2 keeps moving: a chiefdom is split, an option is added to a set, a question is dropped from a stage. Nothing in the published guide knows, and nothing in the toolchain said so until this module.

Drift is measured inside the project's own selection scope and nowhere else. An organisation unit outside [generate.organisation_units] is not drift - the project never asked for it - and neither is a data set the selection tables leave out. The scope is read from the same fhir.toml the publication was generated under, so the question is always "does this guide still describe the part of the instance it claims to describe", never "has the instance changed anywhere".

The five classes

Class Published side Instance side
Organisation units the Location of every unit in the registry the hierarchy under root, to max_level
Options the concepts of a published option-set CodeSystem that option set's options
Tracked entity attributes a tracker or tracked-entity form's questions the program's or type's attributes
Data elements an aggregate, event, or tracker-event form's questions the data set's or stage's elements
Program stages the tracker-event forms a tracker program publishes that program's stages

Each class reports in both directions and on renames alike: something the instance gained, something it lost, and something whose name changed under an identity that did not. A rename matters because the published display is what a reader of the guide sees - D2TEA_CS carries every attribute's name, a CodeSystem concept carries every option's - so a guide naming an object what the instance no longer calls it is wrong in the way documentation is wrong.

Tracked entity types are out of scope. d2w fhir validate already names every type the project never typed, under unmapped-tracked-entity-type, and one fact reported twice in two vocabularies is worse than one report; the drift phase points at that checklist rather than repeating it.

What is compared, and what is deliberately not

Identity is the DHIS2 UID throughout, because that is what survives a rename and what a consumer joins on. Names are compared through flatten_whitespace, and a published name also matches the wording substitute_build_aborting_text produces: a project generated with hostile_names = "substitute" publishes "Fixed, under 1y" for an instance that says "Fixed, <1y", and reporting that as a rename would be reporting the toolchain's own rewrite back at the reader.

Codes are not compared. A code change is a real event, but it is one the identifier slices already carry and one d2w fhir validate grades for FHIR-safety, so the drift report stays about the objects a form asks and the names a reader reads.

The remedy is the same sentence for every finding, which is why it is stated once on the phase rather than once per row: regenerate, then compile. That is the documented lifecycle - d2w fhir generate re-reads the instance, make sushi turns the new source into artifacts - and nothing about a drifted object needs a different answer.

Classes

DriftSubject

Bases: StrEnum

The kinds of object this report covers, each spelled as a reader of the guide would say it.

The first five are what the report is about: the objects a guide carries inside its artifacts. The rest name the DHIS2 object one whole artifact was generated from, for the one case where the artifact outlived it - a selected option set or form the instance no longer holds. Naming that object as an option or a data element would be naming the wrong thing.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class DriftSubject(StrEnum):
    """The kinds of object this report covers, each spelled as a reader of the guide would say it.

    The first five are what the report is about: the objects a guide carries inside its artifacts.
    The rest name the DHIS2 object one whole artifact was generated from, for the one case where the
    artifact outlived it - a selected option set or form the instance no longer holds. Naming that
    object as an option or a data element would be naming the wrong thing.
    """

    ORGANISATION_UNIT = "organisation unit"
    OPTION = "option"
    TRACKED_ENTITY_ATTRIBUTE = "tracked entity attribute"
    DATA_ELEMENT = "data element"
    PROGRAM_STAGE = "program stage"

    OPTION_SET = "option set"
    DATA_SET = "data set"
    EVENT_PROGRAM = "event program"
    TRACKER_PROGRAM = "tracker program"
    TRACKER_PROGRAM_STAGE = "tracker program stage"
    TRACKED_ENTITY_TYPE = "tracked entity type"

DriftKind

Bases: StrEnum

Which direction one object drifted in.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class DriftKind(StrEnum):
    """Which direction one object drifted in."""

    #: The instance holds it and the guide publishes nothing for it.
    ADDED = "added"

    #: The guide publishes it and the instance no longer holds it.
    REMOVED = "removed"

    #: Both hold it, under one identity, under two names.
    RENAMED = "renamed"

DriftFinding

Bases: BaseModel

One drifted object: what it is, where the guide carries it, and what each side says about it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class DriftFinding(BaseModel):
    """One drifted object: what it is, where the guide carries it, and what each side says about it."""

    model_config = ConfigDict(frozen=True)

    subject: DriftSubject
    kind: DriftKind
    uid: str
    holder: str
    """The published artifact the object belongs to, in the words the guide names that artifact by."""

    published_name: str | None = None
    instance_name: str | None = None

    @property
    def name(self) -> str:
        """The one name to call this object by: the instance's when it holds it, the guide's otherwise."""
        return self.instance_name or self.published_name or self.uid

    @property
    def title(self) -> str:
        """The object as a finding names it: what it is, what it is called, and its DHIS2 UID."""
        return f"{self.subject.value} {self.name} ({self.uid})"

    @property
    def detail(self) -> str:
        """What each side says about this object, as the one line the finding is read by."""
        if self.kind is DriftKind.ADDED:
            return f"the instance holds it in {self.holder}; the guide publishes nothing for it"
        if self.kind is DriftKind.REMOVED:
            return f"the guide publishes it under {self.holder}; the instance no longer holds it there"
        return (
            f"the guide publishes the name {self.published_name!r} under {self.holder}; "
            f"the instance now says {self.instance_name!r}"
        )
Attributes
holder instance-attribute

The published artifact the object belongs to, in the words the guide names that artifact by.

name property

The one name to call this object by: the instance's when it holds it, the guide's otherwise.

title property

The object as a finding names it: what it is, what it is called, and its DHIS2 UID.

detail property

What each side says about this object, as the one line the finding is read by.

PublishedObject

Bases: BaseModel

One DHIS2 object as the published guide carries it: the UID it is keyed by, the name it shows.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class PublishedObject(BaseModel):
    """One DHIS2 object as the published guide carries it: the UID it is keyed by, the name it shows."""

    model_config = ConfigDict(frozen=True)

    uid: str
    name: str | None = None

PublishedOptionSet

Bases: BaseModel

One option-set CodeSystem the guide publishes, and the options its concepts stand for.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class PublishedOptionSet(BaseModel):
    """One option-set CodeSystem the guide publishes, and the options its concepts stand for."""

    model_config = ConfigDict(frozen=True)

    uid: str
    title: str | None = None
    options: tuple[PublishedObject, ...] = ()
    """One entry per concept, keyed by the concept code - the option UID, or its code in code mode."""
Attributes
options = () class-attribute instance-attribute

One entry per concept, keyed by the concept code - the option UID, or its code in code mode.

PublishedForm

Bases: BaseModel

One Questionnaire the guide publishes: which DHIS2 object it is keyed to, and what it asks.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class PublishedForm(BaseModel):
    """One Questionnaire the guide publishes: which DHIS2 object it is keyed to, and what it asks."""

    model_config = ConfigDict(frozen=True)

    resource_id: str
    kind: FormKind
    """The DHIS2 form kind off the form's own `code`: aggregate, event, tracker, tracker-event, tracked-entity."""

    uid: str
    title: str | None = None
    questions: tuple[PublishedObject, ...] = ()

    @property
    def asks_attributes(self) -> bool:
        """Whether this form's questions are tracked entity attributes rather than data elements."""
        return self.kind in _ATTRIBUTE_FORM_KINDS

    @property
    def holder(self) -> str:
        """How a finding names the form the drifted question belongs to."""
        return f"the published form {self.title or self.resource_id} ({self.uid})"
Attributes
kind instance-attribute

The DHIS2 form kind off the form's own code: aggregate, event, tracker, tracker-event, tracked-entity.

asks_attributes property

Whether this form's questions are tracked entity attributes rather than data elements.

holder property

How a finding names the form the drifted question belongs to.

PublishedGuide

Bases: BaseModel

Everything on disk this report reads: the registry, the published option sets, the published forms.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class PublishedGuide(BaseModel):
    """Everything on disk this report reads: the registry, the published option sets, the published forms."""

    model_config = ConfigDict(frozen=True)

    organisation_units: tuple[PublishedObject, ...] = ()
    option_sets: tuple[PublishedOptionSet, ...] = ()
    forms: tuple[PublishedForm, ...] = ()

    @property
    def program_stage_uids(self) -> frozenset[str]:
        """Every program stage the guide publishes a form for, which is what a new stage is missing from."""
        return frozenset(form.uid for form in self.forms if form.kind == "tracker-event")
Attributes
program_stage_uids property

Every program stage the guide publishes a form for, which is what a new stage is missing from.

InstanceObject

Bases: BaseModel

One DHIS2 object as the instance states it today.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class InstanceObject(BaseModel):
    """One DHIS2 object as the instance states it today."""

    model_config = ConfigDict(frozen=True)

    uid: str
    name: str = ""

InstanceOption

Bases: BaseModel

One option as the instance states it today, under both the identities a concept can be keyed by.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class InstanceOption(BaseModel):
    """One option as the instance states it today, under both the identities a concept can be keyed by."""

    model_config = ConfigDict(frozen=True)

    uid: str
    code: str | None = None
    name: str = ""

InstanceOptionSet

Bases: BaseModel

One option set as the instance states it today.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class InstanceOptionSet(BaseModel):
    """One option set as the instance states it today."""

    model_config = ConfigDict(frozen=True)

    uid: str
    name: str = ""
    options: tuple[InstanceOption, ...] = ()

InstanceForm

Bases: BaseModel

What the instance says a published form's DHIS2 object collects today.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class InstanceForm(BaseModel):
    """What the instance says a published form's DHIS2 object collects today."""

    model_config = ConfigDict(frozen=True)

    uid: str
    name: str = ""
    questions: tuple[InstanceObject, ...] = ()
    stages: tuple[InstanceObject, ...] = ()
    """The program's stages, read only for a tracker program - the one kind that publishes a form per stage."""
Attributes
stages = () class-attribute instance-attribute

The program's stages, read only for a tracker program - the one kind that publishes a form per stage.

DriftReport

Bases: BaseModel

Everything one drift pass concluded: what it read, what it read it against, and what moved.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
class DriftReport(BaseModel):
    """Everything one drift pass concluded: what it read, what it read it against, and what moved."""

    model_config = ConfigDict(frozen=True)

    registry_scope: str
    organisation_unit_count: int = 0
    option_set_count: int = 0
    form_count: int = 0
    findings: tuple[DriftFinding, ...] = ()

    @property
    def evidence(self) -> str:
        """The one line this pass is read by, quiet when nothing drifted and remedied when something did."""
        read = (
            f"{self.organisation_unit_count:,} organisation unit(s), {self.option_set_count:,} option set(s), "
            f"and {self.form_count:,} form(s) read against {self.registry_scope}"
        )
        if not self.findings:
            return f"the guide publishes the instance as it now stands: {read}"
        return (
            f"{len(self.findings):,} object(s) moved since the guide was published: {read}. "
            f"{DRIFT_REMEDY}. {TRACKED_ENTITY_TYPE_CROSS_REFERENCE}"
        )
Attributes
evidence property

The one line this pass is read by, quiet when nothing drifted and remedied when something did.

Functions:

registry_scope_line(config)

The slice of the hierarchy the registry claims to publish, in the words the evidence states it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
def registry_scope_line(config: GenerateConfig) -> str:
    """The slice of the hierarchy the registry claims to publish, in the words the evidence states it."""
    selection = config.organisation_units
    if selection.root is not None and selection.max_level is not None:
        return f"the hierarchy under {selection.root} down to level {selection.max_level}"
    if selection.root is not None:
        return f"the hierarchy under {selection.root}"
    if selection.max_level is not None:
        return f"the hierarchy down to level {selection.max_level}"
    return "the whole hierarchy"

read_published_guide(project)

Read what one project publishes off disk, through the reader the served store and check-artifacts use.

load_compiled_artifacts is the single reader of the two published trees - ig/fsh-generated for what the compiler wrote, ig/input/resources for the registry and terminology the emitters wrote straight to JSON - so a drift pass, a served store, and a conversion context can never disagree about what a project publishes. It raises CompiledIgMissingError on a project that was generated but never compiled, which is a fact about the project rather than about the instance.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
def read_published_guide(project: FhirProject) -> PublishedGuide:
    """Read what one project publishes off disk, through the reader the served store and check-artifacts use.

    `load_compiled_artifacts` is the single reader of the two published trees - `ig/fsh-generated`
    for what the compiler wrote, `ig/input/resources` for the registry and terminology the emitters
    wrote straight to JSON - so a drift pass, a served store, and a conversion context can never
    disagree about what a project publishes. It raises `CompiledIgMissingError` on a project that was
    generated but never compiled, which is a fact about the project rather than about the instance.
    """
    artifacts = load_compiled_artifacts(project)
    generate = project.config.generate
    identifier_base = f"{generate.identifier_system_base}/id"
    naming = QuestionnaireNaming.from_naming(generate.naming)
    canonical = project.config.ig.canonical
    question_systems = {
        "data-element": code_system_canonical(canonical, naming.data_element_code_system_id),
        "tracked-entity-attribute": code_system_canonical(canonical, naming.tracked_entity_attribute_code_system_id),
    }
    return PublishedGuide(
        organisation_units=_published_organisation_units(artifacts.locations, identifier_base),
        option_sets=_published_option_sets(artifacts.code_systems, identifier_base),
        forms=_published_forms(artifacts.questionnaires, identifier_base, question_systems),
    )

detect_drift(client, project) async

Read the instance for everything one published guide claims, and report every object that moved.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
async def detect_drift(client: Dhis2Client, project: FhirProject) -> DriftReport:
    """Read the instance for everything one published guide claims, and report every object that moved."""
    published = read_published_guide(project)
    generate = project.config.generate
    findings: list[DriftFinding] = []
    findings.extend(
        compare_organisation_units(published.organisation_units, await _instance_organisation_units(client, generate))
    )
    instance_option_sets = await _instance_option_sets(client, [option_set.uid for option_set in published.option_sets])
    for option_set in published.option_sets:
        findings.extend(compare_option_set(option_set, instance_option_sets.get(option_set.uid)))
    instance_forms = await _instance_forms(client, published.forms)
    for form in published.forms:
        findings.extend(compare_form(form, instance_forms.get(form.uid), published.program_stage_uids))
    return DriftReport(
        registry_scope=registry_scope_line(generate),
        organisation_unit_count=len(published.organisation_units),
        option_set_count=len(published.option_sets),
        form_count=len(published.forms),
        findings=tuple(findings),
    )

compare_organisation_units(published, instance)

Judge the published registry against the hierarchy slice the project selected.

Both sides are already narrowed to the selection, so every difference is drift rather than a unit the project never asked for: a unit the instance gained inside the scope, a unit it lost, and a unit whose name changed under a UID that did not.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
def compare_organisation_units(
    published: Sequence[PublishedObject], instance: Sequence[InstanceObject]
) -> list[DriftFinding]:
    """Judge the published registry against the hierarchy slice the project selected.

    Both sides are already narrowed to the selection, so every difference is drift rather than a
    unit the project never asked for: a unit the instance gained inside the scope, a unit it lost,
    and a unit whose name changed under a UID that did not.
    """
    holder = "the registry scope this project publishes"
    return _compare(DriftSubject.ORGANISATION_UNIT, holder, published, instance)

compare_option_set(published, instance)

Judge one published CodeSystem's concepts against the options the instance's set holds today.

A concept is keyed by the option UID under concept_code_source = "id" and by the option's DHIS2 code under "code", so the instance side is indexed under both and the comparison holds whichever mode the guide was published in.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
def compare_option_set(published: PublishedOptionSet, instance: InstanceOptionSet | None) -> list[DriftFinding]:
    """Judge one published CodeSystem's concepts against the options the instance's set holds today.

    A concept is keyed by the option UID under `concept_code_source = "id"` and by the option's DHIS2
    code under `"code"`, so the instance side is indexed under both and the comparison holds whichever
    mode the guide was published in.
    """
    holder = f"the published option set {published.title or published.uid} ({published.uid})"
    if instance is None:
        return [
            DriftFinding(
                subject=DriftSubject.OPTION_SET,
                kind=DriftKind.REMOVED,
                uid=published.uid,
                holder=_SELECTION_HOLDER,
                published_name=published.title,
            )
        ]
    by_identity: dict[str, InstanceOption] = {}
    for option in instance.options:
        by_identity[option.uid] = option
        if option.code:
            by_identity.setdefault(option.code, option)
    findings: list[DriftFinding] = []
    for concept in published.options:
        matched = by_identity.get(concept.uid)
        if matched is None:
            findings.append(
                DriftFinding(
                    subject=DriftSubject.OPTION,
                    kind=DriftKind.REMOVED,
                    uid=concept.uid,
                    holder=holder,
                    published_name=concept.name,
                )
            )
        elif not _names_agree(concept.name, matched.name):
            findings.append(
                DriftFinding(
                    subject=DriftSubject.OPTION,
                    kind=DriftKind.RENAMED,
                    uid=concept.uid,
                    holder=holder,
                    published_name=concept.name,
                    instance_name=flatten_whitespace(matched.name),
                )
            )
    concept_codes = {concept.uid for concept in published.options}
    findings.extend(
        DriftFinding(
            subject=DriftSubject.OPTION, kind=DriftKind.ADDED, uid=option.uid, holder=holder, instance_name=option.name
        )
        for option in instance.options
        if option.uid not in concept_codes and (option.code or "") not in concept_codes
    )
    return findings

compare_form(published, instance, published_stage_uids)

Judge one published form's questions against what the instance says its DHIS2 object collects.

A tracker registration form answers for its program's attributes and a tracked-entity form for its type's; every other kind answers for data elements. A tracker program is also asked for its stages, because a stage the program gained publishes no form and so asks none of its questions.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
def compare_form(
    published: PublishedForm, instance: InstanceForm | None, published_stage_uids: frozenset[str]
) -> list[DriftFinding]:
    """Judge one published form's questions against what the instance says its DHIS2 object collects.

    A tracker registration form answers for its program's attributes and a `tracked-entity` form for
    its type's; every other kind answers for data elements. A tracker program is also asked for its
    stages, because a stage the program gained publishes no form and so asks none of its questions.
    """
    subject = DriftSubject.TRACKED_ENTITY_ATTRIBUTE if published.asks_attributes else DriftSubject.DATA_ELEMENT
    if instance is None:
        return [
            DriftFinding(
                subject=_FORM_SUBJECTS[published.kind],
                kind=DriftKind.REMOVED,
                uid=published.uid,
                holder=_SELECTION_HOLDER,
                published_name=published.title,
            )
        ]
    findings = _compare(subject, published.holder, published.questions, instance.questions)
    findings.extend(
        DriftFinding(
            subject=DriftSubject.PROGRAM_STAGE,
            kind=DriftKind.ADDED,
            uid=stage.uid,
            holder=published.holder,
            instance_name=stage.name,
        )
        for stage in instance.stages
        if stage.uid not in published_stage_uids
    )
    return findings

The capture spool

Where d2w fhir serve writes a receipt and where d2w fhir forward moves it next. The layout is duplicated rather than imported - dhis2w-fhir is a dependency of dhis2w-fhir-serve, so the arrow only points one way and the forwarder reads the files directly under the same conventions.

spool

The capture spool the forwarder drains: where a received response is read from, and where it moves next.

d2w fhir serve writes every accepted QuestionnaireResponse to <spool root>/received/<id>.json as a receipt envelope - the root being .serve/responses inside the project unless [serve] spool_dir names another - and ls on that directory is the pending count. This module is the read side of the same convention plus the four states a receipt can end in:

`received/`   captured, not yet forwarded - the queue.
`forwarded/`  translated, posted, and accepted by DHIS2.
`rejected/`   translated and posted, and DHIS2 refused it.
`withdrawn/`  it landed, and `d2w fhir withdraw` retracted it from DHIS2 afterwards.

Three of those states carry an <id>.report.json sidecar holding what DHIS2 answered. A rejection needs one to say why it was refused; an acceptance needs one because "DHIS2 took it" is not the whole answer either - the import counts are what say how much of it landed, and a receipt filed with nothing beside it leaves that unanswerable from the spool alone.

withdrawn/ is the one state a receipt reaches without being posted again. Its report is what DHIS2 answered the delete, and the report of the import that landed it stays behind in forwarded/: that document is still true of that import, and the two answer different questions. Withdrawal is terminal by DHIS2's rule rather than by ours - the UID a tracker delete burns is refused under every import strategy afterwards - so nothing moves a receipt back out of withdrawn/.

A fourth directory, malformed/, is a holding pen rather than a state: a file that does not read as a receipt is moved there with its reason beside it, and the read that found it carries on. One unreadable byte on disk must not cost the other two hundred receipts their drain, and a file skipped without being named is a submission that has silently disappeared - so the policy is move it, name it, and continue. A directory that cannot be read at all is a different failure and is still raised.

A conversion-refused response never moves, unless what refused it can never be fixed: entered-in-error is a withdrawal, which this toolchain does not build, so that one is filed to rejected/ rather than retried by every drain forever. Every other refusal has a fix in the guide or in the data, so leaving it in received/ makes the next d2w fhir forward a retry with no bookkeeping at all. What a committing drain does leave behind is an <id>.refusal.json beside the receipt - when the drain saw it, how many drains have refused it, and why - so a listing can tell a receipt drains keep refusing from one no drain has touched. The same sidecar carries the other refusal a drain makes and leaves queued: a response whose aggregate values a forwarded receipt already sent, under [forward] overwrites = "refuse". The marker is about the queue, so the move that finally drains the receipt deletes it: the import report supersedes it.

The layout is duplicated rather than imported: dhis2w-fhir is a dependency of dhis2w-fhir-serve, so the arrow only points one way and the forwarder reads the files directly under the same conventions. Moves are os.replace within one filesystem, so a response is in exactly one state at every instant.

WHERE the tree sits is the one thing both sides share code for. [serve] spool_dir moves it, and the server that writes a receipt and the drain that files it have to land on the same directory to the character - so resolve_spool_root here is what both of them resolve the key through, and the duplicated names above are the layout under that root rather than a second answer to where it is.

One drain at a time. A .drain.lock in the spool root carries an exclusive flock for the length of a run and the draining process id inside it, because two drains over one spool would post the same receipt twice and race each other's renames.

Classes

SpoolReadError

Bases: LookupError

Raised when a spool directory cannot be read at all - a permission, a device, a broken mount.

Not what an unreadable file raises: one bad file is quarantined and named, because losing a whole drain to it would be the larger failure. This is the directory itself refusing.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolReadError(LookupError):
    """Raised when a spool directory cannot be read at all - a permission, a device, a broken mount.

    Not what an unreadable *file* raises: one bad file is quarantined and named, because losing a
    whole drain to it would be the larger failure. This is the directory itself refusing.
    """

SpoolLockedError

Bases: LookupError

Raised when another drain of the same spool holds the lock, naming the process that holds it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolLockedError(LookupError):
    """Raised when another drain of the same spool holds the lock, naming the process that holds it."""

SpoolState

Bases: StrEnum

Which of the spool's four directories a receipt currently sits in.

Three of the four are the drain's: a receipt is captured, and DHIS2 either takes the payload or refuses it. The fourth is an operator's, and it is the only one that is reached without posting the receipt again - d2w fhir withdraw retracts from DHIS2 what a forwarded receipt landed, and files the receipt here to say the submission stands withdrawn rather than unsent.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolState(StrEnum):
    """Which of the spool's four directories a receipt currently sits in.

    Three of the four are the drain's: a receipt is captured, and DHIS2 either takes the payload or
    refuses it. The fourth is an operator's, and it is the only one that is reached without posting
    the receipt again - `d2w fhir withdraw` retracts from DHIS2 what a forwarded receipt landed, and
    files the receipt here to say the submission stands withdrawn rather than unsent.
    """

    RECEIVED = "received"
    FORWARDED = "forwarded"
    REJECTED = "rejected"
    WITHDRAWN = "withdrawn"

SpoolLayout

Bases: BaseModel

One project's spool on disk: the project it belongs to, and the root its receipts live under.

Carried rather than recomputed, because every move in a drain is a rename between two directories of this layout and a second opinion about where the root is would file a receipt where nothing looks for it. project_root rides along because a report names a spool file relative to the project, which stays true of a root outside it - the path is simply absolute there.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolLayout(BaseModel):
    """One project's spool on disk: the project it belongs to, and the root its receipts live under.

    Carried rather than recomputed, because every move in a drain is a rename between two
    directories of this layout and a second opinion about where the root is would file a receipt
    where nothing looks for it. `project_root` rides along because a report names a spool file
    relative to the project, which stays true of a root outside it - the path is simply absolute there.
    """

    model_config = ConfigDict(frozen=True)

    project_root: Path
    root: Path

    @classmethod
    def resolve(cls, project_root: Path, spool_dir: str = SPOOL_RELATIVE_PATH) -> SpoolLayout:
        """The layout one project's `[serve] spool_dir` names, defaulting to the tree inside the project."""
        return cls(project_root=project_root, root=resolve_spool_root(project_root, spool_dir))

    def directory_for(self, state: SpoolState) -> Path:
        """Where receipts in one state are read from and moved to."""
        return self.root / SPOOL_STATE_DIRECTORY_NAMES[state]

    @property
    def malformed_directory(self) -> Path:
        """The holding pen for files that do not read as receipts, which is no receipt's state."""
        return self.root / MALFORMED_DIRECTORY_NAME

    @property
    def lock_path(self) -> Path:
        """The lockfile one drain holds for its whole run."""
        return self.root / DRAIN_LOCK_FILE_NAME
Attributes
malformed_directory property

The holding pen for files that do not read as receipts, which is no receipt's state.

lock_path property

The lockfile one drain holds for its whole run.

Methods:
resolve(project_root, spool_dir=SPOOL_RELATIVE_PATH) classmethod

The layout one project's [serve] spool_dir names, defaulting to the tree inside the project.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
@classmethod
def resolve(cls, project_root: Path, spool_dir: str = SPOOL_RELATIVE_PATH) -> SpoolLayout:
    """The layout one project's `[serve] spool_dir` names, defaulting to the tree inside the project."""
    return cls(project_root=project_root, root=resolve_spool_root(project_root, spool_dir))
directory_for(state)

Where receipts in one state are read from and moved to.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def directory_for(self, state: SpoolState) -> Path:
    """Where receipts in one state are read from and moved to."""
    return self.root / SPOOL_STATE_DIRECTORY_NAMES[state]

QuarantinedFile

Bases: BaseModel

One file moved to malformed/ because it does not read as a receipt, and what stopped it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class QuarantinedFile(BaseModel):
    """One file moved to `malformed/` because it does not read as a receipt, and what stopped it."""

    model_config = ConfigDict(frozen=True)

    file_name: str
    """The name the file had in the state directory, kept unchanged by the move."""

    reason: str
Attributes
file_name instance-attribute

The name the file had in the state directory, kept unchanged by the move.

SpooledResponse

Bases: BaseModel

One receipt off the spool: the response to translate, plus where its file currently sits.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpooledResponse(BaseModel):
    """One receipt off the spool: the response to translate, plus where its file currently sits."""

    model_config = ConfigDict(frozen=True)

    response_id: str
    received_at: str
    form_kind: str
    questionnaire: str
    submitted_by: str | None = None
    """The DHIS2 username the facade validated the submission under, or None when it validated none.

    Facade-side provenance. It says who handed this receipt to `d2w fhir serve`, and says nothing
    about the identity the values reach DHIS2 under - a drain posts as the forwarding profile, and
    `storedBy` on the instance is DHIS2's own stamp of that profile.
    """

    layout: SpoolLayout
    """The spool this receipt came off, which is the one that files it - see `SpoolLayout`."""

    path: Path
    """The file this receipt was read from, which is what the lifecycle moves."""

    @property
    def project_root(self) -> Path:
        """The project this receipt belongs to, which is what a report names its path relative to."""
        return self.layout.project_root

    response: QuestionnaireResponse
    """The captured response as a model, parsed from the verbatim resource the receipt carries."""
Attributes
submitted_by = None class-attribute instance-attribute

The DHIS2 username the facade validated the submission under, or None when it validated none.

Facade-side provenance. It says who handed this receipt to d2w fhir serve, and says nothing about the identity the values reach DHIS2 under - a drain posts as the forwarding profile, and storedBy on the instance is DHIS2's own stamp of that profile.

layout instance-attribute

The spool this receipt came off, which is the one that files it - see SpoolLayout.

path instance-attribute

The file this receipt was read from, which is what the lifecycle moves.

project_root property

The project this receipt belongs to, which is what a report names its path relative to.

response instance-attribute

The captured response as a model, parsed from the verbatim resource the receipt carries.

SpoolReading

Bases: BaseModel

What one read of received/ found: the receipts to drain, and the files it moved aside.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolReading(BaseModel):
    """What one read of `received/` found: the receipts to drain, and the files it moved aside."""

    model_config = ConfigDict(frozen=True)

    responses: tuple[SpooledResponse, ...] = ()
    quarantined: tuple[QuarantinedFile, ...] = ()

RefusalReason

Bases: BaseModel

One reason a drain refused a spooled response, as the refusal record states it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class RefusalReason(BaseModel):
    """One reason a drain refused a spooled response, as the refusal record states it."""

    model_config = ConfigDict(frozen=True)

    category: str
    element: str | None = None
    reason: str

ForwardRefusalRecord

Bases: BaseModel

What the last committing drain wrote beside a receipt it refused to send.

The receipt itself never moves and is never rewritten - see the module docstring - so this sidecar is the whole of the drain's mark on the queue: when it last looked, how many drains have refused the receipt, and why. Deleted by the move that finally drains the receipt.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class ForwardRefusalRecord(BaseModel):
    """What the last committing drain wrote beside a receipt it refused to send.

    The receipt itself never moves and is never rewritten - see the module docstring - so this
    sidecar is the whole of the drain's mark on the queue: when it last looked, how many drains
    have refused the receipt, and why. Deleted by the move that finally drains the receipt.
    """

    model_config = ConfigDict(frozen=True)

    refused_at: str
    """The instant the last committing drain refused the receipt, as a FHIR `instant` (UTC)."""

    attempt_count: int = 1
    """How many committing drains have refused this receipt so far."""

    reasons: tuple[RefusalReason, ...] = ()

    @property
    def line(self) -> str:
        """The record as the one line a listing row shows: the first reason, or the bare fact."""
        if self.reasons:
            return self.reasons[0].reason
        return "a forward run refused this response"
Attributes
refused_at instance-attribute

The instant the last committing drain refused the receipt, as a FHIR instant (UTC).

attempt_count = 1 class-attribute instance-attribute

How many committing drains have refused this receipt so far.

line property

The record as the one line a listing row shows: the first reason, or the bare fact.

SpooledReceipt

Bases: BaseModel

One receipt's envelope as a listing reads it, without translating the resource it carries.

A listing answers where a receipt is and what it answers, and neither question needs the QuestionnaireResponse parsed - so a receipt whose resource this package's models cannot read is still a row rather than a hole.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpooledReceipt(BaseModel):
    """One receipt's envelope as a listing reads it, without translating the resource it carries.

    A listing answers where a receipt is and what it answers, and neither question needs the
    QuestionnaireResponse parsed - so a receipt whose resource this package's models cannot read is
    still a row rather than a hole.
    """

    model_config = ConfigDict(frozen=True)

    response_id: str
    received_at: str
    form_kind: str
    questionnaire: str
    submitted_by: str | None = None
    """The DHIS2 username the facade validated this submission under, or None when it validated none."""

    state: SpoolState
    path: Path

    refusal: ForwardRefusalRecord | None = None
    """The last committing drain's refusal of this still-queued receipt, when one is on disk."""
Attributes
submitted_by = None class-attribute instance-attribute

The DHIS2 username the facade validated this submission under, or None when it validated none.

refusal = None class-attribute instance-attribute

The last committing drain's refusal of this still-queued receipt, when one is on disk.

SpoolContents

Bases: BaseModel

Every receipt one project's spool holds, in each state, plus what is sitting in quarantine.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
class SpoolContents(BaseModel):
    """Every receipt one project's spool holds, in each state, plus what is sitting in quarantine."""

    model_config = ConfigDict(frozen=True)

    receipts: tuple[SpooledReceipt, ...] = ()
    quarantined: tuple[QuarantinedFile, ...] = ()

    def in_state(self, state: SpoolState) -> tuple[SpooledReceipt, ...]:
        """Every receipt sitting in one state, in arrival order with the receipt id as the tiebreaker."""
        return tuple(receipt for receipt in self.receipts if receipt.state is state)
Methods:
in_state(state)

Every receipt sitting in one state, in arrival order with the receipt id as the tiebreaker.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def in_state(self, state: SpoolState) -> tuple[SpooledReceipt, ...]:
    """Every receipt sitting in one state, in arrival order with the receipt id as the tiebreaker."""
    return tuple(receipt for receipt in self.receipts if receipt.state is state)

Functions:

resolve_spool_root(project_root, spool_dir=SPOOL_RELATIVE_PATH)

Where one project's spool root sits: the stated directory, against the project unless absolute.

The single answer both the server and the forwarder resolve [serve] spool_dir through. A relative path is the ordinary case and keeps the tree inside the project the receipts belong to; an absolute one is taken as written, which is how a spool lands on a volume the operator chose - and everything outside the project (version control, backups) is theirs to arrange from there.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def resolve_spool_root(project_root: Path, spool_dir: str = SPOOL_RELATIVE_PATH) -> Path:
    """Where one project's spool root sits: the stated directory, against the project unless absolute.

    The single answer both the server and the forwarder resolve `[serve] spool_dir` through. A
    relative path is the ordinary case and keeps the tree inside the project the receipts belong to;
    an absolute one is taken as written, which is how a spool lands on a volume the operator chose -
    and everything outside the project (version control, backups) is theirs to arrange from there.
    """
    stated = Path(spool_dir.strip())
    return stated if stated.is_absolute() else project_root / stated

read_received_responses(layout)

Read every pending receipt of one project, in arrival order with the receipt id as the tiebreaker.

A receipt id is a random hex string, so a directory listing orders on nothing a submission means. received_at is what a drain has to act on: DHIS2 replaces an aggregate value in place, so the instance ends up holding whichever submission was posted last, and posting the older one last would leave it holding the older number. Two receipts stamped the same instant order on their ids, which is arbitrary but the same on every run, so a drain stays reproducible.

An absent spool directory is not an error - it is a project nothing has been captured into yet - and answers with no receipts at all. A file that will not read as a receipt is moved to malformed/ with its reason beside it and named in the reading: a drain that aborted on it would lose every other receipt's turn to one file, and one that skipped it silently would make a submission disappear.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def read_received_responses(layout: SpoolLayout) -> SpoolReading:
    """Read every pending receipt of one project, in arrival order with the receipt id as the tiebreaker.

    A receipt id is a random hex string, so a directory listing orders on nothing a submission means.
    `received_at` is what a drain has to act on: DHIS2 replaces an aggregate value in place, so the
    instance ends up holding whichever submission was posted last, and posting the older one last
    would leave it holding the older number. Two receipts stamped the same instant order on their
    ids, which is arbitrary but the same on every run, so a drain stays reproducible.

    An absent spool directory is not an error - it is a project nothing has been captured into yet -
    and answers with no receipts at all. A file that will not read as a receipt is moved to
    `malformed/` with its reason beside it and named in the reading: a drain that aborted on it
    would lose every other receipt's turn to one file, and one that skipped it silently would make a
    submission disappear.
    """
    directory = layout.directory_for(SpoolState.RECEIVED)
    if not directory.is_dir():
        return SpoolReading()
    responses: list[SpooledResponse] = []
    quarantined: list[QuarantinedFile] = []
    for path in _receipt_paths(directory):
        try:
            responses.append(_read_receipt(path, layout))
        except _MalformedReceiptError as error:
            quarantined.append(_quarantine(path, layout, str(error)))
    responses.sort(key=lambda response: (response.received_at, response.response_id))
    return SpoolReading(responses=tuple(responses), quarantined=tuple(quarantined))

read_receipt(layout, state, response_id)

Read one named receipt out of one state, resource and all, refusing by name when it is not there.

What d2w fhir withdraw reads. A drain reads a whole directory because it acts on the queue; a withdrawal acts on one receipt an operator named, so it opens that file and nothing else - and a file that is not there, or will not read as a receipt, is a refusal rather than a quarantine, because the operator is standing in front of the answer.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def read_receipt(layout: SpoolLayout, state: SpoolState, response_id: str) -> SpooledResponse:
    """Read one named receipt out of one state, resource and all, refusing by name when it is not there.

    What `d2w fhir withdraw` reads. A drain reads a whole directory because it acts on the queue; a
    withdrawal acts on one receipt an operator named, so it opens that file and nothing else - and a
    file that is not there, or will not read as a receipt, is a refusal rather than a quarantine,
    because the operator is standing in front of the answer.
    """
    path = layout.directory_for(state) / f"{response_id}.json"
    if not path.is_file():
        raise SpoolReadError(f"`{response_id}` is not a {state.value} receipt of {layout.project_root}")
    try:
        return _read_receipt(path, layout)
    except _MalformedReceiptError as error:
        raise SpoolReadError(f"{path}: {error}") from error

read_spooled_receipts(layout)

Read the envelope of every receipt in every state, quarantining whatever will not read as one.

What d2w fhir spool lists. It touches no DHIS2 and parses no payload: a receipt is named by its envelope, which is the whole of what a queue listing states.

Each state is listed in arrival order with the receipt id as the tiebreaker, which is the order read_received_responses drains in - so what a listing shows as next in the queue is what the next drain posts first.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def read_spooled_receipts(layout: SpoolLayout) -> SpoolContents:
    """Read the envelope of every receipt in every state, quarantining whatever will not read as one.

    What `d2w fhir spool` lists. It touches no DHIS2 and parses no payload: a receipt is named by its
    envelope, which is the whole of what a queue listing states.

    Each state is listed in arrival order with the receipt id as the tiebreaker, which is the order
    `read_received_responses` drains in - so what a listing shows as next in the queue is what the
    next drain posts first.
    """
    receipts: list[SpooledReceipt] = []
    quarantined: list[QuarantinedFile] = []
    for state in SpoolState:
        directory = layout.directory_for(state)
        if not directory.is_dir():
            continue
        for path in _receipt_paths(directory):
            try:
                receipt = _read_envelope(path, state)
            except _MalformedReceiptError as error:
                quarantined.append(_quarantine(path, layout, str(error)))
                continue
            if state is SpoolState.RECEIVED:
                receipt = receipt.model_copy(update={"refusal": read_refusal_record(directory, receipt.response_id)})
            receipts.append(receipt)
    receipts.sort(key=lambda receipt: (receipt.received_at, receipt.response_id))
    return SpoolContents(receipts=tuple(receipts), quarantined=malformed_files(layout))

malformed_files(layout)

Every file sitting in malformed/, each with the reason written beside it when it was moved.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def malformed_files(layout: SpoolLayout) -> tuple[QuarantinedFile, ...]:
    """Every file sitting in `malformed/`, each with the reason written beside it when it was moved."""
    directory = layout.malformed_directory
    if not directory.is_dir():
        return ()
    return tuple(
        _quarantine_record(path)
        for path in sorted(_scan(directory))
        if not path.name.endswith(QUARANTINE_REASON_SUFFIX)
    )

record_refusal(spooled, record)

Write one still-queued receipt's refusal record beside it, durably, and answer with where it sits.

The receipt itself is never rewritten; the record is a sibling file the next listing reads and the move that finally drains the receipt deletes. Written atomically and fsynced the way a receipt is, because a marker that vanishes with the power is a drain that never happened.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def record_refusal(spooled: SpooledResponse, record: ForwardRefusalRecord) -> Path:
    """Write one still-queued receipt's refusal record beside it, durably, and answer with where it sits.

    The receipt itself is never rewritten; the record is a sibling file the next listing reads and
    the move that finally drains the receipt deletes. Written atomically and fsynced the way a
    receipt is, because a marker that vanishes with the power is a drain that never happened.
    """
    destination = spooled.path.with_name(f"{spooled.response_id}{REFUSAL_RECORD_SUFFIX}")
    _write_atomically(destination, record.model_dump_json(indent=2, exclude_none=True) + "\n")
    return destination

read_refusal_record(directory, response_id)

The refusal record beside one queued receipt, or None when no committing drain has refused it.

A record that will not parse answers None rather than raising: it is the marker that got corrupted, not the receipt that got lost, so a listing still names the receipt and simply says nothing about the drain that refused it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def read_refusal_record(directory: Path, response_id: str) -> ForwardRefusalRecord | None:
    """The refusal record beside one queued receipt, or None when no committing drain has refused it.

    A record that will not parse answers None rather than raising: it is the marker that got
    corrupted, not the receipt that got lost, so a listing still names the receipt and simply says
    nothing about the drain that refused it.
    """
    path = directory / f"{response_id}{REFUSAL_RECORD_SUFFIX}"
    if not path.is_file():
        return None
    try:
        return ForwardRefusalRecord.model_validate_json(path.read_text(encoding="utf-8"))
    except (OSError, ValidationError, ValueError):
        return None

move_to_forwarded(spooled, report)

Write one acceptance's import report beside where the receipt is going, then move the receipt there.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def move_to_forwarded(spooled: SpooledResponse, report: BaseModel) -> Path:
    """Write one acceptance's import report beside where the receipt is going, then move the receipt there."""
    return _file_beside_report(spooled, report, SpoolState.FORWARDED)

write_import_report(layout, state, response_id, report)

Write the import report beside one already-filed receipt, replacing whatever is there.

What a drain uses to say something further about a receipt it has already moved - the answer to a completeness registration posted after the values landed. The receipt itself is never rewritten, and the write is atomic, so a reader either sees the previous report or this one.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def write_import_report(layout: SpoolLayout, state: SpoolState, response_id: str, report: BaseModel) -> Path:
    """Write the import report beside one already-filed receipt, replacing whatever is there.

    What a drain uses to say something further about a receipt it has already moved - the answer to
    a completeness registration posted after the values landed. The receipt itself is never
    rewritten, and the write is atomic, so a reader either sees the previous report or this one.
    """
    directory = layout.directory_for(state)
    directory.mkdir(parents=True, exist_ok=True)
    destination = directory / f"{response_id}{IMPORT_REPORT_SUFFIX}"
    _write_atomically(destination, report.model_dump_json(indent=2, exclude_none=True) + "\n")
    return destination

read_import_reports(layout, state)

Every import report of one state, as the receipt id it belongs to and the JSON text it holds.

The text rather than a model: the shape a report carries is the service's business, and this module writes the file without reading what is in it. A file that will not read at all is left out rather than raised, exactly as an unreadable sidecar is elsewhere - one file must not cost a drain its answer about the rest.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def read_import_reports(layout: SpoolLayout, state: SpoolState) -> tuple[tuple[str, str], ...]:
    """Every import report of one state, as the receipt id it belongs to and the JSON text it holds.

    The text rather than a model: the shape a report carries is the service's business, and this
    module writes the file without reading what is in it. A file that will not read at all is left
    out rather than raised, exactly as an unreadable sidecar is elsewhere - one file must not cost a
    drain its answer about the rest.
    """
    directory = layout.directory_for(state)
    if not directory.is_dir():
        return ()
    reports: list[tuple[str, str]] = []
    for path in sorted(_scan(directory)):
        if not path.name.endswith(IMPORT_REPORT_SUFFIX):
            continue
        try:
            reports.append((path.name.removesuffix(IMPORT_REPORT_SUFFIX), path.read_text(encoding="utf-8")))
        except OSError:
            continue
    return tuple(reports)

move_to_rejected(spooled, report)

Write one rejection's import report beside where the receipt is going, then move the receipt there.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def move_to_rejected(spooled: SpooledResponse, report: BaseModel) -> Path:
    """Write one rejection's import report beside where the receipt is going, then move the receipt there."""
    return _file_beside_report(spooled, report, SpoolState.REJECTED)

move_to_withdrawn(spooled, report)

Write what DHIS2 answered the delete beside withdrawn/, then move the forwarded receipt in after it.

THE FORWARD'S OWN IMPORT REPORT STAYS IN forwarded/. It states what DHIS2 did with the payload when it took it, which is still true of that import and is the only record of what was landed; the document written here states what DHIS2 did when it was asked to take it back. Two answers to two questions, and neither is rewritten - the receipt itself never was.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def move_to_withdrawn(spooled: SpooledResponse, report: BaseModel) -> Path:
    """Write what DHIS2 answered the delete beside `withdrawn/`, then move the forwarded receipt in after it.

    THE FORWARD'S OWN IMPORT REPORT STAYS IN `forwarded/`. It states what DHIS2 did with the payload
    when it took it, which is still true of that import and is the only record of what was landed;
    the document written here states what DHIS2 did when it was asked to take it back. Two answers
    to two questions, and neither is rewritten - the receipt itself never was.
    """
    return _file_beside_report(spooled, report, SpoolState.WITHDRAWN)

move_to_received(layout, response_id)

Move one rejected receipt back into the queue, and answer with where it now sits.

THE SIDECAR STAYS IN rejected/. The report states what DHIS2 answered the last time this payload was posted, which is still true of that post and is the only record of what the receipt was requeued from; carrying it into received/ would put a drained receipt's answer beside a receipt nothing has yet asked about. The next drain writes a fresh report wherever the receipt lands - overwriting the stale one in place when the answer is the same, and leaving it behind as history when the receipt is accepted this time.

ANY REFUSAL RECORD IN received/ GOES. This is the one way a receipt enters the queue from another state, and a marker under its name there can only be a leftover - a drain killed between the rename that filed the receipt and the unlink that cleared its marker. The receipt is entering the queue with no drain having refused it, so nothing beside it may say one did.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def move_to_received(layout: SpoolLayout, response_id: str) -> Path:
    """Move one rejected receipt back into the queue, and answer with where it now sits.

    THE SIDECAR STAYS IN `rejected/`. The report states what DHIS2 answered the last time this
    payload was posted, which is still true of that post and is the only record of what the receipt
    was requeued from; carrying it into `received/` would put a drained receipt's answer beside a
    receipt nothing has yet asked about. The next drain writes a fresh report wherever the receipt
    lands - overwriting the stale one in place when the answer is the same, and leaving it behind as
    history when the receipt is accepted this time.

    ANY REFUSAL RECORD IN `received/` GOES. This is the one way a receipt enters the queue from
    another state, and a marker under its name there can only be a leftover - a drain killed between
    the rename that filed the receipt and the unlink that cleared its marker. The receipt is entering
    the queue with no drain having refused it, so nothing beside it may say one did.
    """
    source = layout.directory_for(SpoolState.REJECTED) / f"{response_id}.json"
    if not source.is_file():
        raise SpoolReadError(f"`{response_id}` is not a rejected receipt of {layout.project_root}")
    directory = layout.directory_for(SpoolState.RECEIVED)
    directory.mkdir(parents=True, exist_ok=True)
    destination = directory / source.name
    os.replace(source, destination)
    marker = directory / f"{response_id}{REFUSAL_RECORD_SUFFIX}"
    marker.unlink(missing_ok=True)
    _fsync_directory(directory)
    _fsync_directory(source.parent)
    return destination

drain_lock(layout)

Hold the spool's exclusive drain lock for one run, refusing at once when another run holds it.

Two drains over one spool would translate the same receipts, post both copies, and race each other's renames - so the second one fails rather than waits: an operator who started it by mistake wants to be told, and one who started it deliberately wants the first run's answer rather than a queue behind it. The lockfile carries the holding process id so the refusal names what to look for, and the lock is an flock on an open descriptor, so the kernel releases it whether the drain returned, raised, or was killed.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
@contextmanager
def drain_lock(layout: SpoolLayout) -> Generator[Path]:
    """Hold the spool's exclusive drain lock for one run, refusing at once when another run holds it.

    Two drains over one spool would translate the same receipts, post both copies, and race each
    other's renames - so the second one fails rather than waits: an operator who started it by
    mistake wants to be told, and one who started it deliberately wants the first run's answer
    rather than a queue behind it. The lockfile carries the holding process id so the refusal names
    what to look for, and the lock is an `flock` on an open descriptor, so the kernel releases it
    whether the drain returned, raised, or was killed.
    """
    layout.root.mkdir(parents=True, exist_ok=True)
    path = layout.lock_path
    descriptor = os.open(path, os.O_RDWR | os.O_CREAT, 0o644)
    try:
        try:
            fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except OSError as error:
            raise SpoolLockedError(_locked_message(path)) from error
        os.ftruncate(descriptor, 0)
        os.write(descriptor, f"{os.getpid()}\n".encode())
        os.fsync(descriptor)
        yield path
    finally:
        os.close(descriptor)

sweep_orphan_temporary_files(layout, *, older_than_seconds=ORPHAN_TEMPORARY_FILE_AGE_SECONDS)

Delete the temporary files an interrupted write left behind, and answer with what was deleted.

THE AGE GUARD IS THE WHOLE OF THE SAFETY. A temporary file being written this instant is indistinguishable from one a killed process abandoned - same name shape, same directory - and the only thing the directory can say about the difference is how long ago the file was touched. So a young one is left alone even though it is probably nothing, because deleting the file a running capture is mid-write into would turn a durable write into a lost one.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
def sweep_orphan_temporary_files(
    layout: SpoolLayout, *, older_than_seconds: float = ORPHAN_TEMPORARY_FILE_AGE_SECONDS
) -> tuple[str, ...]:
    """Delete the temporary files an interrupted write left behind, and answer with what was deleted.

    THE AGE GUARD IS THE WHOLE OF THE SAFETY. A temporary file being written this instant is
    indistinguishable from one a killed process abandoned - same name shape, same directory - and the
    only thing the directory can say about the difference is how long ago the file was touched. So a
    young one is left alone even though it is probably nothing, because deleting the file a running
    capture is mid-write into would turn a durable write into a lost one.
    """
    swept: list[str] = []
    cutoff = time.time() - older_than_seconds
    directories = [
        layout.root,
        *(layout.directory_for(state) for state in SpoolState),
        layout.malformed_directory,
    ]
    for directory in directories:
        if not directory.is_dir():
            continue
        for path in sorted(_scan(directory)):
            if not path.name.endswith(TEMPORARY_FILE_SUFFIX):
                continue
            try:
                if path.stat().st_mtime > cutoff:
                    continue
                path.unlink()
            except OSError:
                continue
            swept.append(path.name)
    return tuple(swept)

Values a previous submission already sent

The identity of an aggregate data value, and the index a drain reads out of forwarded/ to say that a value it is about to send is one an earlier submission already sent. DHIS2 counts that write exactly as it counts a first entry, so the spool is where the answer lives.

overwrite

The aggregate values a drain sends that an earlier submission already sent, and the index that finds them.

DHIS2 cannot answer this question, so the spool does. /api/dataValueSets applies its own CREATE_AND_UPDATE to an envelope that names no strategy, so a second send of one cell replaces the first in place; and the import summary counts updated: 1 for a genuinely first write and for a replacement alike (BUGS.md 85), so nothing DHIS2 answers separates the two. What separates them here is the record every drained receipt leaves: a forwarded receipt's <id>.report.json names the cells its payload put in the instance, and the next drain reads those records back before it sends anything.

The cell is the full DHIS2 identity of a data value - data element, category option combo, period, organisation unit, and attribute option combo - because that tuple is what DHIS2 keys a value on, and anything shorter would call two different cells the same one.

THE SIDECAR IS THE RECORD, NOT THE RECEIPT. A forwarded receipt holds the FHIR submission, which means the cells it landed on are only knowable by translating it again - against today's guide, which may no longer be the guide it was sent under. The sidecar instead states what was actually posted and taken, which is the claim this index needs, and it is one file to read rather than a translation to re-run. It records the identity of each value and never the value itself: what the payload landed on is this index's business, and what it landed is the receipt's.

THE COST. The index is built once per drain and only when that drain carries an aggregate payload, so a tracker-only run reads nothing at all. Building it is one read per <id>.report.json in forwarded/ - the sidecar alone, never the receipt beside it, so a forwarded receipt costs one file open rather than two - and a sidecar is parsed straight into the cells it recorded. forwarded/ grows without bound, which is exactly why the read is the sidecar alone and why a whole run pays it once rather than once per receipt: a spool of a few hundred forwarded receipts is read in a small fraction of the time a single POST to DHIS2 takes, so the index disappears into the network cost of the drain it serves. Nothing here truncates, and nothing here samples. A spool large enough for the read to be felt is one where forwarded/ wants archiving for its own sake, and an index that quietly stopped reading part of it would answer "no earlier submission" about a value that has one - which is the one answer this module must never give.

Classes

AggregateCell

Bases: BaseModel

The full DHIS2 identity of one aggregate data value - the five keys that name exactly one cell.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class AggregateCell(BaseModel):
    """The full DHIS2 identity of one aggregate data value - the five keys that name exactly one cell."""

    model_config = ConfigDict(frozen=True)

    data_element: str
    category_option_combo: str | None = None
    """Unset where the data element rides the default category combo, which is what DHIS2 files it under."""

    period: str | None = None
    organisation_unit: str | None = None
    attribute_option_combo: str | None = None
    """Unset where the data set rides the default category combo, which is what DHIS2 files it under."""

    @property
    def key(self) -> str:
        """The five keys as the one string an index is keyed on, an unset key included as its absence."""
        return _CELL_KEY_SEPARATOR.join(
            part or ""
            for part in (
                self.data_element,
                self.category_option_combo,
                self.period,
                self.organisation_unit,
                self.attribute_option_combo,
            )
        )

    @property
    def line(self) -> str:
        """The five keys as the one cell a report shows, since a data value has no other name."""
        parts = [
            self.data_element,
            self.category_option_combo,
            self.period,
            self.organisation_unit,
            self.attribute_option_combo,
        ]
        return " / ".join(part for part in parts if part)
Attributes
category_option_combo = None class-attribute instance-attribute

Unset where the data element rides the default category combo, which is what DHIS2 files it under.

attribute_option_combo = None class-attribute instance-attribute

Unset where the data set rides the default category combo, which is what DHIS2 files it under.

key property

The five keys as the one string an index is keyed on, an unset key included as its absence.

line property

The five keys as the one cell a report shows, since a data value has no other name.

ForwardedSubmission

Bases: BaseModel

Which forwarded receipt sent a value, and when that receipt was received.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class ForwardedSubmission(BaseModel):
    """Which forwarded receipt sent a value, and when that receipt was received."""

    model_config = ConfigDict(frozen=True)

    response_id: str
    received_at: str = ""
    """Empty where the receipt's envelope recorded no arrival time, which a captured receipt always does."""
Attributes
received_at = '' class-attribute instance-attribute

Empty where the receipt's envelope recorded no arrival time, which a captured receipt always does.

OverwrittenValue

Bases: BaseModel

One value a drain sends that a forwarded receipt already sent, and which receipt sent it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class OverwrittenValue(BaseModel):
    """One value a drain sends that a forwarded receipt already sent, and which receipt sent it."""

    model_config = ConfigDict(frozen=True)

    cell: AggregateCell
    previous_response_id: str
    """The receipt that last sent this value, which is the submission whose number the instance holds."""

    previous_received_at: str = ""

    @property
    def line(self) -> str:
        """The value and its earlier submission as the one line a report file and a terminal cell both want."""
        received = f", received {self.previous_received_at}" if self.previous_received_at else ""
        return f"{self.cell.line} (sent by {self.previous_response_id}{received})"
Attributes
previous_response_id instance-attribute

The receipt that last sent this value, which is the submission whose number the instance holds.

line property

The value and its earlier submission as the one line a report file and a terminal cell both want.

ForwardOverwrite

Bases: BaseModel

Every value one response of a drain sends that an earlier submission had already sent.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class ForwardOverwrite(BaseModel):
    """Every value one response of a drain sends that an earlier submission had already sent."""

    model_config = ConfigDict(frozen=True)

    response_id: str
    values: tuple[OverwrittenValue, ...] = ()

ForwardedValueRecord

Bases: BaseModel

The part of a forwarded receipt's sidecar this index reads: the cells it landed on, and when it arrived.

A projection of the sidecar rather than the whole of it, because the index has no use for DHIS2's counts, and reading them would tie this module to the report shape the service writes.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class ForwardedValueRecord(BaseModel):
    """The part of a forwarded receipt's sidecar this index reads: the cells it landed on, and when it arrived.

    A projection of the sidecar rather than the whole of it, because the index has no use for DHIS2's
    counts, and reading them would tie this module to the report shape the service writes.
    """

    model_config = ConfigDict(extra="ignore")

    received_at: str = ""
    cells: tuple[AggregateCell, ...] = ()
    target_kind: str | None = None

ForwardedCellIndex

Bases: BaseModel

Which forwarded receipt last sent each aggregate value a spool has landed in DHIS2.

Mutable for the length of one drain: a receipt filed to forwarded/ mid-run has landed its values, so a later receipt of the same run that sends them again replaces them just as surely as one from last week does. The run records each payload as DHIS2 takes it, and the answer stays true inside the drain as well as across drains.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
class ForwardedCellIndex(BaseModel):
    """Which forwarded receipt last sent each aggregate value a spool has landed in DHIS2.

    Mutable for the length of one drain: a receipt filed to `forwarded/` mid-run has landed its
    values, so a later receipt of the same run that sends them again replaces them just as surely as
    one from last week does. The run records each payload as DHIS2 takes it, and the answer stays
    true inside the drain as well as across drains.
    """

    covered: dict[str, ForwardedSubmission] = Field(default_factory=dict)
    """The submission that last sent each cell, keyed by `AggregateCell.key`."""

    receipts_read: int = 0
    """How many forwarded sidecars this index read, which is what its cost is counted in."""

    receipts_without_values: int = 0
    """Aggregate receipts in `forwarded/` whose sidecar records no cells, so this index cannot see them."""

    def already_sent(self, cells: Sequence[AggregateCell]) -> tuple[OverwrittenValue, ...]:
        """Every cell of one payload a forwarded receipt already sent, in the order the payload names them."""
        found: list[OverwrittenValue] = []
        for cell in cells:
            submission = self.covered.get(cell.key)
            if submission is not None:
                found.append(
                    OverwrittenValue(
                        cell=cell,
                        previous_response_id=submission.response_id,
                        previous_received_at=submission.received_at,
                    )
                )
        return tuple(found)

    def record(self, cells: Sequence[AggregateCell], submission: ForwardedSubmission) -> None:
        """Record one submission as the sender of these cells, keeping the latest sender of each.

        The receipt named for a cell is the last one to have sent it, because that is the submission
        whose number the instance is holding. Arrival time decides, since `forwarded/` is read in
        file-name order and a receipt id is a random hex string that orders on nothing.
        """
        for cell in cells:
            held = self.covered.get(cell.key)
            if held is None or submission.received_at >= held.received_at:
                self.covered[cell.key] = submission
Attributes
covered = Field(default_factory=dict) class-attribute instance-attribute

The submission that last sent each cell, keyed by AggregateCell.key.

receipts_read = 0 class-attribute instance-attribute

How many forwarded sidecars this index read, which is what its cost is counted in.

receipts_without_values = 0 class-attribute instance-attribute

Aggregate receipts in forwarded/ whose sidecar records no cells, so this index cannot see them.

Methods:
already_sent(cells)

Every cell of one payload a forwarded receipt already sent, in the order the payload names them.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
def already_sent(self, cells: Sequence[AggregateCell]) -> tuple[OverwrittenValue, ...]:
    """Every cell of one payload a forwarded receipt already sent, in the order the payload names them."""
    found: list[OverwrittenValue] = []
    for cell in cells:
        submission = self.covered.get(cell.key)
        if submission is not None:
            found.append(
                OverwrittenValue(
                    cell=cell,
                    previous_response_id=submission.response_id,
                    previous_received_at=submission.received_at,
                )
            )
    return tuple(found)
record(cells, submission)

Record one submission as the sender of these cells, keeping the latest sender of each.

The receipt named for a cell is the last one to have sent it, because that is the submission whose number the instance is holding. Arrival time decides, since forwarded/ is read in file-name order and a receipt id is a random hex string that orders on nothing.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
def record(self, cells: Sequence[AggregateCell], submission: ForwardedSubmission) -> None:
    """Record one submission as the sender of these cells, keeping the latest sender of each.

    The receipt named for a cell is the last one to have sent it, because that is the submission
    whose number the instance is holding. Arrival time decides, since `forwarded/` is read in
    file-name order and a receipt id is a random hex string that orders on nothing.
    """
    for cell in cells:
        held = self.covered.get(cell.key)
        if held is None or submission.received_at >= held.received_at:
            self.covered[cell.key] = submission

Functions:

aggregate_cells(data_value_set)

The full identity of every data value one /api/dataValueSets envelope carries.

A data value may name its own period, organisation unit, and attribute option combo, and DHIS2 reads the envelope's as the default for the ones it does not - so a cell is read the same way round here, the value's own key first and the envelope's behind it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
def aggregate_cells(data_value_set: DataValueSet) -> tuple[AggregateCell, ...]:
    """The full identity of every data value one `/api/dataValueSets` envelope carries.

    A data value may name its own period, organisation unit, and attribute option combo, and DHIS2
    reads the envelope's as the default for the ones it does not - so a cell is read the same way
    round here, the value's own key first and the envelope's behind it.
    """
    return tuple(
        AggregateCell(
            data_element=value.dataElement or "",
            category_option_combo=value.categoryOptionCombo,
            period=value.period or data_value_set.period,
            organisation_unit=value.orgUnit or data_value_set.orgUnit,
            attribute_option_combo=value.attributeOptionCombo or data_value_set.attributeOptionCombo,
        )
        for value in data_value_set.dataValues or []
    )

build_forwarded_cell_index(layout)

Read every forwarded receipt's sidecar into the index of what this spool has already landed.

Only forwarded/ counts. A rejected receipt never landed its values, so it covers nothing, and a receipt still in the queue has not been sent at all.

A sidecar that will not read is counted rather than raised: one unreadable file must not cost a drain its answer about the other two hundred, and the count is what keeps that silence visible.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
def build_forwarded_cell_index(layout: SpoolLayout) -> ForwardedCellIndex:
    """Read every forwarded receipt's sidecar into the index of what this spool has already landed.

    Only `forwarded/` counts. A rejected receipt never landed its values, so it covers nothing, and a
    receipt still in the queue has not been sent at all.

    A sidecar that will not read is counted rather than raised: one unreadable file must not cost a
    drain its answer about the other two hundred, and the count is what keeps that silence visible.
    """
    directory = layout.directory_for(SpoolState.FORWARDED)
    if not directory.is_dir():
        return ForwardedCellIndex()
    index = ForwardedCellIndex()
    for path in sorted(_sidecar_paths(directory)):
        index.receipts_read += 1
        record = _read_value_record(path)
        if record is None or not record.cells:
            if record is None or record.target_kind == _AGGREGATE_TARGET_KIND:
                index.receipts_without_values += 1
            continue
        submission = ForwardedSubmission(
            response_id=path.name.removesuffix(IMPORT_REPORT_SUFFIX), received_at=record.received_at
        )
        index.record(record.cells, submission)
    return index

A client for a running facade

FacadeClient is the typed path to a d2w fhir serve facade: construct it against a base url, hand submit_response a filled form, and read the receipt id off what comes back. The facade publishes no OpenAPI document - /metadata is its contract - so this client is hand-written against the routes it mounts rather than generated from a schema.

Three shapes are worth knowing before the first call. A create answers an OperationOutcome and not the resource, so the id of an accepted submission is the last segment of the Location header - CaptureReceipt carries both, plus the warnings the answer stated. A 201 means the facade stored the submission durably, not that anything reached DHIS2; d2w fhir forward is what drains the queue. And an expression that will not parse is a 200 carrying its line and column, so evaluate answers an EvaluationOutcome rather than raising - FacadeError is reserved for a request the facade cannot serve at all.

from dhis2w_fhir import FacadeClient

async with FacadeClient("http://127.0.0.1:8123") as facade:
    draft = await facade.generate("BfMAe6Itzgt", seed=20260)
    receipt = await facade.submit_response(draft)
    print(receipt.response_id, receipt.note)
    stored = await facade.read_response(receipt.response_id)

Five runnable examples cover the surface, one method group each. send_with_the_client.py is the write half above, end to end. search_with_the_client.py is the read half - canonical_resource_types read off /metadata, a typed ResourceQuery against the published forms and against the live register, and resolve turning a canonical url into whichever type holds it. evaluate_with_the_client.py runs FHIRPath over both evaluation contexts and shows the diagnostic an unparseable expression is answered with. authenticate_with_the_client.py presents a BearerToken to a facade started with --auth token, beside the 401 a caller with no credential and a caller with the wrong one get. And handle_refusals_with_the_client.py reads FacadeError itself: status_code, one typed issue per thing wrong, and diagnostics for the log line that has to say why in one.

facade

A typed async client for a d2w fhir serve facade: read it, fill a form, submit a capture, evaluate an expression.

This client is hand-written against the routes dhis2w_fhir_serve.routes mounts. The base URL is a FHIR endpoint whose contract is the CapabilityStatement at /metadata and which publishes no OpenAPI document at all, so a generated client was never on offer for the surface most of these methods read. The facade's own API under /facade does publish one, at /facade/openapi.json - it is the contract to read when writing against those endpoints directly, and evaluate below is this client's one method that calls them. Every method here answers a model rather than a parsed document, and every refusal arrives as a FacadeError carrying the OperationOutcome the facade stated its reason in.

WHAT THIS IS FOR. An integrator holding a filled form wants three lines, not a request builder: construct a FacadeClient, hand submit_response the document, read the receipt id off what comes back. generate and read_response close that loop, so a caller can prove a form is answerable before writing a line of form-filling code - $generate output is postable at the same server, unchanged, which is the invariant that operation exists for.

THE ID IS NOT IN THE BODY. R4 says a create answers an OperationOutcome, so the identity of an accepted submission is the last segment of the Location header and the body is the server saying what it did. CaptureReceipt carries both, which is why submit_response answers a receipt rather than the OperationOutcome on its own: a caller reading only the body would find no id on any capture the facade ever accepts.

A RECEIPT IS NOT A DHIS2 WRITE. A 201 means the facade understood the submission, checked it against the published form, and wrote it to disk durably. Nothing has reached DHIS2 - d2w fhir forward is what drains the queue into an instance, and until it runs the receipt is a promise about bytes the facade is holding.

THE EVALUATION SHAPES ARE MIRRORED, NOT IMPORTED. POST /facade/evaluate is the facade's own endpoint and its request and response models are defined in dhis2w_fhir_serve.evaluation and dhis2w_fhir_serve.routes.evaluate, which this package cannot import: dhis2w-fhir-serve depends on dhis2w-fhir, so the arrow points one way only. EvaluationRequest, EvaluationOutcome, and the three context shapes below mirror those definitions field for field, and those two modules are the source of truth for them.

Attributes

EvaluationContext = Annotated[StoredResourceContext | InlineResourceContext | RegisteredEntityContext, Field(discriminator='kind')] module-attribute

What an evaluation runs over, discriminated on kind exactly as the facade discriminates it.

Classes

FacadeCredential

Bases: BaseModel

What a caller presents to a guarded facade, as the one Authorization value it becomes.

The facade runs under one of four postures - none, token, dhis2, jwt - and three of them read a credential. A subclass exists per scheme rather than per posture, because token and jwt both take a bearer token and differ only in who minted it, which is the server's business and not the caller's.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class FacadeCredential(BaseModel):
    """What a caller presents to a guarded facade, as the one `Authorization` value it becomes.

    The facade runs under one of four postures - `none`, `token`, `dhis2`, `jwt` - and three of them
    read a credential. A subclass exists per scheme rather than per posture, because `token` and
    `jwt` both take a bearer token and differ only in who minted it, which is the server's business
    and not the caller's.
    """

    model_config = ConfigDict(frozen=True)

    def authorization(self) -> str:
        """The `Authorization` header value this credential becomes."""
        raise NotImplementedError
Methods:
authorization()

The Authorization header value this credential becomes.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def authorization(self) -> str:
    """The `Authorization` header value this credential becomes."""
    raise NotImplementedError

BearerToken

Bases: FacadeCredential

A token the token posture holds in D2W_FHIR_SERVE_TOKENS, or one the jwt posture's issuer minted.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class BearerToken(FacadeCredential):
    """A token the `token` posture holds in `D2W_FHIR_SERVE_TOKENS`, or one the `jwt` posture's issuer minted."""

    token: str

    def authorization(self) -> str:
        """`Bearer <token>`."""
        return f"{BEARER_SCHEME} {self.token}"
Methods:
authorization()

Bearer <token>.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def authorization(self) -> str:
    """`Bearer <token>`."""
    return f"{BEARER_SCHEME} {self.token}"

UsernamePassword

Bases: FacadeCredential

DHIS2 credentials the dhis2 posture replays against GET /api/me on the instance it reads.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class UsernamePassword(FacadeCredential):
    """DHIS2 credentials the `dhis2` posture replays against `GET /api/me` on the instance it reads."""

    username: str
    password: str

    def authorization(self) -> str:
        """`Basic <base64 of username:password>`."""
        encoded = base64.b64encode(f"{self.username}:{self.password}".encode()).decode("ascii")
        return f"{BASIC_SCHEME} {encoded}"
Methods:
authorization()

Basic <base64 of username:password>.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def authorization(self) -> str:
    """`Basic <base64 of username:password>`."""
    encoded = base64.b64encode(f"{self.username}:{self.password}".encode()).decode("ascii")
    return f"{BASIC_SCHEME} {encoded}"

PersonalAccessToken

Bases: FacadeCredential

A DHIS2 personal access token, which the dhis2 posture replays exactly as dhis2w-client sends it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class PersonalAccessToken(FacadeCredential):
    """A DHIS2 personal access token, which the `dhis2` posture replays exactly as `dhis2w-client` sends it."""

    token: str

    def authorization(self) -> str:
        """`ApiToken <token>`."""
        return f"{DHIS2_PERSONAL_ACCESS_TOKEN_SCHEME} {self.token}"
Methods:
authorization()

ApiToken <token>.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def authorization(self) -> str:
    """`ApiToken <token>`."""
    return f"{DHIS2_PERSONAL_ACCESS_TOKEN_SCHEME} {self.token}"

FacadeError

Bases: Exception

Raised when the facade refuses a request, carrying the OperationOutcome it refused with.

Every refusal this facade makes renders as an OperationOutcome - register_error_handlers wires that for the whole application, so a 404 on a resource and a 400 on a search parameter arrive in the same shape. A capture refused at 400 or 422 carries one issue per thing wrong with the submission, which is why issues is a tuple and not one value.

A connection that never reached the facade is not this: httpx2's own TransportError passes through untouched, because a server that did not answer stated no outcome to carry.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class FacadeError(Exception):
    """Raised when the facade refuses a request, carrying the `OperationOutcome` it refused with.

    Every refusal this facade makes renders as an `OperationOutcome` - `register_error_handlers`
    wires that for the whole application, so a 404 on a resource and a 400 on a search parameter
    arrive in the same shape. A capture refused at 400 or 422 carries one issue per thing wrong with
    the submission, which is why `issues` is a tuple and not one value.

    A connection that never reached the facade is not this: httpx2's own `TransportError` passes
    through untouched, because a server that did not answer stated no outcome to carry.
    """

    def __init__(
        self,
        status_code: int,
        method: str,
        url: str,
        outcome: OperationOutcome | None,
        body_text: str,
    ) -> None:
        """Capture the status, the request that drew it, and the outcome the facade answered with."""
        self.status_code = status_code
        self.method = method
        self.url = url
        self.outcome = outcome
        self.body_text = body_text
        stated = self.diagnostics or body_text.strip() or "no body"
        super().__init__(f"{method} {url} was refused with {status_code}: {stated}")

    @property
    def issues(self) -> tuple[OperationOutcomeIssue, ...]:
        """Every issue the refusal named, empty when the body carried no `OperationOutcome`."""
        if self.outcome is None or self.outcome.issue is None:
            return ()
        return tuple(self.outcome.issue)

    @property
    def diagnostics(self) -> str:
        """Every issue's own words, joined - what to print when one line has to say why."""
        return "; ".join(issue.diagnostics for issue in self.issues if issue.diagnostics)
Attributes
issues property

Every issue the refusal named, empty when the body carried no OperationOutcome.

diagnostics property

Every issue's own words, joined - what to print when one line has to say why.

Methods:
__init__(status_code, method, url, outcome, body_text)

Capture the status, the request that drew it, and the outcome the facade answered with.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def __init__(
    self,
    status_code: int,
    method: str,
    url: str,
    outcome: OperationOutcome | None,
    body_text: str,
) -> None:
    """Capture the status, the request that drew it, and the outcome the facade answered with."""
    self.status_code = status_code
    self.method = method
    self.url = url
    self.outcome = outcome
    self.body_text = body_text
    stated = self.diagnostics or body_text.strip() or "no body"
    super().__init__(f"{method} {url} was refused with {status_code}: {stated}")

CaptureReceipt

Bases: BaseModel

What an accepted submission answers: where the receipt lives, and what the facade noted about it.

response_id is the last segment of Location rather than anything in the body, because the body is an OperationOutcome and carries no id. note is the server's own "stored response {id}; a stored response is the submission as received" line, and warnings is every other issue the answer carried - a submission can be accepted and still have something worth saying about it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class CaptureReceipt(BaseModel):
    """What an accepted submission answers: where the receipt lives, and what the facade noted about it.

    `response_id` is the last segment of `Location` rather than anything in the body, because the
    body is an `OperationOutcome` and carries no id. `note` is the server's own "stored response
    {id}; a stored response is the submission as received" line, and `warnings` is every other issue
    the answer carried - a submission can be accepted and still have something worth saying about it.
    """

    model_config = ConfigDict(frozen=True)

    response_id: str
    """The id the receipt is served from, which `read_response` reads it back by."""

    location: str
    """The absolute url the facade said the receipt lives at, verbatim from the `Location` header."""

    note: str | None = None
    """The informational issue the facade always answers with, saying what it stored."""

    warnings: tuple[OperationOutcomeIssue, ...] = ()
    """Everything else the answer carried - accepted, and worth reading."""

    outcome: OperationOutcome
    """The whole answer, for a caller that wants the issues in the order they were stated."""
Attributes
response_id instance-attribute

The id the receipt is served from, which read_response reads it back by.

location instance-attribute

The absolute url the facade said the receipt lives at, verbatim from the Location header.

note = None class-attribute instance-attribute

The informational issue the facade always answers with, saying what it stored.

warnings = () class-attribute instance-attribute

Everything else the answer carried - accepted, and worth reading.

outcome instance-attribute

The whole answer, for a caller that wants the issues in the order they were stated.

ResourceQuery

Bases: BaseModel

The search parameters this facade answers, named rather than spelled.

The facade honours a small, fixed set and treats the rest two different ways: a store search IGNORES a parameter it does not know, and a register search REFUSES one. So a free-form mapping of query parameters is a quiet way to get the wrong answer on one route and a 400 on the other, and this model can only express something the facade actually reads.

Not every parameter answers on every type. _id, url, and identifier answer on the eleven conformance types; QuestionnaireResponse answers _id and questionnaire and is paged; the live register types answer identifier, _tag, d2-attribute, and _content.

Several values in one field are sent comma-separated, which is how the facade spells OR within one parameter. A value carrying a comma of its own cannot be expressed that way.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class ResourceQuery(BaseModel):
    """The search parameters this facade answers, named rather than spelled.

    The facade honours a small, fixed set and treats the rest two different ways: a store search
    IGNORES a parameter it does not know, and a register search REFUSES one. So a free-form mapping
    of query parameters is a quiet way to get the wrong answer on one route and a 400 on the other,
    and this model can only express something the facade actually reads.

    Not every parameter answers on every type. `_id`, `url`, and `identifier` answer on the eleven
    conformance types; `QuestionnaireResponse` answers `_id` and `questionnaire` and is paged;
    the live register types answer `identifier`, `_tag`, `d2-attribute`, and `_content`.

    Several values in one field are sent comma-separated, which is how the facade spells OR within
    one parameter. A value carrying a comma of its own cannot be expressed that way.
    """

    model_config = ConfigDict(frozen=True)

    ids: tuple[str, ...] = ()
    """`_id` - the resource ids to match."""

    urls: tuple[str, ...] = ()
    """`url` - the canonical urls to match, on the conformance types that declare it."""

    identifiers: tuple[str, ...] = ()
    """`identifier` - `system|value` tokens; a bare value matches any system."""

    questionnaire: str | None = None
    """`questionnaire` - the form canonical a receipt answers, matched exactly."""

    tags: tuple[str, ...] = ()
    """`_tag` - a register search's tag filter."""

    attribute_filters: tuple[str, ...] = ()
    """`d2-attribute` - a register search's DHIS2 tracked entity attribute filter."""

    text: str | None = None
    """`_content` - free-text search, answerable only where the projection backend is configured."""

    count: int | None = None
    """`_count` - a cap on the entries returned, not a page size; `Bundle.total` stays the whole set."""

    page: str | None = None
    """`page` - an opaque cursor the server minted; compose one only by following a Bundle's `next` link."""

    def to_query_parameters(self) -> tuple[tuple[str, str], ...]:
        """This query as the parameter pairs the facade reads, omitting everything left unset."""
        pairs: list[tuple[str, str]] = []
        for name, values in (
            ("_id", self.ids),
            (_CANONICAL_SEARCH_PARAMETER, self.urls),
            ("identifier", self.identifiers),
            ("_tag", self.tags),
            ("d2-attribute", self.attribute_filters),
        ):
            if values:
                pairs.append((name, ",".join(values)))
        for name, value in (("questionnaire", self.questionnaire), ("_content", self.text), ("page", self.page)):
            if value is not None:
                pairs.append((name, value))
        if self.count is not None:
            pairs.append(("_count", str(self.count)))
        return tuple(pairs)
Attributes
ids = () class-attribute instance-attribute

_id - the resource ids to match.

urls = () class-attribute instance-attribute

url - the canonical urls to match, on the conformance types that declare it.

identifiers = () class-attribute instance-attribute

identifier - system|value tokens; a bare value matches any system.

questionnaire = None class-attribute instance-attribute

questionnaire - the form canonical a receipt answers, matched exactly.

tags = () class-attribute instance-attribute

_tag - a register search's tag filter.

attribute_filters = () class-attribute instance-attribute

d2-attribute - a register search's DHIS2 tracked entity attribute filter.

text = None class-attribute instance-attribute

_content - free-text search, answerable only where the projection backend is configured.

count = None class-attribute instance-attribute

_count - a cap on the entries returned, not a page size; Bundle.total stays the whole set.

page = None class-attribute instance-attribute

page - an opaque cursor the server minted; compose one only by following a Bundle's next link.

Methods:
to_query_parameters()

This query as the parameter pairs the facade reads, omitting everything left unset.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def to_query_parameters(self) -> tuple[tuple[str, str], ...]:
    """This query as the parameter pairs the facade reads, omitting everything left unset."""
    pairs: list[tuple[str, str]] = []
    for name, values in (
        ("_id", self.ids),
        (_CANONICAL_SEARCH_PARAMETER, self.urls),
        ("identifier", self.identifiers),
        ("_tag", self.tags),
        ("d2-attribute", self.attribute_filters),
    ):
        if values:
            pairs.append((name, ",".join(values)))
    for name, value in (("questionnaire", self.questionnaire), ("_content", self.text), ("page", self.page)):
        if value is not None:
            pairs.append((name, value))
    if self.count is not None:
        pairs.append(("_count", str(self.count)))
    return tuple(pairs)

EvaluationLanguage

Bases: StrEnum

Which of the three languages a source is written in - mirrors dhis2w_fhir_serve.evaluation.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class EvaluationLanguage(StrEnum):
    """Which of the three languages a source is written in - mirrors `dhis2w_fhir_serve.evaluation`."""

    FHIRPATH = "fhirpath"
    CQL = "cql"
    ELM = "elm"

DiagnosticKind

Bases: StrEnum

Whether the source never parsed, or parsed and then refused to run.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class DiagnosticKind(StrEnum):
    """Whether the source never parsed, or parsed and then refused to run."""

    PARSE = "parse"
    EVALUATION = "evaluation"

StoredResourceContext

Bases: BaseModel

Evaluate over a resource the facade already holds, named by type and id.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class StoredResourceContext(BaseModel):
    """Evaluate over a resource the facade already holds, named by type and id."""

    model_config = ConfigDict(frozen=True)

    kind: Literal["stored"] = "stored"
    resource_type: str
    resource_id: str

InlineResourceContext

Bases: BaseModel

Evaluate over a resource carried in the request itself - the expression is checked against exactly it.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class InlineResourceContext(BaseModel):
    """Evaluate over a resource carried in the request itself - the expression is checked against exactly it."""

    model_config = ConfigDict(frozen=True)

    kind: Literal["inline"] = "inline"
    resource: JsonResource

    @classmethod
    def over(cls, resource: FhirBase | Mapping[str, Any]) -> InlineResourceContext:
        """Evaluate over this resource, whether the caller holds a typed R4 model or a parsed document.

        The field itself is a `JsonResource`, because that is what goes on the wire. This is the
        constructor a caller reaches for: an integrator holding a `QuestionnaireResponse` and one
        holding the document they just parsed are both one call from an evaluation.
        """
        if isinstance(resource, FhirBase):
            return cls(resource=json_resource(resource))
        return cls(resource=JsonResource.model_validate(dict(resource)))
Methods:
over(resource) classmethod

Evaluate over this resource, whether the caller holds a typed R4 model or a parsed document.

The field itself is a JsonResource, because that is what goes on the wire. This is the constructor a caller reaches for: an integrator holding a QuestionnaireResponse and one holding the document they just parsed are both one call from an evaluation.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
@classmethod
def over(cls, resource: FhirBase | Mapping[str, Any]) -> InlineResourceContext:
    """Evaluate over this resource, whether the caller holds a typed R4 model or a parsed document.

    The field itself is a `JsonResource`, because that is what goes on the wire. This is the
    constructor a caller reaches for: an integrator holding a `QuestionnaireResponse` and one
    holding the document they just parsed are both one call from an evaluation.
    """
    if isinstance(resource, FhirBase):
        return cls(resource=json_resource(resource))
    return cls(resource=JsonResource.model_validate(dict(resource)))

RegisteredEntityContext

Bases: BaseModel

Evaluate over a person the live register holds, named by their DHIS2 tracked entity uid.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class RegisteredEntityContext(BaseModel):
    """Evaluate over a person the live register holds, named by their DHIS2 tracked entity uid."""

    model_config = ConfigDict(frozen=True)

    kind: Literal["registered"] = "registered"
    resource_type: str = DEFAULT_SUBJECT_RESOURCE_TYPE
    tracked_entity_uid: str

EvaluationRequest

Bases: BaseModel

One evaluation as a caller asks for it - mirrors dhis2w_fhir_serve.routes.evaluate.EvaluationRequest.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class EvaluationRequest(BaseModel):
    """One evaluation as a caller asks for it - mirrors `dhis2w_fhir_serve.routes.evaluate.EvaluationRequest`."""

    model_config = ConfigDict(frozen=True)

    language: EvaluationLanguage
    source: str
    """The FHIRPath expression, the CQL library text, or the ELM library as JSON."""

    expression_name: str | None = None
    """Which define to answer. Omitted, a CQL or ELM library answers every define it declares."""

    context: EvaluationContext | None = None
    """The resource to evaluate over. Omitted, the expression runs over no resource at all."""
Attributes
source instance-attribute

The FHIRPath expression, the CQL library text, or the ELM library as JSON.

expression_name = None class-attribute instance-attribute

Which define to answer. Omitted, a CQL or ELM library answers every define it declares.

context = None class-attribute instance-attribute

The resource to evaluate over. Omitted, the expression runs over no resource at all.

EvaluationDiagnostic

Bases: BaseModel

One thing that stopped the run, at the position the parser stated one.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class EvaluationDiagnostic(BaseModel):
    """One thing that stopped the run, at the position the parser stated one."""

    model_config = ConfigDict(frozen=True)

    kind: DiagnosticKind
    message: str
    line: int | None = None
    column: int | None = None
    """The column on that line, counted from one."""

    expression_name: str | None = None
    """The define this diagnostic is about, for a name the library does not declare."""
Attributes
column = None class-attribute instance-attribute

The column on that line, counted from one.

expression_name = None class-attribute instance-attribute

The define this diagnostic is about, for a name the library does not declare.

EvaluationResult

Bases: BaseModel

What one expression or one define answered, as JSON.

values is always a collection, because FHIRPath answers collections and a CQL define answering a single value is that value carried as one. An empty collection is an answer - the expression matched nothing - and is not the same state as a refusal.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class EvaluationResult(BaseModel):
    """What one expression or one define answered, as JSON.

    `values` is always a collection, because FHIRPath answers collections and a CQL define answering
    a single value is that value carried as one. An empty collection is an answer - the expression
    matched nothing - and is not the same state as a refusal.
    """

    model_config = ConfigDict(frozen=True)

    name: str
    """The define this row answers, or `expression` for a FHIRPath collection."""

    values: tuple[JsonValue, ...] = ()
    refusal: str | None = None
    """Why this one define answered nothing, when the rest of the library still answered."""
Attributes
name instance-attribute

The define this row answers, or expression for a FHIRPath collection.

refusal = None class-attribute instance-attribute

Why this one define answered nothing, when the rest of the library still answered.

EvaluationOutcome

Bases: BaseModel

One evaluation, whole: what it answered, what it declares, and what stopped it.

A source that will not parse is a 200 carrying a diagnostic, never a refusal - which is why evaluate answers this rather than raising on a bad expression. FacadeError is reserved for a request the facade cannot serve at all: a stored resource it does not hold, a register it does not publish.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class EvaluationOutcome(BaseModel):
    """One evaluation, whole: what it answered, what it declares, and what stopped it.

    A source that will not parse is a 200 carrying a diagnostic, never a refusal - which is why
    `evaluate` answers this rather than raising on a bad expression. `FacadeError` is reserved for a
    request the facade cannot serve at all: a stored resource it does not hold, a register it does
    not publish.
    """

    model_config = ConfigDict(frozen=True)

    language: EvaluationLanguage
    results: tuple[EvaluationResult, ...] = ()
    diagnostics: tuple[EvaluationDiagnostic, ...] = ()
    definitions: tuple[str, ...] = ()
    """Every define the library declares, in declaration order - empty for FHIRPath, which has none."""
Attributes
definitions = () class-attribute instance-attribute

Every define the library declares, in declaration order - empty for FHIRPath, which has none.

FacadeClient

A typed async client for one d2w fhir serve facade.

Use it as an async context manager and it owns its connection pool; hand it an httpx2.AsyncClient and it borrows that one, leaving it open at exit - which is what a caller pooling several clients, or a test driving the application in-process, wants.

Every request carries Accept: application/fhir+json, except POST /facade/evaluate, which is on the facade's own API rather than the FHIR surface and answers plain JSON. Every non-2xx raises FacadeError.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
class FacadeClient:
    """A typed async client for one `d2w fhir serve` facade.

    Use it as an async context manager and it owns its connection pool; hand it an
    `httpx2.AsyncClient` and it borrows that one, leaving it open at exit - which is what a caller
    pooling several clients, or a test driving the application in-process, wants.

    Every request carries `Accept: application/fhir+json`, except `POST /facade/evaluate`, which is
    on the facade's own API rather than the FHIR surface and answers plain JSON. Every non-2xx raises
    `FacadeError`.
    """

    def __init__(
        self,
        base_url: str,
        *,
        auth: FacadeCredential | None = None,
        timeout: float = 30.0,
        http_client: httpx2.AsyncClient | None = None,
    ) -> None:
        """Point a client at a facade, optionally with a credential and a pool to borrow."""
        self._base_url = base_url.rstrip("/")
        self._auth = auth
        self._timeout = timeout
        self._borrowed_http_client = http_client
        self._http_client = http_client
        self._capability: CapabilityStatement | None = None

    @property
    def base_url(self) -> str:
        """The facade this client talks to, without a trailing slash."""
        return self._base_url

    async def __aenter__(self) -> Self:
        """Open the connection pool, unless one was handed in."""
        self._open()
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        traceback: TracebackType | None,
    ) -> None:
        """Close the pool this client opened."""
        await self.aclose()

    async def aclose(self) -> None:
        """Close the pool this client opened; a pool the caller supplied is left for the caller to close."""
        if self._borrowed_http_client is not None or self._http_client is None:
            return
        await self._http_client.aclose()
        self._http_client = None

    async def capability(self, *, refresh: bool = False) -> CapabilityStatement:
        """The facade's `/metadata` - its only contract, read once and held unless `refresh` asks again."""
        if self._capability is None or refresh:
            answered = await self._request("GET", "/metadata")
            self._capability = CapabilityStatement.model_validate(answered.json())
        return self._capability

    async def read(self, resource_type: str, resource_id: str) -> JsonResource:
        """One resource by type and id, carried as the document the facade served.

        A stored resource is answered verbatim, so which keys it has is the document's business
        rather than this client's - `JsonResource` keeps every one of them. `read_response` is the
        typed path for the one type a caller reads back most.
        """
        answered = await self._request("GET", f"/{resource_type}/{resource_id}")
        return JsonResource.model_validate(answered.json())

    async def read_response(self, response_id: str) -> QuestionnaireResponse:
        """One receipt by id - the submission exactly as it arrived, not a view of anything DHIS2 holds."""
        answered = await self._request("GET", f"/{_QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}/{response_id}")
        return QuestionnaireResponse.model_validate(answered.json())

    async def search(self, resource_type: str, query: ResourceQuery | None = None) -> Bundle:
        """One searchset Bundle for a resource type.

        `Bundle.total` is the whole match set and `entry` is what this request asked for, so a
        capped search answers a total larger than the entries it carries.
        """
        parameters = (query or ResourceQuery()).to_query_parameters()
        answered = await self._request("GET", f"/{resource_type}", params=parameters)
        return Bundle.model_validate(answered.json())

    async def resolve(self, canonical: str) -> JsonResource | None:
        """The one resource a canonical url names, looked for across every type that declares `url`.

        A canonical says what a resource is without saying where it lives, so resolving one means
        asking each type in turn. `None` is an answer: this facade publishes nothing under that url.
        """
        for resource_type in await self.canonical_resource_types():
            bundle = await self.search(resource_type, ResourceQuery(urls=(canonical,), count=1))
            for entry in bundle.entry or ():
                if entry.resource is not None:
                    return entry.resource
        return None

    async def canonical_resource_types(self) -> tuple[str, ...]:
        """Every resource type this facade's `/metadata` declares a `url` search parameter on.

        Read off the CapabilityStatement rather than hard-coded, because the facade lists a type only
        when its store actually holds instances of it - a project publishing no ConceptMap does not
        declare one, and `resolve` should not ask.
        """
        capability = await self.capability()
        declared: list[str] = []
        for rest in capability.rest or ():
            for resource in rest.resource or ():
                searches_by_canonical = any(
                    parameter.name == _CANONICAL_SEARCH_PARAMETER for parameter in resource.searchParam or ()
                )
                if resource.type is not None and searches_by_canonical:
                    declared.append(resource.type)
        return tuple(declared)

    async def generate(self, questionnaire_id: str, *, seed: int | None = None) -> QuestionnaireResponse:
        """A filled draft answering one published form, against that form's own rules.

        `seed` makes the draft byte-reproducible. What comes back is postable to `submit_response`
        unchanged, which is the invariant this operation exists for.
        """
        parameters = (("seed", str(seed)),) if seed is not None else None
        path = f"/{_QUESTIONNAIRE_RESOURCE_TYPE}/{questionnaire_id}/$generate"
        answered = await self._request("GET", path, params=parameters)
        return QuestionnaireResponse.model_validate(answered.json())

    async def submit_response(
        self,
        questionnaire_response: QuestionnaireResponse | Mapping[str, Any],
    ) -> CaptureReceipt:
        """Submit one filled form and answer the receipt the facade handed back.

        Takes a model or a parsed document, because an integrator holding either should be one call
        away. A submission the facade will not accept raises `FacadeError` carrying one issue per
        thing wrong with it, which is what a capture screen renders.
        """
        answered = await self._request(
            "POST",
            f"/{_QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}",
            content_type=FHIR_JSON_MEDIA_TYPE,
            content=_capture_payload(questionnaire_response),
        )
        return _receipt(answered)

    async def evaluate(
        self,
        language: EvaluationLanguage | str,
        source: str,
        *,
        expression_name: str | None = None,
        context: StoredResourceContext | InlineResourceContext | RegisteredEntityContext | None = None,
    ) -> EvaluationOutcome:
        """Evaluate one FHIRPath expression, CQL library, or compiled ELM library against this facade.

        A source that will not parse answers an outcome carrying the line and column the parser
        stopped on - it does not raise. `FacadeError` means the facade could not serve the request
        at all, which is a different thing from an expression it could not run.
        """
        request = EvaluationRequest(
            language=EvaluationLanguage(language),
            source=source,
            expression_name=expression_name,
            context=context,
        )
        answered = await self._request(
            "POST",
            f"{FACADE_API_PATH}/evaluate",
            accept=JSON_MEDIA_TYPE,
            content_type=JSON_MEDIA_TYPE,
            content=request.model_dump_json(exclude_none=True).encode(),
        )
        return EvaluationOutcome.model_validate(answered.json())

    async def _request(
        self,
        method: str,
        path: str,
        *,
        accept: str = FHIR_JSON_MEDIA_TYPE,
        content_type: str | None = None,
        params: tuple[tuple[str, str], ...] | None = None,
        content: bytes | None = None,
    ) -> httpx2.Response:
        """One request against the facade, raising `FacadeError` on anything that is not an answer."""
        http_client = self._open()
        answered = await http_client.request(
            method,
            f"{self._base_url}{path}",
            headers=self._headers(accept, content_type),
            params=params,
            content=content,
        )
        if answered.status_code >= _FIRST_REFUSAL_STATUS:
            raise _refusal(method, f"{self._base_url}{path}", answered)
        return answered

    def _headers(self, accept: str, content_type: str | None) -> dict[str, str]:
        """The headers every request carries: what it accepts, what it sends, and who is sending it."""
        headers = {"Accept": accept}
        if content_type is not None:
            headers["Content-Type"] = content_type
        if self._auth is not None:
            headers["Authorization"] = self._auth.authorization()
        return headers

    def _open(self) -> httpx2.AsyncClient:
        """The pool to send on, opened on first use when the caller supplied none."""
        if self._http_client is None:
            self._http_client = httpx2.AsyncClient(timeout=self._timeout)
        return self._http_client
Attributes
base_url property

The facade this client talks to, without a trailing slash.

Methods:
__init__(base_url, *, auth=None, timeout=30.0, http_client=None)

Point a client at a facade, optionally with a credential and a pool to borrow.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
def __init__(
    self,
    base_url: str,
    *,
    auth: FacadeCredential | None = None,
    timeout: float = 30.0,
    http_client: httpx2.AsyncClient | None = None,
) -> None:
    """Point a client at a facade, optionally with a credential and a pool to borrow."""
    self._base_url = base_url.rstrip("/")
    self._auth = auth
    self._timeout = timeout
    self._borrowed_http_client = http_client
    self._http_client = http_client
    self._capability: CapabilityStatement | None = None
__aenter__() async

Open the connection pool, unless one was handed in.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def __aenter__(self) -> Self:
    """Open the connection pool, unless one was handed in."""
    self._open()
    return self
__aexit__(exc_type, exc, traceback) async

Close the pool this client opened.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def __aexit__(
    self,
    exc_type: type[BaseException] | None,
    exc: BaseException | None,
    traceback: TracebackType | None,
) -> None:
    """Close the pool this client opened."""
    await self.aclose()
aclose() async

Close the pool this client opened; a pool the caller supplied is left for the caller to close.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def aclose(self) -> None:
    """Close the pool this client opened; a pool the caller supplied is left for the caller to close."""
    if self._borrowed_http_client is not None or self._http_client is None:
        return
    await self._http_client.aclose()
    self._http_client = None
capability(*, refresh=False) async

The facade's /metadata - its only contract, read once and held unless refresh asks again.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def capability(self, *, refresh: bool = False) -> CapabilityStatement:
    """The facade's `/metadata` - its only contract, read once and held unless `refresh` asks again."""
    if self._capability is None or refresh:
        answered = await self._request("GET", "/metadata")
        self._capability = CapabilityStatement.model_validate(answered.json())
    return self._capability
read(resource_type, resource_id) async

One resource by type and id, carried as the document the facade served.

A stored resource is answered verbatim, so which keys it has is the document's business rather than this client's - JsonResource keeps every one of them. read_response is the typed path for the one type a caller reads back most.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def read(self, resource_type: str, resource_id: str) -> JsonResource:
    """One resource by type and id, carried as the document the facade served.

    A stored resource is answered verbatim, so which keys it has is the document's business
    rather than this client's - `JsonResource` keeps every one of them. `read_response` is the
    typed path for the one type a caller reads back most.
    """
    answered = await self._request("GET", f"/{resource_type}/{resource_id}")
    return JsonResource.model_validate(answered.json())
read_response(response_id) async

One receipt by id - the submission exactly as it arrived, not a view of anything DHIS2 holds.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def read_response(self, response_id: str) -> QuestionnaireResponse:
    """One receipt by id - the submission exactly as it arrived, not a view of anything DHIS2 holds."""
    answered = await self._request("GET", f"/{_QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}/{response_id}")
    return QuestionnaireResponse.model_validate(answered.json())
search(resource_type, query=None) async

One searchset Bundle for a resource type.

Bundle.total is the whole match set and entry is what this request asked for, so a capped search answers a total larger than the entries it carries.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def search(self, resource_type: str, query: ResourceQuery | None = None) -> Bundle:
    """One searchset Bundle for a resource type.

    `Bundle.total` is the whole match set and `entry` is what this request asked for, so a
    capped search answers a total larger than the entries it carries.
    """
    parameters = (query or ResourceQuery()).to_query_parameters()
    answered = await self._request("GET", f"/{resource_type}", params=parameters)
    return Bundle.model_validate(answered.json())
resolve(canonical) async

The one resource a canonical url names, looked for across every type that declares url.

A canonical says what a resource is without saying where it lives, so resolving one means asking each type in turn. None is an answer: this facade publishes nothing under that url.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def resolve(self, canonical: str) -> JsonResource | None:
    """The one resource a canonical url names, looked for across every type that declares `url`.

    A canonical says what a resource is without saying where it lives, so resolving one means
    asking each type in turn. `None` is an answer: this facade publishes nothing under that url.
    """
    for resource_type in await self.canonical_resource_types():
        bundle = await self.search(resource_type, ResourceQuery(urls=(canonical,), count=1))
        for entry in bundle.entry or ():
            if entry.resource is not None:
                return entry.resource
    return None
canonical_resource_types() async

Every resource type this facade's /metadata declares a url search parameter on.

Read off the CapabilityStatement rather than hard-coded, because the facade lists a type only when its store actually holds instances of it - a project publishing no ConceptMap does not declare one, and resolve should not ask.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def canonical_resource_types(self) -> tuple[str, ...]:
    """Every resource type this facade's `/metadata` declares a `url` search parameter on.

    Read off the CapabilityStatement rather than hard-coded, because the facade lists a type only
    when its store actually holds instances of it - a project publishing no ConceptMap does not
    declare one, and `resolve` should not ask.
    """
    capability = await self.capability()
    declared: list[str] = []
    for rest in capability.rest or ():
        for resource in rest.resource or ():
            searches_by_canonical = any(
                parameter.name == _CANONICAL_SEARCH_PARAMETER for parameter in resource.searchParam or ()
            )
            if resource.type is not None and searches_by_canonical:
                declared.append(resource.type)
    return tuple(declared)
generate(questionnaire_id, *, seed=None) async

A filled draft answering one published form, against that form's own rules.

seed makes the draft byte-reproducible. What comes back is postable to submit_response unchanged, which is the invariant this operation exists for.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def generate(self, questionnaire_id: str, *, seed: int | None = None) -> QuestionnaireResponse:
    """A filled draft answering one published form, against that form's own rules.

    `seed` makes the draft byte-reproducible. What comes back is postable to `submit_response`
    unchanged, which is the invariant this operation exists for.
    """
    parameters = (("seed", str(seed)),) if seed is not None else None
    path = f"/{_QUESTIONNAIRE_RESOURCE_TYPE}/{questionnaire_id}/$generate"
    answered = await self._request("GET", path, params=parameters)
    return QuestionnaireResponse.model_validate(answered.json())
submit_response(questionnaire_response) async

Submit one filled form and answer the receipt the facade handed back.

Takes a model or a parsed document, because an integrator holding either should be one call away. A submission the facade will not accept raises FacadeError carrying one issue per thing wrong with it, which is what a capture screen renders.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def submit_response(
    self,
    questionnaire_response: QuestionnaireResponse | Mapping[str, Any],
) -> CaptureReceipt:
    """Submit one filled form and answer the receipt the facade handed back.

    Takes a model or a parsed document, because an integrator holding either should be one call
    away. A submission the facade will not accept raises `FacadeError` carrying one issue per
    thing wrong with it, which is what a capture screen renders.
    """
    answered = await self._request(
        "POST",
        f"/{_QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}",
        content_type=FHIR_JSON_MEDIA_TYPE,
        content=_capture_payload(questionnaire_response),
    )
    return _receipt(answered)
evaluate(language, source, *, expression_name=None, context=None) async

Evaluate one FHIRPath expression, CQL library, or compiled ELM library against this facade.

A source that will not parse answers an outcome carrying the line and column the parser stopped on - it does not raise. FacadeError means the facade could not serve the request at all, which is a different thing from an expression it could not run.

Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
async def evaluate(
    self,
    language: EvaluationLanguage | str,
    source: str,
    *,
    expression_name: str | None = None,
    context: StoredResourceContext | InlineResourceContext | RegisteredEntityContext | None = None,
) -> EvaluationOutcome:
    """Evaluate one FHIRPath expression, CQL library, or compiled ELM library against this facade.

    A source that will not parse answers an outcome carrying the line and column the parser
    stopped on - it does not raise. `FacadeError` means the facade could not serve the request
    at all, which is a different thing from an expression it could not run.
    """
    request = EvaluationRequest(
        language=EvaluationLanguage(language),
        source=source,
        expression_name=expression_name,
        context=context,
    )
    answered = await self._request(
        "POST",
        f"{FACADE_API_PATH}/evaluate",
        accept=JSON_MEDIA_TYPE,
        content_type=JSON_MEDIA_TYPE,
        content=request.model_dump_json(exclude_none=True).encode(),
    )
    return EvaluationOutcome.model_validate(answered.json())

Functions:

Package surface

The names below re-export from dhis2w_fhir itself; the generate page covers what each emitter produces.

dhis2w_fhir

Version-neutral FHIR IG generation: fhir.toml config, FSH emission, and project scaffolding.

Each component owns its schemas; this module is the one stable import surface over them, so from dhis2w_fhir import GenerateConfig keeps working however the components are arranged internally.