Skip to content

FHIR facade server (dhis2w_fhir_serve)

dhis2w_fhir_serve is the package behind d2w fhir serve: a FastAPI application that serves one generated IG as a FHIR endpoint and receives QuestionnaireResponse captures against it. It is its own workspace member because it needs FastAPI and uvicorn, and dhis2w-fhir - which generates a file tree - needs neither; pip install 'dhis2w-cli[serve]' (or uv add dhis2w-fhir-serve) is what puts the command on the CLI.

Everything a running facade serves is loaded once at startup and held on app.state.context, so a request is index lookups and nothing else. The store is either the compiled IG on disk or - under --live - the same read set built off a DHIS2 instance through one client opened during startup. That client stays open for the life of the process, because GET /Patient and the enrollment listing answer from the instance per request; no read of the store ever touches DHIS2 again.

The receipt spool is the one exception, and deliberately so: it is a path rather than a loaded index, and every read of it re-reads the directory. d2w fhir forward runs as a separate process and renames receipt files between the spool's four lifecycle directories while the server is up, so anything cached would be stale within seconds of a drain.

When to reach for it

  • Embed the facade in another ASGI process (create_app, ServeSettings), or mount its routers beside your own routes over a runtime you opened yourself (ServeSettings.resolve, open_serve_runtime, attach_serve_runtime, serve_routers, register_error_handlers) - see Embed the facade.
  • Get the settings d2w fhir serve gets for the same project and the same flags, precedence rules included (ServeSettings.resolve, ServeInvocation).
  • Load a project's store, spool, and register surface without an HTTP server at all (open_serve_runtime, ServeRuntime, ServeContext).
  • Build the served read set off a live DHIS2 instance, over a client you hold open (open_live_client, build_live_store, build_store).
  • Translate a concept through the ConceptMaps a project publishes, with no server running (find_translations, TranslateRequest, TranslationMatch).
  • Run one FHIRPath expression, CQL library, or ELM library over a resource and read what the parser and the evaluator said about it (evaluate_source, EvaluationLanguage, EvaluationOutcome, EvaluationResult, EvaluationDiagnostic, json_safe, syntax_diagnostic).
  • Read an evaluation's Parameters input, and say an evaluation's answer as the Parameters a FHIR client reads (evaluation_ask, evaluation_parameters, EVALUATE_OPERATION_PATH).
  • Ask what a code means in the vocabularies one project publishes, with no server running (load_terminology, TerminologyState, LookedUpCode, ValidatedCode, ConceptProperty).
  • Answer a CDS Hooks invocation with cards built from a CQL library (CdsService, CdsDiscovery, CdsHookRequest, CdsHookResponse, CdsCard, CqlLibraryHookContext).
  • Page through the receipts a facade holds the way GET /facade/spool pages through them (page_of, SpoolCursor, SpoolPage, requested_page_size, requested_cursor).
  • Load a project's served resources without an HTTP server (load_compiled_store, ResourceStore, SearchQuery, IdentifierToken).
  • Read or write the receipt spool a running facade keeps, in any of its four lifecycle states (ResponseSpool, StoredReceipt, StoredResponseEnvelope, ResponseLifecycle, RECEIVED_RESPONSES_RELATIVE_PATH, WITHDRAWN_RESPONSES_RELATIVE_PATH).
  • Validate a QuestionnaireResponse against a served IG outside the endpoint (validate_response, build_capture_index, CaptureNaming, CodingResolverSet).
  • Generate a synthetic response against a served form (generate_response, draw_seed, resolve_period_type, MAXIMUM_SEED).
  • Render the FHIR error and outcome bodies the facade answers with (outcome, rejection_outcome, success_outcome, build_server_capability).
  • Read what a guide states about the tracked entities an instance holds, or search a DHIS2 instance for one, without an HTTP server (TrackedEntityIndex, PublishedAttribute, PublishedTrackedEntityType, RegisterSurface, ServedRegister, registered_entity_for, search_tracked_entities, fetch_tracked_entity, TrackedEntityEnrollment, TrackedEntityEnrollments).
  • Read a register's value filter, or narrow a search of your own by one (AttributeFilter, requested_attribute_filters, ATTRIBUTE_FILTER_PARAMETER, ATTRIBUTE_FILTER_OPERATOR).
  • Put something other than the DHIS2 instance behind a register search, or hold a copy of an instance as FHIR resources (NameSearchIndex, NameQuery, NameMatch, NameMatches, Dhis2NameSearchIndex, build_name_search_index, ProjectionStore, ProjectedResource, ProjectionBatch, ProjectionCursor).

Worked example - load a store and read one resource

from dhis2w_fhir import load_project
from dhis2w_fhir_serve import SearchQuery, load_compiled_store

project = load_project()
store = load_compiled_store(project)

store.summary().counts_by_type
# {'CodeSystem': 42, 'Location': 1332, 'Organization': 1332, 'Questionnaire': 9, 'ValueSet': 42}

entry = store.by_type_and_id("Questionnaire", "BfMAe6Itzgt")
entry.body["title"]
# 'Child Health'

store.search("Questionnaire", SearchQuery(urls=("http://example.org/fhir/demo/Questionnaire/BfMAe6Itzgt",)))
# (StoreEntry(resource_type='Questionnaire', resource_id='BfMAe6Itzgt', ...),)

Embed the facade

create_app builds the whole server, and an application that already runs FastAPI wants the FHIR surface inside the process it has. That is four steps, and each one is a name:

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from dhis2w_fhir import load_project
from dhis2w_fhir_serve import (
    ServeSettings,
    accept_head_wherever_get_is_served,
    attach_serve_runtime,
    open_serve_runtime,
    register_error_handlers,
    require_json_is_acceptable,
    serve_routers,
)
from fastapi import Depends, FastAPI

settings = ServeSettings.resolve(load_project()).settings


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    async with open_serve_runtime(settings) as runtime:
        attach_serve_runtime(app, runtime)
        yield


application = FastAPI(lifespan=lifespan)
register_error_handlers(application)

routers = serve_routers(capture=settings.capture)
for router in routers.in_mount_order():
    accept_head_wherever_get_is_served(router)
for router in routers.fhir:
    application.include_router(router, dependencies=[Depends(require_json_is_acceptable)])
for router in routers.facade:
    application.include_router(router)
application.include_router(routers.read, dependencies=[Depends(require_json_is_acceptable)])

What each step is for:

  1. The settings. ServeSettings.resolve applies the flag-over-[serve] precedence, resolves the DHIS2 profile, and refuses a project that has never been built. Constructing ServeSettings directly is supported too, and a facade built that way differs from d2w fhir serve on purpose rather than by accident.
  2. The runtime. open_serve_runtime loads the project, the store, the spool, the register surface, and the CapabilityStatement, and holds the DHIS2 client a live run reads through open for as long as the context manager is entered. Under auth = "dhis2" it holds a second connection beside it - ServeRuntime.caller_client, pointed at the same instance and carrying no credential - which is the pool a register read forwards each caller's own Authorization over; see credential pass-through. attach_serve_runtime puts all three things the handlers read onto the application, and nothing serves a request before it has been called.
  3. The routers. serve_routers states the mount requirements as data: the FHIR routers carry Depends(require_json_is_acceptable), the facade routers do not, the read catch-alls mount after every fixed path your application serves - /{resource_type} claims any one-segment path, so your own /health mounts first or it is gone - and every router gets the HEAD sweep, or a liveness probe asking HEAD /metadata reads a live facade as down. ServeRouters.guarded is the fourth requirement, and the one an application usually answers itself: see bring your own authentication.
  4. The error handlers. Without register_error_handlers, every typed refusal the facade raises - RegisterDisabledError, NotServedError, CaptureDisabledError - is a 500 with no OperationOutcome in it.

Pick what you mount

serve_routers answers every router this facade has, and an embedding application rarely wants all of them. The two collections it answers - fhir and facade - are what you narrow, and the route modules are where each router is imported from:

from dhis2w_fhir_serve.metadata import router as metadata_router
from dhis2w_fhir_serve.routes.capture import refusal_router as capture_refusal_router
from dhis2w_fhir_serve.routes.capture import router as capture_router
from dhis2w_fhir_serve.routes.cds import router as cds_router
from dhis2w_fhir_serve.routes.enrollments import router as enrollments_router
from dhis2w_fhir_serve.routes.evaluate import router as evaluate_router
from dhis2w_fhir_serve.routes.spool import router as spool_router
from dhis2w_fhir_serve.routes.terminology import router as terminology_router

Five named picks, each a pair of tuples in place of routers.fhir and routers.facade in the loop above. Every one of them still mounts serve_routers().read last, still wants the HEAD sweep, and still needs register_error_handlers - those three are not part of the choice.

Capture only. Receive submissions, and publish the forms they answer.

fhir = (metadata_router, capture_router)
facade = (spool_router,)

Capture and register. The same, plus who the submissions are about. The register itself claims no router - GET /Patient is the read catch-all dispatching to it - so what this pick adds beyond the one above is the enrollment listing a stage form picks from. Live runs only.

fhir = (metadata_router, capture_router)
facade = (spool_router, enrollments_router)

Read-only guide server. Publish the guide and receive nothing. The refusal router claims POST /QuestionnaireResponse so the address answers 405 naming [serve] capture = false, rather than falling through to a 405 that says nothing.

fhir = (metadata_router, capture_refusal_router)
facade = ()

Evaluate playground. The guide, and the three surfaces that run over it: FHIRPath, CQL, and ELM evaluation, this guide's own vocabularies, and CDS Hooks.

fhir = (metadata_router,)
facade = (evaluate_router, terminology_router, cds_router)

The full facade. Every line above, in one call.

routers = serve_routers(capture=settings.capture)

Bring your own authentication

serve_routers answers a fourth value beside the three collections: guarded, the routers the authentication check belongs on for the posture and scope it was called with. It is a subset of the routers already in fhir, facade, and read, compared by identity through routers.is_guarded, so a router is mounted once and the guard is one more dependency on that mount.

d2w fhir serve mounts dhis2w_fhir_serve.auth.require_authenticated over it. An application that already knows who its callers are mounts its own dependency instead - the set is the same set, and nothing else about the facade changes:

from dhis2w_fhir_serve import ServeRouters, serve_routers
from fastapi import Depends, Request


async def whoever_this_application_says(request: Request) -> None: ...  # raise your own 401 here


routers = serve_routers(capture=settings.capture, auth=settings.auth, auth_scope=settings.auth_scope)
for router in routers.fhir:
    guard = [Depends(whoever_this_application_says)] if routers.is_guarded(router) else []
    application.include_router(router, dependencies=[*guard, Depends(require_json_is_acceptable)])

A dependency mounted this way that wants its captures attributed writes a dhis2w_fhir_serve.auth.RequestIdentity onto request.state under dhis2w_fhir_serve.auth.REQUEST_IDENTITY_ATTRIBUTE; the capture route reads it there and stamps the username onto the receipt. An identity written with posture=ServeAuth.DHIS2 also puts the register reads on the pass-through path, where the request's own Authorization header is what reaches DHIS2 - so write that posture only for an identity whose credential DHIS2 itself would accept. register_routes takes the same seam as its authentication argument for an application that wants the facade's own mounting and its own check.

Two things stay outside this contract. The capture UI is not a router and is reached by running create_app with settings.ui; see the UI boundary. And a facade served under a path is mounted rather than included under a prefix: every fullUrl, self, and paging link is built from request.base_url, which carries an ASGI mount's path and not an include_router prefix.

A worked example of all of this is coming to the facade ladder in Build your own facade, as the level above complex_facade.py: an application that mounts these routers rather than reimplementing them.

Reference

Settings

What one running facade is built from: the project it serves, whether the store is built live, which DHIS2 profile that build reads with, and how strictly a received code is checked - and how one invocation resolves all of that from a project's [serve] table and the dials it was given.

settings

The settings the FHIR facade app factory is built from, and how one invocation resolves them.

Attributes

Classes

ServeSettings

Bases: BaseModel

Everything the app factory needs to build the FHIR facade for one project.

Host and port stay uvicorn-side: they describe where the process listens, not what it serves, so the factory never sees them. live selects a store built from a live DHIS2 instance over the compiled IG on disk, profile names the DHIS2 profile that store connects with, and strict_codes rejects a received answer whose code is not in the served terminology instead of recording a warning.

strict_codes is the runtime source every capture is validated against; the default it starts from lives with the capture path, in capture.validate.DEFAULT_STRICT_CODES.

capture is whether this process receives submissions. It comes off [serve] capture and no flag overrides it, for the reason tracked_entities states below: it changes what /metadata declares, which is this server's contract rather than a property of one invocation. False mounts a refusal in place of the create route, drops create from the QuestionnaireResponse entry, and tells the screens not to offer a Submit. Everything else about that resource type stays: the receipts already on disk are read, searched, and counted exactly as they were.

ui serves the built capture UI at /, same-origin with the FHIR routes it talks to. It is off by default: a facade is an endpoint first, and a process answering /metadata for a scripted client has no reason to also hold a React bundle open.

spool_dir is where the receipt tree lives, off [serve] spool_dir - relative to the project root unless it is absolute. It is resolved through dhis2w_fhir.spool.resolve_spool_root, the same function d2w fhir forward resolves it through, because the writer and the drainer landing on two different directories would be a receipt nothing ever forwards.

basemaps are the raster tile layers the UI's organisation-unit map offers under the boundaries, first one first, and an empty list offers none. They live here rather than being read from the project at request time for the same reason strict_codes does: a flag may override the table, so the resolved value is a property of this run.

dhis2_base_url is the address of the instance this run resolved a profile for, and None when it resolved none - a compiled guide served on a machine that names no profile is a whole, supported posture. It is what the UI links an organisation unit, a form, or a data element out to; with no address there are no links, which is the honest rendering of not knowing which of a hundred DHIS2 instances a guide was generated from. The profile's NAME and its credentials stay here and never reach a browser - see dhis2w_fhir_serve.routes.uiconfig.

auth is who this facade serves and auth_scope is how much of it the posture covers. Both are resolved values rather than the table read back, because a flag may state either - and the posture's NAME is the one part of it that crosses to a browser, through /facade/uiconfig. No token, no password, and no header ever reaches these settings: dhis2w_fhir_serve.auth.AuthState holds what a check needs, on the application, for exactly that reason.

jwt is what the jwt posture runs on, off [serve.jwt] with no flag over it: which issuer this facade trusts, the audience it insists on where one is stated, the claim that names the caller, and whether a register read carries the caller's token on to DHIS2. It is a table of PUBLIC facts and that is what makes it safe here - an issuer's identifier is what a client has to be told, not something to keep from one. The issuer's keys are not here: they are fetched while the server starts and held on dhis2w_fhir_serve.auth.AuthState, because they are a live document rather than a setting.

tracked_entities is the register this run serves - whether the instance's tracked entities are answered for at all, whether they can be listed, and how a listing is paged. It comes off [serve.tracked_entities] and no flag overrides it, because every value in it says what this facade tells a client about the subjects the instance holds, which is a decision the project makes once rather than per invocation. Which FHIR resources those subjects are served as is not stated here at all: the published D2TET_CM says that, and the register reads the artifact.

data_sets is what this run answers about the instance's aggregate data - whether the values DHIS2 holds for a data set are served at all, which data sets they are served for, how a page is sized, and how many periods one read may name. It comes off [serve.data_sets] and no flag overrides it, for the reason tracked_entities states: what this facade tells a client about the instance is its contract rather than a property of one invocation.

search is what answers a register search - the NameSearchIndex backend behind every lookup. It comes off [serve.search] and no flag overrides it, for the reason tracked_entities states: which searches this server answers is its contract rather than a property of one invocation.

projection is the materialized projection this project holds: which store, where its file is, and how far back an incremental sync re-reads. It comes off [serve.projection] and no flag overrides it either - a projection is a thing on disk that d2w fhir sync fills and this process reads, so which one a run reads is not a property of the run. store = "none" - the default - is no projection at all, and a facade configured that way behaves exactly as it always did.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/settings.py
class ServeSettings(BaseModel):
    """Everything the app factory needs to build the FHIR facade for one project.

    Host and port stay uvicorn-side: they describe where the process listens, not what
    it serves, so the factory never sees them. `live` selects a store built from a live
    DHIS2 instance over the compiled IG on disk, `profile` names the DHIS2 profile that
    store connects with, and `strict_codes` rejects a received answer whose code is not
    in the served terminology instead of recording a warning.

    `strict_codes` is the runtime source every capture is validated against; the default it
    starts from lives with the capture path, in `capture.validate.DEFAULT_STRICT_CODES`.

    `capture` is whether this process receives submissions. It comes off `[serve] capture` and no
    flag overrides it, for the reason `tracked_entities` states below: it changes what `/metadata`
    declares, which is this server's contract rather than a property of one invocation. False mounts
    a refusal in place of the create route, drops `create` from the QuestionnaireResponse entry, and
    tells the screens not to offer a Submit. Everything else about that resource type stays: the
    receipts already on disk are read, searched, and counted exactly as they were.

    `ui` serves the built capture UI at `/`, same-origin with the FHIR routes it talks to. It is
    off by default: a facade is an endpoint first, and a process answering `/metadata` for a
    scripted client has no reason to also hold a React bundle open.

    `spool_dir` is where the receipt tree lives, off `[serve] spool_dir` - relative to the project
    root unless it is absolute. It is resolved through `dhis2w_fhir.spool.resolve_spool_root`, the
    same function `d2w fhir forward` resolves it through, because the writer and the drainer landing
    on two different directories would be a receipt nothing ever forwards.

    `basemaps` are the raster tile layers the UI's organisation-unit map offers under the
    boundaries, first one first, and an empty list offers none. They live here rather than being
    read from the project at request time for the same reason `strict_codes` does: a flag may
    override the table, so the resolved value is a property of this run.

    `dhis2_base_url` is the address of the instance this run resolved a profile for, and None when
    it resolved none - a compiled guide served on a machine that names no profile is a whole,
    supported posture. It is what the UI links an organisation unit, a form, or a data element out
    to; with no address there are no links, which is the honest rendering of not knowing which of a
    hundred DHIS2 instances a guide was generated from. The profile's NAME and its credentials stay
    here and never reach a browser - see `dhis2w_fhir_serve.routes.uiconfig`.

    `auth` is who this facade serves and `auth_scope` is how much of it the posture covers. Both are
    resolved values rather than the table read back, because a flag may state either - and the
    posture's NAME is the one part of it that crosses to a browser, through `/facade/uiconfig`. No token, no
    password, and no header ever reaches these settings: `dhis2w_fhir_serve.auth.AuthState` holds what
    a check needs, on the application, for exactly that reason.

    `jwt` is what the `jwt` posture runs on, off `[serve.jwt]` with no flag over it: which issuer this
    facade trusts, the audience it insists on where one is stated, the claim that names the caller, and
    whether a register read carries the caller's token on to DHIS2. It is a table of PUBLIC facts and
    that is what makes it safe here - an issuer's identifier is what a client has to be told, not
    something to keep from one. The issuer's keys are not here: they are fetched while the server
    starts and held on `dhis2w_fhir_serve.auth.AuthState`, because they are a live document rather than
    a setting.

    `tracked_entities` is the register this run serves - whether the instance's tracked entities are
    answered for at all, whether they can be listed, and how a listing is paged. It comes off
    `[serve.tracked_entities]` and no flag overrides it, because every value in it says what this
    facade tells a client about the subjects the instance holds, which is a decision the project
    makes once rather than per invocation. Which FHIR resources those subjects are served as is not
    stated here at all: the published `D2TET_CM` says that, and the register reads the artifact.

    `data_sets` is what this run answers about the instance's aggregate data - whether the values
    DHIS2 holds for a data set are served at all, which data sets they are served for, how a page is
    sized, and how many periods one read may name. It comes off `[serve.data_sets]` and no flag
    overrides it, for the reason `tracked_entities` states: what this facade tells a client about the
    instance is its contract rather than a property of one invocation.

    `search` is what answers a register search - the `NameSearchIndex` backend behind every lookup.
    It comes off `[serve.search]` and no flag overrides it, for the reason `tracked_entities` states:
    which searches this server answers is its contract rather than a property of one invocation.

    `projection` is the materialized projection this project holds: which store, where its file is,
    and how far back an incremental sync re-reads. It comes off `[serve.projection]` and no flag
    overrides it either - a projection is a thing on disk that `d2w fhir sync` fills and this process
    reads, so which one a run reads is not a property of the run. `store = "none"` - the default - is
    no projection at all, and a facade configured that way behaves exactly as it always did.
    """

    model_config = ConfigDict(frozen=True)

    project_dir: Path
    live: bool = False
    profile: str | None = None
    auth: ServeAuth = ServeAuth.NONE
    auth_scope: ServeAuthScope = ServeAuthScope.WRITE
    jwt: ServeJwtConfig = Field(default_factory=ServeJwtConfig)
    strict_codes: bool = DEFAULT_STRICT_CODES
    capture: bool = True
    ui: bool = False
    spool_dir: str = SPOOL_RELATIVE_PATH
    basemaps: list[BasemapSource] = Field(default_factory=lambda: list(DEFAULT_BASEMAPS))
    dhis2_base_url: str | None = None
    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)

    @classmethod
    def resolve(
        cls,
        project: FhirProject,
        *,
        live: bool = False,
        host: str | None = None,
        port: int | None = None,
        strict_codes: bool | None = None,
        ui: bool | None = None,
        basemaps: list[str] | None = None,
        profile: str | None = None,
        auth: ServeAuth | None = None,
        auth_scope: ServeAuthScope | None = None,
    ) -> ServeInvocation:
        """Resolve one invocation of the facade: a stated dial wins, then `[serve]`, then this model's defaults.

        Every argument is what one run stated and None is "stated nothing", which is why the
        booleans are `bool | None` rather than `bool`: `--no-ui` on a project whose table says
        `ui = true` has to be able to say False and be heard. `live` is the exception and takes a
        plain bool, because no `[serve]` key answers it - a live store is a property of the run.

        `basemaps` are the raw `--basemap` strings, read through `basemaps_from_options`; stating
        none leaves the table's layers alone. A value that names nothing servable raises
        `ValueError`, which is the caller's to render - `d2w fhir serve` renders it against the
        flag the value came from.

        Two of `[serve]` have no dial at all and are carried across verbatim: `capture`, which says
        what this server offers rather than what one run does, and `spool_dir`, which says where the
        receipts a previous run wrote already live.

        The profile is resolved here, before anything is built, so a named profile that does not
        exist refuses the run rather than failing under a banner saying the server is starting. A
        machine that names no profile at all resolves to None and serves its compiled guide offline,
        which is a whole posture rather than a degraded one; `live` makes the profile required,
        since a live store has an instance to read. The address it resolved is what the screens link
        an identity out to, and it is the only part of the profile that reaches the settings - the
        name, the origin, and the credentials stay on the invocation, which never leaves the process
        that opened it.

        The compiled guide is checked last, and only when the store is the one on disk: a project
        that has never run SUSHI has nothing to serve, and refusing here says so in one line rather
        than answering every read with a 404.

        THE AUTH POSTURE IS PREFLIGHTED HERE TOO, and for the same reason every other refusal is: a
        server that starts and then cannot honour what it was asked for is a failure nobody reads
        until somebody meets it. Five refusals, in `dhis2w_fhir_serve.auth.preflight_auth`: an
        interface other than loopback while neither this run nor fhir.toml has stated a posture, the
        `token` posture with its environment variable unset, the `dhis2` posture on a run that reads
        a compiled guide and so has no instance to check anybody against, the `jwt` posture with no
        `[serve.jwt] issuer`, and `[serve.jwt] forward_bearer` on a run with no instance to forward
        to. Whether the posture was STATED is what the first of those turns on - `auth = "none"`
        written down is a decision, an absent key is not - so a stated flag counts as much as a
        stated table key, and only the two of them being silent is silence.

        The sixth thing that could stop a `jwt` run - an issuer this machine cannot reach - is a
        round trip, so it is not made here: `open_serve_runtime` reads the issuer while the server
        starts and refuses with the same error type. This function reads values.
        """
        serve_config = project.config.serve
        stated_basemaps = list(basemaps or [])
        # The refusals are ordered as a reader meets them: a value this run stated and cannot mean
        # is answered before a profile is looked up, and both before the guide on disk is counted.
        resolved_basemaps = basemaps_from_options(stated_basemaps) if stated_basemaps else list(serve_config.basemaps)
        resolved_host = host if host is not None else serve_config.host
        resolved_auth = auth if auth is not None else serve_config.auth
        preflight_auth(
            posture=resolved_auth if resolved_auth is not None else ServeAuth.NONE,
            host=resolved_host,
            live=live,
            stated=auth is not None or serve_config.auth is not None,
            jwt=serve_config.jwt,
        )
        generation = _resolved_generation(project, profile, required=live)
        if not live and not any((project.ig_directory / COMPILED_RESOURCES_RELATIVE_PATH).glob("*.json")):
            raise CompiledIgMissingError
        return ServeInvocation(
            settings=cls(
                project_dir=project.project_root,
                live=live,
                profile=profile,
                auth=resolved_auth if resolved_auth is not None else ServeAuth.NONE,
                auth_scope=auth_scope if auth_scope is not None else serve_config.auth_scope,
                jwt=serve_config.jwt,
                strict_codes=strict_codes if strict_codes is not None else serve_config.strict_codes,
                capture=serve_config.capture,
                ui=ui if ui is not None else serve_config.ui,
                spool_dir=serve_config.spool_dir,
                basemaps=resolved_basemaps,
                dhis2_base_url=None if generation is None else generation.profile.base_url,
                tracked_entities=serve_config.tracked_entities,
                data_sets=serve_config.data_sets,
                search=serve_config.search,
                projection=serve_config.projection,
            ),
            host=resolved_host,
            port=port if port is not None else serve_config.port,
            generation=generation,
        )
Methods:
resolve(project, *, live=False, host=None, port=None, strict_codes=None, ui=None, basemaps=None, profile=None, auth=None, auth_scope=None) classmethod

Resolve one invocation of the facade: a stated dial wins, then [serve], then this model's defaults.

Every argument is what one run stated and None is "stated nothing", which is why the booleans are bool | None rather than bool: --no-ui on a project whose table says ui = true has to be able to say False and be heard. live is the exception and takes a plain bool, because no [serve] key answers it - a live store is a property of the run.

basemaps are the raw --basemap strings, read through basemaps_from_options; stating none leaves the table's layers alone. A value that names nothing servable raises ValueError, which is the caller's to render - d2w fhir serve renders it against the flag the value came from.

Two of [serve] have no dial at all and are carried across verbatim: capture, which says what this server offers rather than what one run does, and spool_dir, which says where the receipts a previous run wrote already live.

The profile is resolved here, before anything is built, so a named profile that does not exist refuses the run rather than failing under a banner saying the server is starting. A machine that names no profile at all resolves to None and serves its compiled guide offline, which is a whole posture rather than a degraded one; live makes the profile required, since a live store has an instance to read. The address it resolved is what the screens link an identity out to, and it is the only part of the profile that reaches the settings - the name, the origin, and the credentials stay on the invocation, which never leaves the process that opened it.

The compiled guide is checked last, and only when the store is the one on disk: a project that has never run SUSHI has nothing to serve, and refusing here says so in one line rather than answering every read with a 404.

THE AUTH POSTURE IS PREFLIGHTED HERE TOO, and for the same reason every other refusal is: a server that starts and then cannot honour what it was asked for is a failure nobody reads until somebody meets it. Five refusals, in dhis2w_fhir_serve.auth.preflight_auth: an interface other than loopback while neither this run nor fhir.toml has stated a posture, the token posture with its environment variable unset, the dhis2 posture on a run that reads a compiled guide and so has no instance to check anybody against, the jwt posture with no [serve.jwt] issuer, and [serve.jwt] forward_bearer on a run with no instance to forward to. Whether the posture was STATED is what the first of those turns on - auth = "none" written down is a decision, an absent key is not - so a stated flag counts as much as a stated table key, and only the two of them being silent is silence.

The sixth thing that could stop a jwt run - an issuer this machine cannot reach - is a round trip, so it is not made here: open_serve_runtime reads the issuer while the server starts and refuses with the same error type. This function reads values.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/settings.py
@classmethod
def resolve(
    cls,
    project: FhirProject,
    *,
    live: bool = False,
    host: str | None = None,
    port: int | None = None,
    strict_codes: bool | None = None,
    ui: bool | None = None,
    basemaps: list[str] | None = None,
    profile: str | None = None,
    auth: ServeAuth | None = None,
    auth_scope: ServeAuthScope | None = None,
) -> ServeInvocation:
    """Resolve one invocation of the facade: a stated dial wins, then `[serve]`, then this model's defaults.

    Every argument is what one run stated and None is "stated nothing", which is why the
    booleans are `bool | None` rather than `bool`: `--no-ui` on a project whose table says
    `ui = true` has to be able to say False and be heard. `live` is the exception and takes a
    plain bool, because no `[serve]` key answers it - a live store is a property of the run.

    `basemaps` are the raw `--basemap` strings, read through `basemaps_from_options`; stating
    none leaves the table's layers alone. A value that names nothing servable raises
    `ValueError`, which is the caller's to render - `d2w fhir serve` renders it against the
    flag the value came from.

    Two of `[serve]` have no dial at all and are carried across verbatim: `capture`, which says
    what this server offers rather than what one run does, and `spool_dir`, which says where the
    receipts a previous run wrote already live.

    The profile is resolved here, before anything is built, so a named profile that does not
    exist refuses the run rather than failing under a banner saying the server is starting. A
    machine that names no profile at all resolves to None and serves its compiled guide offline,
    which is a whole posture rather than a degraded one; `live` makes the profile required,
    since a live store has an instance to read. The address it resolved is what the screens link
    an identity out to, and it is the only part of the profile that reaches the settings - the
    name, the origin, and the credentials stay on the invocation, which never leaves the process
    that opened it.

    The compiled guide is checked last, and only when the store is the one on disk: a project
    that has never run SUSHI has nothing to serve, and refusing here says so in one line rather
    than answering every read with a 404.

    THE AUTH POSTURE IS PREFLIGHTED HERE TOO, and for the same reason every other refusal is: a
    server that starts and then cannot honour what it was asked for is a failure nobody reads
    until somebody meets it. Five refusals, in `dhis2w_fhir_serve.auth.preflight_auth`: an
    interface other than loopback while neither this run nor fhir.toml has stated a posture, the
    `token` posture with its environment variable unset, the `dhis2` posture on a run that reads
    a compiled guide and so has no instance to check anybody against, the `jwt` posture with no
    `[serve.jwt] issuer`, and `[serve.jwt] forward_bearer` on a run with no instance to forward
    to. Whether the posture was STATED is what the first of those turns on - `auth = "none"`
    written down is a decision, an absent key is not - so a stated flag counts as much as a
    stated table key, and only the two of them being silent is silence.

    The sixth thing that could stop a `jwt` run - an issuer this machine cannot reach - is a
    round trip, so it is not made here: `open_serve_runtime` reads the issuer while the server
    starts and refuses with the same error type. This function reads values.
    """
    serve_config = project.config.serve
    stated_basemaps = list(basemaps or [])
    # The refusals are ordered as a reader meets them: a value this run stated and cannot mean
    # is answered before a profile is looked up, and both before the guide on disk is counted.
    resolved_basemaps = basemaps_from_options(stated_basemaps) if stated_basemaps else list(serve_config.basemaps)
    resolved_host = host if host is not None else serve_config.host
    resolved_auth = auth if auth is not None else serve_config.auth
    preflight_auth(
        posture=resolved_auth if resolved_auth is not None else ServeAuth.NONE,
        host=resolved_host,
        live=live,
        stated=auth is not None or serve_config.auth is not None,
        jwt=serve_config.jwt,
    )
    generation = _resolved_generation(project, profile, required=live)
    if not live and not any((project.ig_directory / COMPILED_RESOURCES_RELATIVE_PATH).glob("*.json")):
        raise CompiledIgMissingError
    return ServeInvocation(
        settings=cls(
            project_dir=project.project_root,
            live=live,
            profile=profile,
            auth=resolved_auth if resolved_auth is not None else ServeAuth.NONE,
            auth_scope=auth_scope if auth_scope is not None else serve_config.auth_scope,
            jwt=serve_config.jwt,
            strict_codes=strict_codes if strict_codes is not None else serve_config.strict_codes,
            capture=serve_config.capture,
            ui=ui if ui is not None else serve_config.ui,
            spool_dir=serve_config.spool_dir,
            basemaps=resolved_basemaps,
            dhis2_base_url=None if generation is None else generation.profile.base_url,
            tracked_entities=serve_config.tracked_entities,
            data_sets=serve_config.data_sets,
            search=serve_config.search,
            projection=serve_config.projection,
        ),
        host=resolved_host,
        port=port if port is not None else serve_config.port,
        generation=generation,
    )

ServeInvocation

Bases: BaseModel

What one run of the facade resolved to: what it serves, where it listens, and the profile behind it.

The address is here rather than on ServeSettings because it is what the process binds and not what the app serves - the factory reads the settings and never asks where the socket is. The resolved profile is here for the sharper reason: it carries the credentials the store connects with, and the settings are handed to the app and read out again by /facade/uiconfig, so a profile on them would be a credential one HTTP response away from a browser. What the screens are entitled to - the instance's address - is on the settings as dhis2_base_url, and the name and the origin stay here, where the command that resolved them can say which profile it used.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/settings.py
class ServeInvocation(BaseModel):
    """What one run of the facade resolved to: what it serves, where it listens, and the profile behind it.

    The address is here rather than on `ServeSettings` because it is what the process binds and not
    what the app serves - the factory reads the settings and never asks where the socket is. The
    resolved profile is here for the sharper reason: it carries the credentials the store connects
    with, and the settings are handed to the app and read out again by `/facade/uiconfig`, so a profile on
    them would be a credential one HTTP response away from a browser. What the screens are entitled
    to - the instance's address - is on the settings as `dhis2_base_url`, and the name and the origin
    stay here, where the command that resolved them can say which profile it used.
    """

    model_config = ConfigDict(frozen=True)

    settings: ServeSettings
    host: str
    port: int
    generation: GenerationProfile | None = None

Functions:

The runtime

What one loaded facade holds - the project, its store, its spool, its register surface, the statement it answers /metadata with, and the DHIS2 client a live run reads through - as a value a caller can open without starting a server. The lifespan is its first caller.

runtime

What one running facade holds, as a value a caller can open without starting a server.

A facade is a project, the resources it serves, the spool it writes receipts into, the register it answers for, and - under --live - one DHIS2 client held open for the life of the process. Loading all of that is what the ASGI lifespan does, and it is the only thing the lifespan does: this module is those steps, in order, behind an async context manager, and dhis2w_fhir_serve.app is its first caller.

That split is what lets a test, a batch job, or an embedding application have a loaded store, a spool, and a register surface for a project without building an application it never intends to serve - and it is what lets an application that already runs FastAPI mount the facade's routers over a runtime it opened itself, which is the contract attach_serve_runtime states.

The client's lifetime is the context manager's: entering opens it when the settings say the store is built live, leaving closes it, and a caller that already holds an authenticated one hands it in and keeps ownership of it. It rides beside the served context rather than on it, for the reason dhis2w_fhir_serve.routes.context gives - a Pydantic model of what a facade serves is not the place for a live HTTP connection - and ServeRuntime is the name for the pair.

A live run that forwards credentials - [serve] auth = "dhis2", or "jwt" with [serve.jwt] forward_bearer = true - opens a second connection to the same instance, and this one carries no credential: it is the pool a register read sends the CALLER'S own Authorization over, so DHIS2 decides per caller what comes back. dhis2w_fhir_serve.passthrough states why, and opens it; the lifespan owns it exactly as it owns the client, so both close when the process unwinds.

A project holding a materialized projection opens it here too - [serve.projection] store names the backend, dhis2w_fhir_serve.projection.factory builds it, and this context manager owns it exactly as it owns the two HTTP connections, so the database closes when the process unwinds. It is opened whether or not [serve.search] reads it, because a projection is a thing this project holds rather than a thing one search does.

A run under [serve] auth = "jwt" also reads its issuer here, before anything is served: the discovery document and then the keys it names. That is a network read at startup and it is a deliberate one - an issuer this machine cannot reach is a posture this process cannot honour, and finding that out on a caller's behalf would mean a server that starts and 401s everybody for a reason none of them can act on. dhis2w_fhir_serve.oidc does the reading and auth.open_jwt_verifier turns a failure into the same refusal the other postures raise.

Classes

ServeContext

Bases: BaseModel

Everything one running facade serves: the project, its resources, its spool, its settings.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
class ServeContext(BaseModel):
    """Everything one running facade serves: the project, its resources, its spool, its settings."""

    model_config = ConfigDict(frozen=True)

    project: FhirProject
    store: ResourceStore
    spool: ResponseSpool
    settings: ServeSettings
    register_surface: RegisterSurface
    """What this process answers for: the published register, narrowed by `[serve.tracked_entities]`."""

    capability_body: dict[str, Any]
    """The `/metadata` document, pre-rendered - the same HTTP-boundary escape hatch `StoreEntry.body` documents."""
Attributes
register_surface instance-attribute

What this process answers for: the published register, narrowed by [serve.tracked_entities].

capability_body instance-attribute

The /metadata document, pre-rendered - the same HTTP-boundary escape hatch StoreEntry.body documents.

ServeRuntime

Bases: BaseModel

One loaded facade: everything it serves, and the DHIS2 client it holds open beside that.

The client is the reason this model allows arbitrary types where ServeContext does not. It is a live connection rather than a value, it is None in the default mode - which is the whole of what makes the register routes live-only - and naming the pair is what an application mounting the facade's routers has to be handed.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
class ServeRuntime(BaseModel):
    """One loaded facade: everything it serves, and the DHIS2 client it holds open beside that.

    The client is the reason this model allows arbitrary types where `ServeContext` does not. It is
    a live connection rather than a value, it is None in the default mode - which is the whole of
    what makes the register routes live-only - and naming the pair is what an application mounting
    the facade's routers has to be handed.
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    context: ServeContext
    live_client: Dhis2Client | None = None

    caller_client: httpx2.AsyncClient | None = None
    """The credential-free pool a register read forwards one caller's `Authorization` over.

    None in every mode but live, and in every posture that forwards nothing - `none`, `token`, and
    `jwt` with `[serve.jwt] forward_bearer` off. See `dhis2w_fhir_serve.passthrough`.
    """

    jwt_verifier: JwtVerifier | None = None
    """The `jwt` posture's issuer and its published keys, read while the server started.

    None in every other posture. It holds public keys and no secret, so unlike the client beside it
    there is nothing here a process would want to keep from itself.
    """

    projection_store: ProjectionStore | None = None
    """The materialized projection this project holds, or None where `[serve.projection]` names none.

    A database connection rather than a value, which is why it rides here beside the two HTTP ones
    rather than on `ServeContext`. Nothing in this process writes it - D2 makes `d2w fhir sync` the
    only writer - and every read of it states the cursor it was read at.
    """
Attributes
caller_client = None class-attribute instance-attribute

The credential-free pool a register read forwards one caller's Authorization over.

None in every mode but live, and in every posture that forwards nothing - none, token, and jwt with [serve.jwt] forward_bearer off. See dhis2w_fhir_serve.passthrough.

jwt_verifier = None class-attribute instance-attribute

The jwt posture's issuer and its published keys, read while the server started.

None in every other posture. It holds public keys and no secret, so unlike the client beside it there is nothing here a process would want to keep from itself.

projection_store = None class-attribute instance-attribute

The materialized projection this project holds, or None where [serve.projection] names none.

A database connection rather than a value, which is why it rides here beside the two HTTP ones rather than on ServeContext. Nothing in this process writes it - D2 makes d2w fhir sync the only writer - and every read of it states the cursor it was read at.

Functions:

open_serve_runtime(settings, *, client=None) async

Load one facade: the project, its store, its spool, its register, and the statement it answers /metadata with.

Entering opens the DHIS2 client a live run reads through and leaving closes it. A caller that already holds an authenticated client hands it in instead, and keeps it open afterwards - the runtime never closes a connection it did not open. Handing one to a run that is not live is refused rather than ignored: live is what says this facade reads an instance, and a client beside live = False is two statements that disagree.

A live run under a forwarding posture opens the credential-free pool beside it, which is what a register read forwards the caller's own header over. It is opened here, rather than lazily at the first request, because it is the connection that decides whose rights an answer is read under: a process that could not open it should refuse to serve rather than discover that on a caller's behalf. A run under the jwt posture reads its issuer here for the same reason - the discovery document and the keys it names, before a single request is taken.

Nothing here is retried and nothing is defaulted. CompiledIgMissingError from a project that has never been built, or an unreachable instance, propagates out and the server refuses to start, rather than serving an empty guide that reads to a client as a project that published nothing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
@asynccontextmanager
async def open_serve_runtime(
    settings: ServeSettings, *, client: Dhis2Client | None = None
) -> AsyncGenerator[ServeRuntime]:
    """Load one facade: the project, its store, its spool, its register, and the statement it answers `/metadata` with.

    Entering opens the DHIS2 client a live run reads through and leaving closes it. A caller that
    already holds an authenticated client hands it in instead, and keeps it open afterwards - the
    runtime never closes a connection it did not open. Handing one to a run that is not live is
    refused rather than ignored: `live` is what says this facade reads an instance, and a client
    beside `live = False` is two statements that disagree.

    A live run under a forwarding posture opens the credential-free pool beside it, which is what a
    register read forwards the caller's own header over. It is opened here, rather than lazily at
    the first request, because it is the connection that decides whose rights an answer is read
    under: a process that could not open it should refuse to serve rather than discover that on a
    caller's behalf. A run under the `jwt` posture reads its issuer here for the same reason - the
    discovery document and the keys it names, before a single request is taken.

    Nothing here is retried and nothing is defaulted. `CompiledIgMissingError` from a project that
    has never been built, or an unreachable instance, propagates out and the server refuses to
    start, rather than serving an empty guide that reads to a client as a project that published
    nothing.
    """
    if client is not None and not settings.live:
        raise ValueError("a DHIS2 client is only read through by a live run: build the settings with live=True")
    project = load_project(settings.project_dir)
    async with AsyncExitStack() as connections:
        live = client
        if settings.live and live is None:
            live = await connections.enter_async_context(open_live_client(project, settings))
        caller = None
        if live is not None and _forwards_credentials(settings) and settings.dhis2_base_url is not None:
            caller = await connections.enter_async_context(
                open_pass_through_client(settings.dhis2_base_url, provenance=facade_provenance())
            )
        verifier = await open_jwt_verifier(settings.jwt) if settings.auth is ServeAuth.JWT else None
        projection = await connections.enter_async_context(
            open_projection_store(settings.projection, project_root=project.project_root)
        )
        store = await build_store(settings, project, live)
        spool = ResponseSpool.at(project.project_root, settings.spool_dir)
        register_surface = RegisterSurface.resolve(
            TrackedEntityIndex.from_store(project, store), settings.tracked_entities
        )
        yield ServeRuntime(
            context=ServeContext(
                project=project,
                store=store,
                spool=spool,
                settings=settings,
                register_surface=register_surface,
                capability_body=build_metadata_body(
                    project=project,
                    store_summary=store.summary(),
                    settings=settings,
                    register_surface=register_surface,
                    server_version=server_version(),
                ),
            ),
            live_client=live,
            caller_client=caller,
            jwt_verifier=verifier,
            projection_store=projection,
        )

attach_serve_runtime(app, runtime)

Put one loaded runtime where every route handler reads it from, before the first request.

This is the whole of what an application mounting the facade's routers must promise them. The state is the application's rather than the request's because one project, one store, and one spool are properties of the process. Four of the five names it writes are the four dhis2w_fhir_serve.routes.context reads back; the fifth is the JWT verifier, which dhis2w_fhir_serve.auth reads because it is the only thing that has a use for it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
def attach_serve_runtime(app: FastAPI, runtime: ServeRuntime) -> None:
    """Put one loaded runtime where every route handler reads it from, before the first request.

    This is the whole of what an application mounting the facade's routers must promise them. The
    state is the application's rather than the request's because one project, one store, and one
    spool are properties of the process. Four of the five names it writes are the four
    `dhis2w_fhir_serve.routes.context` reads back; the fifth is the JWT verifier, which
    `dhis2w_fhir_serve.auth` reads because it is the only thing that has a use for it.
    """
    setattr(app.state, SERVE_CONTEXT_ATTRIBUTE, runtime.context)
    setattr(app.state, LIVE_CLIENT_ATTRIBUTE, runtime.live_client)
    setattr(app.state, CALLER_CLIENT_ATTRIBUTE, runtime.caller_client)
    setattr(app.state, JWT_VERIFIER_ATTRIBUTE, runtime.jwt_verifier)
    setattr(app.state, PROJECTION_STORE_ATTRIBUTE, runtime.projection_store)

build_store(settings, project, client) async

Select the store the facade serves from: the compiled IG on disk, or one built live from DHIS2.

client is the connection a live run holds open, and None in the default mode - the two are the same fact stated once, which is why the caller opens it rather than this function.

Both paths are deliberate about failing loudly, and neither is retried: CompiledIgMissingError or an unreachable instance propagates out of the lifespan and the server refuses to start, rather than serving an empty IG that reads to a client as a project that published nothing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
async def build_store(settings: ServeSettings, project: FhirProject, client: Dhis2Client | None) -> ResourceStore:
    """Select the store the facade serves from: the compiled IG on disk, or one built live from DHIS2.

    `client` is the connection a live run holds open, and None in the default mode - the two are the
    same fact stated once, which is why the caller opens it rather than this function.

    Both paths are deliberate about failing loudly, and neither is retried: `CompiledIgMissingError`
    or an unreachable instance propagates out of the lifespan and the server refuses to start,
    rather than serving an empty IG that reads to a client as a project that published nothing.
    """
    if client is not None:
        return attach_builtin_conformance(await build_live_store(project, settings, client))
    return attach_builtin_conformance(load_compiled_store(project))

open_projection_store(config, *, project_root) async

Open the projection this project holds, closing it on the way out, or hold none where it names none.

Nothing is read while it opens and nothing is refused: a projection nobody has synced into is a valid state of a valid file, and the surface that would answer out of it says so per request rather than stopping the server. That keeps R11 true - a facade configured with a projection is not a facade that fails differently from one without.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
@asynccontextmanager
async def open_projection_store(
    config: ProjectionConfig, *, project_root: Path
) -> AsyncGenerator[ProjectionStore | None]:
    """Open the projection this project holds, closing it on the way out, or hold none where it names none.

    Nothing is read while it opens and nothing is refused: a projection nobody has synced into is a
    valid state of a valid file, and the surface that would answer out of it says so per request
    rather than stopping the server. That keeps R11 true - a facade configured with a projection is
    not a facade that fails differently from one without.
    """
    store = build_projection_store(config, project_root=project_root)
    if store is None:
        yield None
        return
    try:
        yield store
    finally:
        await _closed(store)

server_version()

The installed version of this package, as the app and its CapabilityStatement report it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
def server_version() -> str:
    """The installed version of this package, as the app and its CapabilityStatement report it."""
    return version(DISTRIBUTION_NAME)

facade_provenance()

What a pass-through read names itself as to the instance: this software, and its version.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/runtime.py
def facade_provenance() -> str:
    """What a pass-through read names itself as to the instance: this software, and its version."""
    return f"{DISTRIBUTION_NAME}/{server_version()}"

Application

The app factory and the lifespan that opens the runtime and attaches it.

app

The FastAPI application: what the process loads at startup, and what every route reads from.

The factory takes settings and returns an app; everything the app serves is loaded once in the lifespan and held on app.state.context. That is what makes the facade cheap - the store is parsed once, the CapabilityStatement is rendered once, and a request does index lookups and nothing else. What the lifespan loads is dhis2w_fhir_serve.runtime's to say: this module opens one runtime, attaches it, and says in one log line what was loaded.

The spool is the one exception, and deliberately so: it is a path, not a loaded index, and every read of it re-reads the directory. d2w fhir forward runs as a separate process and moves receipt files between the spool's three states while this server is up, so anything cached here would go stale within seconds of a drain. See dhis2w_fhir_serve.spool.

The default mode is fully offline: a compiled IG on disk is the whole world, and no DHIS2 client is constructed anywhere in this module. --live swaps the store for one built from a DHIS2 instance, and keeps the client that built it open for the life of the process, because the register routes answer from the instance per request rather than from anything loaded here. That client is the only thing in a running facade that is neither the store nor the spool, and it lives on app.state rather than on the context for the reason dhis2w_fhir_serve.routes.context states. It is closed when the lifespan unwinds; the default mode never opens one, which is what makes the register routes live-only.

Classes

Functions:

create_app(settings)

Build the FHIR facade for one project, loading nothing until the lifespan runs.

THE APPLICATION THIS RETURNS PUBLISHES NO OPENAPI DOCUMENT, and that is about the base URL rather than about the process. The base URL is a FHIR endpoint, its contract is the CapabilityStatement at /metadata, and the two routes it mostly serves are catch-alls over application/fhir+json bodies that an OpenAPI document could only misdescribe as untyped JSON on a path variable.

The facade's OWN API - the receipts, the settings, the caller, the evaluator, the vocabularies, the register listings - is a different contract and gets a document of its own: register_routes mounts it as an application at /facade, with its OpenAPI at /facade/openapi.json and the interactive page at /facade/docs. The mounted application shares this one's state, so the runtime the lifespan attaches here is the runtime its handlers read. dhis2w_fhir_serve.routes states the whole arrangement.

settings.ui adds the built capture UI as a static mount at /, after every FHIR route. A missing bundle raises here, while the app is being built, so --ui on a checkout that has never built the frontend fails as one line rather than as a white page on the first request.

settings.capture decides which router claims POST /QuestionnaireResponse - the create route, or the refusal that names the key. It is settled here, at build time, because it is what this server offers rather than something a request could be judged against.

settings.auth and settings.auth_scope decide which routers carry the authentication check, which is settled at build time for the same reason. What the check then DOES is a request-time question and lives in dhis2w_fhir_serve.auth; an application embedding the facade mounts its own dependency over the same set. Nothing secret reaches this factory: the tokens a token posture compares against are read from the environment by the check itself, never from the settings.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/app.py
def create_app(settings: ServeSettings) -> FastAPI:
    """Build the FHIR facade for one project, loading nothing until the lifespan runs.

    THE APPLICATION THIS RETURNS PUBLISHES NO OPENAPI DOCUMENT, and that is about the base URL rather
    than about the process. The base URL is a FHIR endpoint, its contract is the CapabilityStatement
    at `/metadata`, and the two routes it mostly serves are catch-alls over `application/fhir+json`
    bodies that an OpenAPI document could only misdescribe as untyped JSON on a path variable.

    The facade's OWN API - the receipts, the settings, the caller, the evaluator, the vocabularies,
    the register listings - is a different contract and gets a document of its own: `register_routes`
    mounts it as an application at `/facade`, with its OpenAPI at `/facade/openapi.json` and the
    interactive page at `/facade/docs`. The mounted application shares this one's `state`, so the
    runtime the lifespan attaches here is the runtime its handlers read. `dhis2w_fhir_serve.routes`
    states the whole arrangement.

    `settings.ui` adds the built capture UI as a static mount at `/`, after every FHIR route.
    A missing bundle raises here, while the app is being built, so `--ui` on a checkout that has
    never built the frontend fails as one line rather than as a white page on the first request.

    `settings.capture` decides which router claims `POST /QuestionnaireResponse` - the create route,
    or the refusal that names the key. It is settled here, at build time, because it is what this
    server offers rather than something a request could be judged against.

    `settings.auth` and `settings.auth_scope` decide which routers carry the authentication check,
    which is settled at build time for the same reason. What the check then DOES is a request-time
    question and lives in `dhis2w_fhir_serve.auth`; an application embedding the facade mounts its own
    dependency over the same set. Nothing secret reaches this factory: the tokens a `token` posture
    compares against are read from the environment by the check itself, never from the settings.
    """
    app = FastAPI(
        title=APPLICATION_TITLE,
        version=server_version(),
        docs_url=None,
        redoc_url=None,
        openapi_url=None,
        lifespan=_lifespan,
    )
    app.state.settings = settings
    app.add_middleware(RequestLogMiddleware)
    register_error_handlers(app)
    register_routes(
        app,
        serve_ui=settings.ui,
        capture=settings.capture,
        auth=settings.auth,
        auth_scope=settings.auth_scope,
    )
    return app

The routers

Every router the facade mounts, with what mounting it requires stated as data: which carry the Accept negotiation, which answer plain JSON about the facade rather than FHIR resources out of it, which claim every path of their shape and therefore mount last, and where the runtime state the handlers read is written and read back.

Two surfaces, two addresses. The base URL is FHIR's and /metadata is its contract; the routers that answer about the facade itself are mounted as an application of their own at FACADE_MOUNT_PATH (/facade), which publishes its own OpenAPI document at /facade/openapi.json and an interactive page at /facade/docs. Both stay readable under every [serve] auth_scope, for the reason /metadata does. /cds-services is the third family and stays at the base URL, because CDS Hooks fixes its discovery path there. An application embedding this facade beside its own routes gets the order, the split, and the guarded set as values from serve_routers, and mounts the facade API under whichever prefix it prefers.

routes

Route assembly: every router the facade mounts, where each group mounts, and in what order.

TWO SURFACES, TWO ADDRESSES. The base URL is FHIR's. /metadata is its contract, the read catch-alls answer resource types out of the store, and every path a FHIR client is entitled to guess at is where the specification says it is. Everything this facade answers ABOUT ITSELF - the receipts it holds, the settings it was started with, who it just decided the caller is, the expression evaluator, the vocabularies it publishes, the register listings it reads from the instance - is a different API with a different contract, and it is served under /facade as an application of its own with its own OpenAPI document at /facade/openapi.json. dhis2w_fhir_serve.routes.spool argues why those answers are not FHIR; this module is where that argument becomes an address.

/cds-services is the third family and it is at the root beside FHIR, because CDS Hooks fixes the discovery path at {base}/cds-services exactly as FHIR fixes {base}/metadata. It answers plain JSON and it is not this facade's own API - it is somebody else's specification implemented here, and a specification's path is not ours to move. dhis2w_fhir_serve.routes.cds is the implementation.

WHAT MOUNTS BEFORE WHAT, AT THE ROOT. /{resource_type} and /{resource_type}/{resource_id} match any path of their shape, so the read router mounts last and every router carrying a fixed path mounts ahead of it. /metadata, /cds-services, and the /facade mount itself are one-segment fixed paths and all sit in that group - as is /cds-services/{id} one level down. FHIR resource types are PascalCase, so a lowercase segment can never be one the read router should have claimed, whichever depth it sits at.

$evaluate is a one-segment fixed path too, and a FHIR one: it is the system-level operation answering an evaluation as a Parameters resource, so it mounts with the FHIR group while POST /facade/evaluate answers the same evaluation as this facade's own JSON. A segment beginning with $ can shadow no resource type either, and both addresses run the same evaluation over the same three contexts - dhis2w_fhir_serve.routes.evaluate_operation argues the split.

$summary is the one FHIR path whose fixed segment sits at the END rather than the start, and it mounts in that same group for exactly that reason: /{ResourceType}/$summary is the shape /{resource_type}/{resource_id} also matches, so a summary router mounted after the read catch-alls would never be reached and a client asking for one would be told there is no resource with the id $summary. dhis2w_fhir_serve.routes.summary is what it answers.

THE INSTANCE-SOURCED READS ARE THE FACADE'S, ALL THREE OF THEM. /facade/tracked-entities/{uid}/enrollments lists what one entity is enrolled in, /facade/tracked-entities/{uid}/events is that entity's record, and /facade/data-sets/{uid}/responses is what the instance holds for one data set - and none of them is a FHIR interaction: the CapabilityStatement names them in prose and declares none, because FHIR has no interaction at any of those addresses. The record and the data set responses answer application/fhir+json all the same - a Bundle of QuestionnaireResponses is a FHIR document however it was asked for - which is why they carry the Accept negotiation as their own mount-time requirement rather than as the group's. ServeRouters.negotiated is that requirement stated as data.

/facade/whoami is the one path whose ANSWER a posture decides rather than a scope guarding it. Every other route is mounted by every run and the posture only decides which of them carry the check; that one answers who the caller is, so under auth = "none" there is nobody to answer about and the address is mounted as the refusal that says so. dhis2w_fhir_serve.routes.whoami argues it.

The register's resource types are the exception that needs no mount of its own. They are FHIR resource types answered from the DHIS2 instance rather than from the store, but which types they are is a property of the guide this process loaded, so the read router dispatches to dhis2w_fhir_serve.routes.register at request time instead of a router claiming paths that could only be named once the store was open.

capture picks which router claims POST /QuestionnaireResponse: the create route, or the refusal that names [serve] capture = false. One of the two is always mounted, so the address never falls through to the read catch-all - which would answer the same 405 without saying why.

The capture UI sits on both sides of that line, which is why serve_ui is an argument here rather than something the UI module could arrange for itself. Its asset tree is a fixed path and mounts with the other fixed paths, ahead of the catch-alls that would otherwise claim /assets/<file>; its shell is a catch-all of its own and mounts after everything. See dhis2w_fhir_serve.ui for what each mount is and why the split is not optional.

Every GET route also answers HEAD. RFC 9110 defines HEAD as GET without the body, and FHIR liveness probes lean on that - a monitor asking HEAD /metadata is asking whether the server is up, and a 405 there reads as down. FastAPI registers only the methods a decorator names, so the parity is applied here in one sweep over each router as it is mounted rather than repeated (and one day forgotten) on every route. The UI mounts need no sweep: StaticFiles answers HEAD itself.

WHICH ROUTERS ARE BEHIND THE AUTHENTICATION CHECK is decided here as well, and stated the same way: ServeRouters.guarded names them, and [serve] auth_scope is the whole of what decides the set. write guards the state-changing surface, which is one route - POST /QuestionnaireResponse, the create. Every other POST this facade serves writes nothing: $generate reads a published form and answers with a draft, POST /facade/evaluate and $evaluate run an expression over what is served, a CDS Hooks call answers cards, and POST / is a refusal on every posture. all guards everything except /metadata, which stays open because a client has to be able to read the posture it is expected to meet - a server that refuses to say how to authenticate to it is one nobody can authenticate to. /facade/openapi.json and the documentation page beside it stay open under both scopes for exactly that reason: they are the facade API's own /metadata, and a contract nobody may read is a contract nobody can meet. Neither carries a credential in either direction, and neither says anything a request to /metadata does not. The UI mounts stay open under both, because a sign-in prompt has to be servable. /facade/whoami is guarded under both scopes, because a credential check that answered without checking would be no check at all.

All of that is stated as data by serve_routers, so an application mounting the facade beside its own routes gets the order, the split, the capture choice, and the guarded set as values rather than as four paragraphs it has to read. register_routes is a loop over what that function answers, and holds no router knowledge of its own - including the check itself, which is one dependency an embedding application is free to replace with its own over the same guarded set.

Classes

ServeRouters

Bases: BaseModel

Every router one facade mounts, grouped by what mounting it requires.

The four fields are four different requirements, not a taxonomy: fhir mounts at the base URL under Depends(require_json_is_acceptable), cds_hooks mounts at the base URL without it, facade mounts under a prefix of the mounting application's choosing - /facade is where this package's own factory puts it - and read mounts at the base URL after every fixed path an application serves, its own included, since /{resource_type} claims any one-segment path. Every one of them wants the HEAD sweep.

THE GROUPS ARE WHAT AN EMBEDDER PICKS FROM, and the picking is the point of stating them as data. An application that wants FHIR out of DHIS2 and intends to operate itself mounts fhir and read and stops: what it gets serves /metadata, the reads, the searches, and the capture, and not one operational endpoint - no receipts listing, no settings document, no caller check, no evaluator, no vocabularies, no register listings. An application that wants the controls hands this value to build_facade_api and mounts what comes back, at /facade or at a prefix of its own, and gets an OpenAPI contract describing exactly what it mounted. register_routes is both choices made the way this package's factory makes them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
class ServeRouters(BaseModel):
    """Every router one facade mounts, grouped by what mounting it requires.

    The four fields are four different requirements, not a taxonomy: `fhir` mounts at the base URL
    under `Depends(require_json_is_acceptable)`, `cds_hooks` mounts at the base URL without it,
    `facade` mounts under a prefix of the mounting application's choosing - `/facade` is where this
    package's own factory puts it - and `read` mounts at the base URL after every fixed path an
    application serves, its own included, since `/{resource_type}` claims any one-segment path.
    Every one of them wants the HEAD sweep.

    THE GROUPS ARE WHAT AN EMBEDDER PICKS FROM, and the picking is the point of stating them as
    data. An application that wants FHIR out of DHIS2 and intends to operate itself mounts `fhir`
    and `read` and stops: what it gets serves `/metadata`, the reads, the searches, and the capture,
    and not one operational endpoint - no receipts listing, no settings document, no caller check,
    no evaluator, no vocabularies, no register listings. An application that wants the controls
    hands this value to `build_facade_api` and mounts what comes back, at `/facade` or at a prefix
    of its own, and gets an OpenAPI contract describing exactly what it mounted.
    `register_routes` is both choices made the way this package's factory makes them.
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    fhir: tuple[APIRouter, ...]
    """The FHIR surface: what a client that takes no JSON is refused before, at mount time."""

    cds_hooks: tuple[APIRouter, ...]
    """The CDS Hooks discovery document and the one service behind it, whose path the specification fixes.

    At the base URL rather than under the facade mount, and not because it is FHIR: CDS Hooks defines
    discovery at `{base}/cds-services`, so an EHR configured with this server's base URL asks for
    that path and no other. It answers plain JSON, so it carries no `Accept` negotiation.
    """

    facade: tuple[APIRouter, ...]
    """The routers answering about this facade rather than serving FHIR resources out of it.

    Mounted under one prefix as an application of its own, which is what gives them an OpenAPI
    document of their own. Their order is the order a reader meets them in: who is calling
    (`/whoami`), what a running server holds (`/spool`, `/uiconfig`, `/metadata-health`), what it
    answers about the instance (`/tracked-entities/{uid}/enrollments`, the record beside it, and one
    data set's own responses), and what it runs over either (`/evaluate`, `/terminology/*`).
    """

    read: APIRouter
    """The catch-alls, named on their own because they mount last."""

    guarded: tuple[APIRouter, ...] = ()
    """The routers the authentication check belongs on, as objects also present in the fields above.

    A subset rather than a fifth group: a router is mounted once, in the group whose requirement it
    carries, and this names which of those mounts additionally take `Depends(require_authenticated)`.
    Empty under `[serve] auth = "none"`, which is what makes the default posture cost a request
    nothing. An application that authenticates its callers its own way mounts its own dependency over
    exactly this set.
    """

    negotiated: tuple[APIRouter, ...] = ()
    """The routers outside the FHIR group that carry the `Accept` negotiation anyway.

    A subset for the same reason `guarded` is one, and today it names two routers: the tracked entity
    record and one data set's own responses. Both are the facade's own addresses - FHIR declares no
    interaction at either - and what both answer is a FHIR Bundle, so a client that takes no JSON is
    refused before it runs exactly as it is on the FHIR surface. Every other router in `facade`
    answers `application/json` and negotiates nothing.
    """

    def is_guarded(self, router: APIRouter) -> bool:
        """Whether one router carries the authentication check, compared by identity rather than by path."""
        return any(router is guarded for guarded in self.guarded)

    def is_negotiated(self, router: APIRouter) -> bool:
        """Whether one router outside the FHIR group carries the `Accept` negotiation as well."""
        return any(router is negotiated for negotiated in self.negotiated)

    def in_mount_order(self) -> tuple[APIRouter, ...]:
        """Every router, in the order a route table has to see them: fixed paths first, catch-alls last."""
        return (*self.fhir, *self.cds_hooks, *self.facade, self.read)
Attributes
fhir instance-attribute

The FHIR surface: what a client that takes no JSON is refused before, at mount time.

cds_hooks instance-attribute

The CDS Hooks discovery document and the one service behind it, whose path the specification fixes.

At the base URL rather than under the facade mount, and not because it is FHIR: CDS Hooks defines discovery at {base}/cds-services, so an EHR configured with this server's base URL asks for that path and no other. It answers plain JSON, so it carries no Accept negotiation.

facade instance-attribute

The routers answering about this facade rather than serving FHIR resources out of it.

Mounted under one prefix as an application of its own, which is what gives them an OpenAPI document of their own. Their order is the order a reader meets them in: who is calling (/whoami), what a running server holds (/spool, /uiconfig, /metadata-health), what it answers about the instance (/tracked-entities/{uid}/enrollments, the record beside it, and one data set's own responses), and what it runs over either (/evaluate, /terminology/*).

read instance-attribute

The catch-alls, named on their own because they mount last.

guarded = () class-attribute instance-attribute

The routers the authentication check belongs on, as objects also present in the fields above.

A subset rather than a fifth group: a router is mounted once, in the group whose requirement it carries, and this names which of those mounts additionally take Depends(require_authenticated). Empty under [serve] auth = "none", which is what makes the default posture cost a request nothing. An application that authenticates its callers its own way mounts its own dependency over exactly this set.

negotiated = () class-attribute instance-attribute

The routers outside the FHIR group that carry the Accept negotiation anyway.

A subset for the same reason guarded is one, and today it names two routers: the tracked entity record and one data set's own responses. Both are the facade's own addresses - FHIR declares no interaction at either - and what both answer is a FHIR Bundle, so a client that takes no JSON is refused before it runs exactly as it is on the FHIR surface. Every other router in facade answers application/json and negotiates nothing.

Methods:
is_guarded(router)

Whether one router carries the authentication check, compared by identity rather than by path.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def is_guarded(self, router: APIRouter) -> bool:
    """Whether one router carries the authentication check, compared by identity rather than by path."""
    return any(router is guarded for guarded in self.guarded)
is_negotiated(router)

Whether one router outside the FHIR group carries the Accept negotiation as well.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def is_negotiated(self, router: APIRouter) -> bool:
    """Whether one router outside the FHIR group carries the `Accept` negotiation as well."""
    return any(router is negotiated for negotiated in self.negotiated)
in_mount_order()

Every router, in the order a route table has to see them: fixed paths first, catch-alls last.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def in_mount_order(self) -> tuple[APIRouter, ...]:
    """Every router, in the order a route table has to see them: fixed paths first, catch-alls last."""
    return (*self.fhir, *self.cds_hooks, *self.facade, self.read)

Functions:

serve_routers(*, capture=True, serve_ui=False, auth=ServeAuth.NONE, auth_scope=ServeAuthScope.WRITE)

The facade's routers for one posture, with what mounting each group requires stated as data.

capture picks which router claims POST /QuestionnaireResponse - the create route, or the refusal that names [serve] capture = false. serve_ui decides whether the service base router claims GET /: with the capture UI mounted, the shell serves it instead, and a router claiming the path in order to refuse it would take it away from the mount. Neither is a request-time question, which is why both are settled here.

auth also picks which /whoami is mounted - the one that names the caller a check established, or the one that refuses under none because no check ran. auth and auth_scope fill guarded. none guards nothing. write guards the create route and only the create route, and guards nothing at all on a server that receives nothing - putting a 405 behind a credential would answer "who are you" where the honest answer is "this server takes no submissions". all guards every router but /metadata.

The routers are imported inside this function rather than at module scope: a route module reaches the serve context through dhis2w_fhir_serve.routes.context, which imports this package, so importing the route modules from this package's body would close the cycle.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def serve_routers(
    *,
    capture: bool = True,
    serve_ui: bool = False,
    auth: ServeAuth = ServeAuth.NONE,
    auth_scope: ServeAuthScope = ServeAuthScope.WRITE,
) -> ServeRouters:
    """The facade's routers for one posture, with what mounting each group requires stated as data.

    `capture` picks which router claims `POST /QuestionnaireResponse` - the create route, or the
    refusal that names `[serve] capture = false`. `serve_ui` decides whether the service base router
    claims `GET /`: with the capture UI mounted, the shell serves it instead, and a router claiming
    the path in order to refuse it would take it away from the mount. Neither is a request-time
    question, which is why both are settled here.

    `auth` also picks which `/whoami` is mounted - the one that names the caller a check established,
    or the one that refuses under `none` because no check ran. `auth` and `auth_scope` fill
    `guarded`. `none` guards nothing. `write` guards the create route
    and only the create route, and guards nothing at all on a server that receives nothing - putting
    a 405 behind a credential would answer "who are you" where the honest answer is "this server
    takes no submissions". `all` guards every router but `/metadata`.

    The routers are imported inside this function rather than at module scope: a route module reaches
    the serve context through `dhis2w_fhir_serve.routes.context`, which imports this package, so
    importing the route modules from this package's body would close the cycle.
    """
    from dhis2w_fhir_serve.metadata import router as metadata_router
    from dhis2w_fhir_serve.routes.capture import refusal_router as capture_refusal_router
    from dhis2w_fhir_serve.routes.capture import router as capture_router
    from dhis2w_fhir_serve.routes.cds import router as cds_router
    from dhis2w_fhir_serve.routes.data_sets import router as data_sets_router
    from dhis2w_fhir_serve.routes.enrollments import router as enrollments_router
    from dhis2w_fhir_serve.routes.evaluate import router as evaluate_router
    from dhis2w_fhir_serve.routes.evaluate_operation import router as evaluate_operation_router
    from dhis2w_fhir_serve.routes.generate import router as generate_router
    from dhis2w_fhir_serve.routes.history import router as history_router
    from dhis2w_fhir_serve.routes.metadata_health import router as metadata_health_router
    from dhis2w_fhir_serve.routes.read import router as read_router
    from dhis2w_fhir_serve.routes.root import build_root_router
    from dhis2w_fhir_serve.routes.spool import router as spool_router
    from dhis2w_fhir_serve.routes.summary import router as summary_router
    from dhis2w_fhir_serve.routes.terminology import router as terminology_router
    from dhis2w_fhir_serve.routes.translate import router as translate_router
    from dhis2w_fhir_serve.routes.uiconfig import router as ui_config_router
    from dhis2w_fhir_serve.routes.whoami import refusal_router as whoami_refusal_router
    from dhis2w_fhir_serve.routes.whoami import router as whoami_router

    submissions = capture_router if capture else capture_refusal_router
    fhir = (
        metadata_router,
        submissions,
        build_root_router(serve_ui),
        evaluate_operation_router,
        translate_router,
        generate_router,
        summary_router,
    )
    cds_hooks = (cds_router,)
    # `/whoami` names a caller only where a posture does: a server that checks nobody has nobody to
    # name, and under `none` the address is mounted as the refusal that says so - left unmounted it
    # would answer the facade mount's own 404 rather than the sentence that says which posture is
    # missing. It leads the group because it is the one router that answers about the caller rather
    # than about what is served. See `dhis2w_fhir_serve.routes.whoami`.
    naming = (whoami_router,) if auth is not ServeAuth.NONE else (whoami_refusal_router,)
    facade = (
        *naming,
        spool_router,
        ui_config_router,
        metadata_health_router,
        enrollments_router,
        history_router,
        data_sets_router,
        evaluate_router,
        terminology_router,
    )
    return ServeRouters(
        fhir=fhir,
        cds_hooks=cds_hooks,
        facade=facade,
        read=read_router,
        negotiated=(history_router, data_sets_router),
        guarded=_guarded_routers(
            auth=auth,
            auth_scope=auth_scope,
            capture=capture,
            submissions=submissions,
            conformance=metadata_router,
            naming=naming,
            fhir=fhir,
            cds_hooks=cds_hooks,
            facade=facade,
            read=read_router,
        ),
    )

build_facade_api(routers, *, authentication, state=None, mount_path=FACADE_MOUNT_PATH)

Build this facade's own API as an application of its own, so it can publish its own contract.

A sub-application rather than a prefix on a router, and the OpenAPI document is the whole reason: the base URL's application publishes none - its contract is the CapabilityStatement, and an OpenAPI document of the FHIR surface could only describe two catch-alls over a path variable - so the facade API gets an application whose document describes exactly the operations in it.

mount_path is where the mounting application puts it, and it is stated in the document's own servers rather than left for a reader to work out: the paths in an OpenAPI document are relative to the server it names, so a document that named none would describe /spool at a URL this process answers nothing at.

state is the state object the mounting application holds its runtime on. A mounted application is what request.app resolves to inside it, so the two share one State rather than copying values between them: attach_serve_runtime writes the runtime once, and both applications read the same four names back. Passing None leaves the sub-application its own state, which is what a caller mounting these routers without this package's factory arranges for itself.

The error handlers are registered here as well as on the mounting application. Starlette's exception middleware is per-application, so an exception raised inside this mount never reaches the handlers outside it - and a NotFoundError that answered a bare 500 instead of an OperationOutcome would make refusals under this mount a different shape from refusals beside it.

THE CONTRACT ITSELF IS OPEN IN EVERY SCOPE. /openapi.json and the documentation page are this application's own routes rather than routes on any router in guarded, so no posture puts a credential in front of them - deliberately, and for /metadata's reason: a description of how to call a server says nothing a caller could not learn by calling it, and one nobody may read is one nobody can meet.

THE DOCUMENTATION PAGE IS THE ONE THING HERE THAT REACHES ANOTHER ORIGIN. It is FastAPI's Swagger UI, whose script and stylesheet come from a public CDN, so a machine with no route out serves the page and renders nothing in it. The document itself is this server's own bytes and needs nobody: a deployment behind a closed network reads /openapi.json and opens it in whatever it already has. /docs is the convenience, not the contract.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def build_facade_api(
    routers: ServeRouters, *, authentication: Any, state: Any = None, mount_path: str = FACADE_MOUNT_PATH
) -> FastAPI:
    """Build this facade's own API as an application of its own, so it can publish its own contract.

    A sub-application rather than a prefix on a router, and the OpenAPI document is the whole reason:
    the base URL's application publishes none - its contract is the CapabilityStatement, and an
    OpenAPI document of the FHIR surface could only describe two catch-alls over a path variable - so
    the facade API gets an application whose document describes exactly the operations in it.

    `mount_path` is where the mounting application puts it, and it is stated in the document's own
    `servers` rather than left for a reader to work out: the paths in an OpenAPI document are
    relative to the server it names, so a document that named none would describe `/spool` at a URL
    this process answers nothing at.

    `state` is the state object the mounting application holds its runtime on. A mounted application
    is what `request.app` resolves to inside it, so the two share one `State` rather than copying
    values between them: `attach_serve_runtime` writes the runtime once, and both applications read
    the same four names back. Passing None leaves the sub-application its own state, which is what a
    caller mounting these routers without this package's factory arranges for itself.

    The error handlers are registered here as well as on the mounting application. Starlette's
    exception middleware is per-application, so an exception raised inside this mount never reaches
    the handlers outside it - and a `NotFoundError` that answered a bare 500 instead of an
    `OperationOutcome` would make refusals under this mount a different shape from refusals beside it.

    THE CONTRACT ITSELF IS OPEN IN EVERY SCOPE. `/openapi.json` and the documentation page are this
    application's own routes rather than routes on any router in `guarded`, so no posture puts a
    credential in front of them - deliberately, and for `/metadata`'s reason: a description of how to
    call a server says nothing a caller could not learn by calling it, and one nobody may read is one
    nobody can meet.

    THE DOCUMENTATION PAGE IS THE ONE THING HERE THAT REACHES ANOTHER ORIGIN. It is FastAPI's Swagger
    UI, whose script and stylesheet come from a public CDN, so a machine with no route out serves the
    page and renders nothing in it. The document itself is this server's own bytes and needs nobody:
    a deployment behind a closed network reads `/openapi.json` and opens it in whatever it already
    has. `/docs` is the convenience, not the contract.
    """
    from dhis2w_fhir_serve.errors import register_error_handlers
    from dhis2w_fhir_serve.runtime import server_version

    api = FastAPI(
        title=FACADE_API_TITLE,
        summary=FACADE_API_SUMMARY,
        description=FACADE_API_DESCRIPTION,
        version=server_version(),
        openapi_tags=FACADE_API_TAGS,
        servers=[{"url": mount_path, "description": "This facade's own API, beside the FHIR base URL."}],
        openapi_url=FACADE_OPENAPI_PATH,
        docs_url=FACADE_DOCUMENTATION_PATH,
        redoc_url=None,
        generate_unique_id_function=facade_operation_id,
    )
    if state is not None:
        api.state = state
    register_error_handlers(api)
    for router in routers.facade:
        guard = [Depends(authentication)] if routers.is_guarded(router) else []
        negotiation = [Depends(require_json_is_acceptable)] if routers.is_negotiated(router) else []
        api.include_router(router, dependencies=[*guard, *negotiation])
    describe_each_read_once(api)
    return api

api_routes(app)

Every API route one application answers, in table order, however deeply a router nests them.

include_router does not flatten what it includes: an application's own routes holds one object per inclusion and the routes are inside it, and a mounted application holds its own table the same way. Anything reading a route table - a test comparing two applications, a debug dump - has to walk through those rather than over them, so the walk is written once here. The paths are each router's own, so a route under a mount reads as the mount serves it rather than as the base URL does.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def api_routes(app: FastAPI) -> tuple[APIRoute, ...]:
    """Every API route one application answers, in table order, however deeply a router nests them.

    `include_router` does not flatten what it includes: an application's own `routes` holds one
    object per inclusion and the routes are inside it, and a mounted application holds its own table
    the same way. Anything reading a route table - a test comparing two applications, a debug dump -
    has to walk through those rather than over them, so the walk is written once here. The paths are
    each router's own, so a route under a mount reads as the mount serves it rather than as the base
    URL does.
    """
    found: list[APIRoute] = []
    _collect_api_routes(app.routes, found)
    return tuple(found)

facade_operation_id(route)

What one operation is called in the facade API's document, which is the handler's own name.

FastAPI's default composes the name, the path, and the method into one identifier, which reads as machinery in a document a person opens. The handler names here are already the sentence - a generated client calling read_spool() needs no more.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def facade_operation_id(route: APIRoute) -> str:
    """What one operation is called in the facade API's document, which is the handler's own name.

    FastAPI's default composes the name, the path, and the method into one identifier, which reads as
    machinery in a document a person opens. The handler names here are already the sentence - a
    generated client calling `read_spool()` needs no more.
    """
    return route.name

describe_each_read_once(api)

Render the facade API's document now, describing every read once rather than twice.

accept_head_wherever_get_is_served gives every GET route a HEAD twin so a liveness probe is answered, and FastAPI describes a route once per method - so a document built over those routes carries a HEAD twin of every read: the same operation, under the same identifier, said again with no body. HEAD is GET without the body and there is nothing about it a reader of a contract needs, so the twins come out of the document here.

That is also the whole of what the duplicate-identifier warning below is about, which is why it is silenced rather than worked around: FastAPI reaches the same operation identifier twice because the two methods are one operation, and dropping the twin is this function agreeing with it.

Rendered while the application is being built rather than on the first request that asks for it, so the document a caller reads is settled before anything can read it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def describe_each_read_once(api: FastAPI) -> None:
    """Render the facade API's document now, describing every read once rather than twice.

    `accept_head_wherever_get_is_served` gives every GET route a HEAD twin so a liveness probe is
    answered, and FastAPI describes a route once per method - so a document built over those routes
    carries a HEAD twin of every read: the same operation, under the same identifier, said again with
    no body. HEAD is GET without the body and there is nothing about it a reader of a contract needs,
    so the twins come out of the document here.

    That is also the whole of what the duplicate-identifier warning below is about, which is why it is
    silenced rather than worked around: FastAPI reaches the same operation identifier twice because
    the two methods are one operation, and dropping the twin is this function agreeing with it.

    Rendered while the application is being built rather than on the first request that asks for it,
    so the document a caller reads is settled before anything can read it.
    """
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", message="Duplicate Operation ID", category=UserWarning)
        document = api.openapi()
    paths: dict[str, dict[str, Any]] = document.get("paths", {})
    for operations in paths.values():
        operations.pop("head", None)
    api.openapi_schema = document

register_routes(app, serve_ui=False, capture=True, auth=ServeAuth.NONE, auth_scope=ServeAuthScope.WRITE, authentication=None)

Mount the facade's routes: FHIR at the base URL, this facade's own API at /facade, the shell last.

The UI mounts are this function's own and are not in ServeRouters: they are StaticFiles rather than routers, and their order requirement only makes sense inside this facade's own route table. See dhis2w_fhir_serve.ui.

authentication is the dependency the guarded routers carry, and defaults to dhis2w_fhir_serve.auth.require_authenticated. An application that already knows who its callers are passes its own callable here - the set it is mounted over is ServeRouters.guarded, which is a value that application can read for itself.

The check is mounted AHEAD of the content negotiation on the FHIR routers it shares a mount with: a caller this server will not answer learns that before it learns which media types the server answers in.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def register_routes(
    app: FastAPI,
    serve_ui: bool = False,
    capture: bool = True,
    auth: ServeAuth = ServeAuth.NONE,
    auth_scope: ServeAuthScope = ServeAuthScope.WRITE,
    authentication: Any = None,
) -> None:
    """Mount the facade's routes: FHIR at the base URL, this facade's own API at `/facade`, the shell last.

    The UI mounts are this function's own and are not in `ServeRouters`: they are `StaticFiles`
    rather than routers, and their order requirement only makes sense inside this facade's own route
    table. See `dhis2w_fhir_serve.ui`.

    `authentication` is the dependency the guarded routers carry, and defaults to
    `dhis2w_fhir_serve.auth.require_authenticated`. An application that already knows who its callers
    are passes its own callable here - the set it is mounted over is `ServeRouters.guarded`, which is
    a value that application can read for itself.

    The check is mounted AHEAD of the content negotiation on the FHIR routers it shares a mount with:
    a caller this server will not answer learns that before it learns which media types the server
    answers in.
    """
    from dhis2w_fhir_serve.auth import require_authenticated
    from dhis2w_fhir_serve.ui import mount_ui_assets, mount_ui_shell

    check = require_authenticated if authentication is None else authentication
    routers = serve_routers(capture=capture, serve_ui=serve_ui, auth=auth, auth_scope=auth_scope)
    if serve_ui:
        mount_ui_assets(app)
    for router in routers.in_mount_order():
        accept_head_wherever_get_is_served(router)
    for router in routers.fhir:
        guard = [Depends(check)] if routers.is_guarded(router) else []
        app.include_router(router, dependencies=[*guard, Depends(require_json_is_acceptable)])
    for router in routers.cds_hooks:
        app.include_router(router, dependencies=[Depends(check)] if routers.is_guarded(router) else [])
    app.mount(FACADE_MOUNT_PATH, build_facade_api(routers, authentication=check, state=app.state))
    # The read catch-alls claim every path of their shape, so they mount after every fixed path -
    # the `/facade` mount above included, since `/facade` is a one-segment path like any other.
    read_guard = [Depends(check)] if routers.is_guarded(routers.read) else []
    app.include_router(routers.read, dependencies=[*read_guard, Depends(require_json_is_acceptable)])
    if serve_ui:
        mount_ui_shell(app)

accept_head_wherever_get_is_served(router)

Answer HEAD on every GET route - Starlette runs the endpoint and the server withholds the body.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/__init__.py
def accept_head_wherever_get_is_served(router: APIRouter) -> None:
    """Answer HEAD on every GET route - Starlette runs the endpoint and the server withholds the body."""
    for route in router.routes:
        if isinstance(route, APIRoute) and route.methods and "GET" in route.methods:
            route.methods.add("HEAD")

context

How a route handler reaches the serve context the lifespan built.

The context is application state, not per-request state: one project, one store, one spool for the life of the process. Handlers read it off request.app.state rather than through a FastAPI dependency so nothing about it leaks into the route signatures, which are FHIR's, not ours.

The DHIS2 client is state of the same shape and lives beside it rather than on it: ServeContext is a Pydantic model of what the facade serves, and a live HTTP client is not a value that model can hold without opening itself to arbitrary types. It is None in the default mode, which is the whole of what makes the register routes live-only. ServeRuntime is the name for the pair, and attach_serve_runtime is what writes both of them under the two names this module reads.

The dhis2 posture holds a third: a connection to the same instance carrying no credential at all, which a register read borrows to send the CALLER'S header over. It is state of the process for the same reason the client is - a pool, not a value - and it is None in every other posture, because nothing else forwards anybody's credential. dhis2w_fhir_serve.passthrough is what reads it.

The materialized projection is the fourth, and it is a database connection held open for the life of the process - so it lives beside the context rather than on it, exactly as the two HTTP connections do. It is None wherever [serve.projection] store names none, which is the zero-ops default and the whole of what makes a facade configured without one behave as it always did.

Classes

Functions:

serve_context(request)

The project, store, spool, register surface, and settings this process serves.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/context.py
def serve_context(request: Request) -> ServeContext:
    """The project, store, spool, register surface, and settings this process serves."""
    context: ServeContext = getattr(request.app.state, SERVE_CONTEXT_ATTRIBUTE)
    return context

live_client(request)

The DHIS2 client this process holds open, or None when it serves a compiled guide.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/context.py
def live_client(request: Request) -> Dhis2Client | None:
    """The DHIS2 client this process holds open, or None when it serves a compiled guide."""
    client: Dhis2Client | None = getattr(request.app.state, LIVE_CLIENT_ATTRIBUTE, None)
    return client

caller_client(request)

The credential-free connection pass-through reads borrow, or None outside the dhis2 posture.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/context.py
def caller_client(request: Request) -> httpx2.AsyncClient | None:
    """The credential-free connection pass-through reads borrow, or None outside the `dhis2` posture."""
    connection: httpx2.AsyncClient | None = getattr(request.app.state, CALLER_CLIENT_ATTRIBUTE, None)
    return connection

projection_store(request)

The materialized projection this process serves from, or None where the project holds none.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/context.py
def projection_store(request: Request) -> ProjectionStore | None:
    """The materialized projection this process serves from, or None where the project holds none."""
    store: ProjectionStore | None = getattr(request.app.state, PROJECTION_STORE_ATTRIBUTE, None)
    return store

negotiation

What a FHIR route answers in, and the one request it cannot answer at all.

This server serves application/fhir+json and nothing else. /metadata says so - format names json alone - and a client that asked for XML is entitled to be told that rather than handed a JSON body it will fail to parse under a media type it declared it could not read. So a request whose Accept rules JSON out is refused with 406 before the route runs.

THE TEST IS DELIBERATELY MINIMAL: does any media range in the header admit JSON? */*, application/*, application/json, application/fhir+json, and every other application/…+json do; application/fhir+xml alone does not. Quality values are read past rather than ranked, because ranking matters only where a server has several formats to choose between and this one has one - a header naming XML first and */* last is a client that will take what it is given.

An absent or empty Accept is a client with no opinion, and R4 answers those in the server's own format. That is the case a curl with no flags and every liveness probe sends, so it is never the case a refusal falls on.

A browser is the client the header test falls on hardest: it asks for text/html and it is the client most likely to be following a link somebody pasted. R4 gives that client _format, and this server reads it as the override the specification makes it. _format=json, _format=application/json, and _format=application/fhir+json - in any casing - make JSON acceptable whatever Accept said, so a FHIR query is a URL that can be linked, mailed, and opened. A _format naming anything else is refused even where Accept would have admitted JSON: the client named the format it wants, and this server has only the one. An absent or blank _format says nothing, and the header decides alone.

This applies to the FHIR surface, and to two routers outside it. The facade API under /facade answers plain application/json about this facade rather than resources out of it, /cds-services answers plain JSON to an EHR, and the UI mounts serve a browser whose Accept is about HTML - none of those is a FHIR interaction to negotiate. The exceptions are the tracked entity record at /facade/tracked-entities/{uid}/events and one data set's responses at /facade/data-sets/{uid}/responses, which answer FHIR Bundles from facade-owned addresses and carry this check as their own mount-time requirement - see dhis2w_fhir_serve.routes.ServeRouters.

Classes

Functions:

accepts_json(accept)

Whether one Accept header admits a JSON body - an absent or empty one always does.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/negotiation.py
def accepts_json(accept: str | None) -> bool:
    """Whether one `Accept` header admits a JSON body - an absent or empty one always does."""
    if accept is None or not accept.strip():
        return True
    for stated in accept.split(","):
        media_range = stated.split(";")[0].strip().lower()
        if media_range in _WILDCARD_MEDIA_RANGES:
            return True
        media_type, _, subtype = media_range.partition("/")
        if media_type != _JSON_MEDIA_TYPE_PREFIX:
            continue
        if subtype == _JSON_MEDIA_SUBTYPE or subtype.endswith(JSON_MEDIA_TYPE_SUFFIX):
            return True
    return False

format_asks_for_json(stated_format)

Whether one _format value names the format this server answers in - casing is not read.

A space is read as the + it was: ?_format=application/fhir+json is how the media type is written everywhere a reader meets it, and a query string decodes an unescaped + to a space. Refusing the spelling every FHIR document uses would make the parameter unusable by hand.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/negotiation.py
def format_asks_for_json(stated_format: str) -> bool:
    """Whether one `_format` value names the format this server answers in - casing is not read.

    A space is read as the `+` it was: `?_format=application/fhir+json` is how the media type is
    written everywhere a reader meets it, and a query string decodes an unescaped `+` to a space.
    Refusing the spelling every FHIR document uses would make the parameter unusable by hand.
    """
    return stated_format.strip().lower().replace(" ", "+") in JSON_FORMAT_VALUES

require_json_is_acceptable(request) async

Refuse a FHIR interaction that asks for a format this server does not answer in.

_format is read first because R4 makes it the override: a value naming JSON settles the negotiation on its own, and a value naming anything else is refused whatever the header says.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/negotiation.py
async def require_json_is_acceptable(request: Request) -> None:
    """Refuse a FHIR interaction that asks for a format this server does not answer in.

    `_format` is read first because R4 makes it the override: a value naming JSON settles the
    negotiation on its own, and a value naming anything else is refused whatever the header says.
    """
    stated_format = request.query_params.get(FORMAT_PARAMETER)
    if stated_format is not None and stated_format.strip():
        if not format_asks_for_json(stated_format):
            raise UnsupportedFormatError(stated_format)
        return
    accept = request.headers.get(ACCEPT_HEADER)
    if not accepts_json(accept):
        raise NotAcceptableError(accept or "")

Authentication

Who the facade serves: the four postures [serve] auth picks between, the scope [serve] auth_scope covers, the startup refusals a posture this run could not honour meets before the socket opens, and the one dependency the guarded routers carry.

The dhis2 posture's 401 challenges with xBasic, not Basic - a browser that meets Basic on a request a page made opens its own credential dialog and never hands the response back, leaving the capture UI's Submit pending instead of rendering the refusal. The scheme callers send is untouched, and the header reads the same for every caller rather than shifting by Accept or user agent. BROWSER_SAFE_BASIC_SCHEME is the constant.

auth

Who the facade serves: the four postures, the check they run, and the identity a capture is stamped with.

FOUR POSTURES, ONE DEPENDENCY. [serve] auth picks between them and [serve] auth_scope picks how much of the surface the pick covers; the check itself is one FastAPI dependency, mounted on the routers dhis2w_fhir_serve.routes.serve_routers names as guarded. Nothing in a route handler knows which posture is running - a handler that wants to know who is calling reads request_identity, which answers None wherever no identity was established.

none serves every caller. It is the default and it is honest about being one: /metadata declares it in rest.security like any other posture, so a client never has to infer an absence, and ServeSettings.resolve refuses to bind an interface other than loopback while the project's fhir.toml has not written the key down. An absent key on loopback is the zero-friction demo; an absent key on 0.0.0.0 is a deployment nobody stated, and it is refused before the socket opens.

token compares an Authorization: Bearer <token> against the values of D2W_FHIR_SERVE_TOKENS, comma-separated. THE TOKENS COME FROM THE ENVIRONMENT AND NOWHERE ELSE - never from fhir.toml, which is a file projects commit. Comparison is hmac.compare_digest, so the time a refusal takes says nothing about how much of a token was right. Rotation is replacing the variable and restarting the process: the values are read once, at first use, and a running server holds what it started with.

dhis2 is the flagship: the caller's DHIS2 credentials are their facade credentials. Whatever the caller put in Authorization - Basic for a username and password, ApiToken for a DHIS2 personal access token, which is the header shape dhis2w_client.v43.auth.pat sends - is replayed against GET /api/me on the same instance this run reads, in a request of its own that carries the caller's header and NEVER the runtime's client. The facade's own credentials are not a fallback and are not a second attempt: a caller who cannot read /api/me cannot use this facade. The username DHIS2 answers with becomes the request identity, and the capture route stamps it onto the receipt.

ITS REFUSAL NAMES xBasic, AND THAT IS NOT A TYPO. A browser meeting WWW-Authenticate: Basic on a request a page made opens its own credential dialog and never hands the response back, so a capture screen's Submit would sit pending forever instead of rendering the refusal it was written to render. BROWSER_SAFE_BASIC_SCHEME is what the challenge says instead; the scheme callers SEND is untouched, and every non-browser client reads the status and the OperationOutcome as it always did.

THE VALIDATION IS CACHED, BRIEFLY, BY A HASH OF THE HEADER. Without a cache every request to a gated route would cost a round trip to DHIS2, which would make the facade slower than the instance it fronts. The key is sha256 of the header value rather than the header itself, so the process holds no plaintext credential in a dictionary, and entries expire CREDENTIAL_CACHE_SECONDS after the answer that filled them - long enough to make a page of requests one round trip, short enough that a disabled account stops working in the minute rather than at the next restart.

jwt takes a token an identity provider this facade does not run minted. [serve.jwt] issuer names that provider; the facade reads its /.well-known/openid-configuration and its JWKS while it starts, and every request is then verified in memory against those public keys - signature, iss, exp, nbf, and aud where one is configured. dhis2w_fhir_serve.oidc is the whole of that machinery and argues it; what matters here is the outcome: the claim [serve.jwt] username_claim names becomes the request identity, exactly as the DHIS2 username does under dhis2, so a receipt captured under this posture records a person and the pass-through machinery reads the same field it always read.

WHAT jwt CANNOT DO ON ITS OWN IS READ THE REGISTER AS THE CALLER. A token this facade accepts is a token DHIS2 accepts only when the instance was configured to trust the same issuer, through DHIS2's own oidc.jwt.token.authentication.enabled. So [serve.jwt] forward_bearer is false by default, and under it the live register is refused with a 501 that names what would make it answerable. The refusal is deliberate and the alternative is the trap it exists to close: reading the register as the facade's own profile would answer every caller with that profile's rights, which under an administrator profile is DHIS2's whole ownership and access-level model skipped with no audit entry. forward_bearer = true states that the instance does trust the issuer, and the caller's Authorization is then forwarded over exactly the path the dhis2 posture forwards over - the same opaque header on the same credential-free pool.

oauth2 is the posture that is not here. DHIS2 2.43.1's authorization server 500s for any client the API creates (BUGS.md 96), so there is nothing to build against; dhis2w_fhir.config.ServeAuth says what the name is reserved for and docs/fhir/301-serving.md says the same to a deployer. A deployment that wants bearer tokens from an authorization server today runs jwt against the one it already has.

Classes

UnauthenticatedError

Bases: ServeError

The request carried no credential this posture accepts, or one the posture refused.

401 rather than 403 in every case, the refused credential included: this facade holds no permissions of its own, so there is nothing it could grant one caller and withhold from another. Either the credential establishes who is calling or it does not, and both answers are the same one - present a credential this server accepts.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class UnauthenticatedError(ServeError):
    """The request carried no credential this posture accepts, or one the posture refused.

    401 rather than 403 in every case, the refused credential included: this facade holds no
    permissions of its own, so there is nothing it could grant one caller and withhold from another.
    Either the credential establishes who is calling or it does not, and both answers are the same
    one - present a credential this server accepts.
    """

    status_code = 401
    issue_code = "login"

    def __init__(self, diagnostics: str, challenge: str) -> None:
        super().__init__(diagnostics)
        self.challenge = challenge

    def response_headers(self) -> dict[str, str]:
        """The `WWW-Authenticate` challenge RFC 9110 requires of every 401."""
        return {"WWW-Authenticate": self.challenge}
Methods:
response_headers()

The WWW-Authenticate challenge RFC 9110 requires of every 401.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def response_headers(self) -> dict[str, str]:
    """The `WWW-Authenticate` challenge RFC 9110 requires of every 401."""
    return {"WWW-Authenticate": self.challenge}

ServeAuthConfigurationError

Bases: ValueError

A posture this run cannot honour, refused while the settings resolve rather than at a request.

A ValueError because that is what ServeSettings.resolve already raises for a stated value it cannot mean, and d2w fhir serve renders it as one line against the dial it came from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class ServeAuthConfigurationError(ValueError):
    """A posture this run cannot honour, refused while the settings resolve rather than at a request.

    A `ValueError` because that is what `ServeSettings.resolve` already raises for a stated value it
    cannot mean, and `d2w fhir serve` renders it as one line against the dial it came from.
    """

RequestIdentity

Bases: BaseModel

Who one request established itself as, under the posture that established it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class RequestIdentity(BaseModel):
    """Who one request established itself as, under the posture that established it."""

    model_config = ConfigDict(frozen=True)

    posture: ServeAuth
    username: str | None = None
    """Who the credential named: the DHIS2 username under `dhis2`, the claim `[serve.jwt]
    username_claim` names under `jwt`. None under `token`, which names no person."""
Attributes
username = None class-attribute instance-attribute

Who the credential named: the DHIS2 username under dhis2, the claim [serve.jwt] username_claim names under jwt. None under token, which names no person.

ValidatedCredential

Bases: BaseModel

One /api/me answer, held until it expires - the username DHIS2 gave, and when it stops counting.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class ValidatedCredential(BaseModel):
    """One `/api/me` answer, held until it expires - the username DHIS2 gave, and when it stops counting."""

    model_config = ConfigDict(frozen=True)

    username: str
    expires_at: float
    """A `time.monotonic` reading, so the entry survives the machine's clock being set."""
Attributes
expires_at instance-attribute

A time.monotonic reading, so the entry survives the machine's clock being set.

CredentialCache

Bases: BaseModel

The DHIS2 answers this process is still reusing, keyed by a hash of the header that earned them.

Mutable on purpose - it is state a running process fills - and it holds no plaintext credential: the key is sha256 of the Authorization value, so a memory dump of this dictionary names nobody's password.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class CredentialCache(BaseModel):
    """The DHIS2 answers this process is still reusing, keyed by a hash of the header that earned them.

    Mutable on purpose - it is state a running process fills - and it holds no plaintext credential:
    the key is `sha256` of the `Authorization` value, so a memory dump of this dictionary names
    nobody's password.
    """

    model_config = ConfigDict(arbitrary_types_allowed=False)

    entries: dict[str, ValidatedCredential] = Field(default_factory=dict)

    def valid(self, key: str, *, now: float) -> ValidatedCredential | None:
        """The unexpired answer under one key, dropping it when it has expired."""
        held = self.entries.get(key)
        if held is None:
            return None
        if held.expires_at <= now:
            del self.entries[key]
            return None
        return held

    def remember(self, key: str, username: str, *, now: float) -> ValidatedCredential:
        """Hold one answer for `CREDENTIAL_CACHE_SECONDS` from the moment it was given."""
        entry = ValidatedCredential(username=username, expires_at=now + CREDENTIAL_CACHE_SECONDS)
        self.entries[key] = entry
        return entry
Methods:
valid(key, *, now)

The unexpired answer under one key, dropping it when it has expired.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def valid(self, key: str, *, now: float) -> ValidatedCredential | None:
    """The unexpired answer under one key, dropping it when it has expired."""
    held = self.entries.get(key)
    if held is None:
        return None
    if held.expires_at <= now:
        del self.entries[key]
        return None
    return held
remember(key, username, *, now)

Hold one answer for CREDENTIAL_CACHE_SECONDS from the moment it was given.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def remember(self, key: str, username: str, *, now: float) -> ValidatedCredential:
    """Hold one answer for `CREDENTIAL_CACHE_SECONDS` from the moment it was given."""
    entry = ValidatedCredential(username=username, expires_at=now + CREDENTIAL_CACHE_SECONDS)
    self.entries[key] = entry
    return entry

AuthState

Bases: BaseModel

What the check needs beyond the settings: the tokens this process started with, and its cache.

The tokens are read once, at first use, because rotation is replacing the variable and restarting - a process that re-read the environment per request would honour a rotation nobody restarted for and hide the restart the deployment actually needs.

THEY ARE NOT ON ServeSettings, and that is the whole reason this model exists. The settings are handed to the app and read back out by /facade/uiconfig, so a secret on them would be a secret one HTTP response away from a browser. What crosses to the browser is the posture's name.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
class AuthState(BaseModel):
    """What the check needs beyond the settings: the tokens this process started with, and its cache.

    The tokens are read once, at first use, because rotation is replacing the variable and restarting
    - a process that re-read the environment per request would honour a rotation nobody restarted for
    and hide the restart the deployment actually needs.

    THEY ARE NOT ON `ServeSettings`, and that is the whole reason this model exists. The settings are
    handed to the app and read back out by `/facade/uiconfig`, so a secret on them would be a secret one
    HTTP response away from a browser. What crosses to the browser is the posture's name.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    tokens: tuple[str, ...] = ()
    cache: CredentialCache = Field(default_factory=CredentialCache)

    verifier: JwtVerifier | None = None
    """The `jwt` posture's issuer and its published keys, opened while the server started.

    It holds public keys and no secret, which is what makes it the exception to the paragraph above:
    there is nothing on it a `/facade/uiconfig` response could leak. It is here rather than fetched per
    request because reading an identity provider's JWKS on every call would put that provider in the
    path of every read - see `dhis2w_fhir_serve.oidc`.
    """
Attributes
verifier = None class-attribute instance-attribute

The jwt posture's issuer and its published keys, opened while the server started.

It holds public keys and no secret, which is what makes it the exception to the paragraph above: there is nothing on it a /facade/uiconfig response could leak. It is here rather than fetched per request because reading an identity provider's JWKS on every call would put that provider in the path of every read - see dhis2w_fhir_serve.oidc.

Functions:

read_serve_tokens(environment=None)

The static bearer tokens D2W_FHIR_SERVE_TOKENS names, comma-separated, blanks dropped.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def read_serve_tokens(environment: dict[str, str] | None = None) -> tuple[str, ...]:
    """The static bearer tokens `D2W_FHIR_SERVE_TOKENS` names, comma-separated, blanks dropped."""
    raw = (environment if environment is not None else dict(os.environ)).get(SERVE_TOKENS_VARIABLE, "")
    return tuple(value for value in (part.strip() for part in raw.split(",")) if value)

matches_a_serve_token(presented, tokens)

Whether one presented token is one of this run's, compared in constant time.

Every configured token is compared, and the loop does not stop at the first match: returning as soon as one hits would make the time a request takes a function of which token was presented, which is the leak hmac.compare_digest exists to close on the bytes.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def matches_a_serve_token(presented: str, tokens: tuple[str, ...]) -> bool:
    """Whether one presented token is one of this run's, compared in constant time.

    Every configured token is compared, and the loop does not stop at the first match: returning as
    soon as one hits would make the time a request takes a function of which token was presented,
    which is the leak `hmac.compare_digest` exists to close on the bytes.
    """
    matched = False
    for token in tokens:
        if hmac.compare_digest(presented, token):
            matched = True
    return matched

current_monotonic()

The reading the cache measures its own entries against - monotonic, so a clock change is not an expiry.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def current_monotonic() -> float:
    """The reading the cache measures its own entries against - monotonic, so a clock change is not an expiry."""
    return time.monotonic()

credential_key(header_value)

The cache key for one Authorization value - a hash, so no plaintext credential is held.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def credential_key(header_value: str) -> str:
    """The cache key for one `Authorization` value - a hash, so no plaintext credential is held."""
    return hashlib.sha256(header_value.encode("utf-8")).hexdigest()

challenge_for(posture, issuer=None)

The WWW-Authenticate value one posture refuses with.

issuer is stated under jwt and ignored everywhere else. RFC 6750 has no parameter for naming the authorization server a bearer token should come from, so it rides in error_description, which is the one place a client is allowed to read prose from - and it is the fact a caller holding no token most needs.

THE DHIS2 POSTURE NAMES xBasic RATHER THAN Basic, for every caller alike - see BROWSER_SAFE_BASIC_SCHEME. It is stated once here rather than decided per request off Accept or off a user agent, because a refusal whose shape depends on who is reading it is a refusal nobody can reason about, and the header's audience is a browser either way: a command-line client reads the status and the OperationOutcome, both of which are unchanged.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def challenge_for(posture: ServeAuth, issuer: str | None = None) -> str:
    """The `WWW-Authenticate` value one posture refuses with.

    `issuer` is stated under `jwt` and ignored everywhere else. RFC 6750 has no parameter for naming
    the authorization server a bearer token should come from, so it rides in `error_description`,
    which is the one place a client is allowed to read prose from - and it is the fact a caller
    holding no token most needs.

    THE DHIS2 POSTURE NAMES `xBasic` RATHER THAN `Basic`, for every caller alike - see
    `BROWSER_SAFE_BASIC_SCHEME`. It is stated once here rather than decided per request off `Accept`
    or off a user agent, because a refusal whose shape depends on who is reading it is a refusal
    nobody can reason about, and the header's audience is a browser either way: a command-line client
    reads the status and the OperationOutcome, both of which are unchanged.
    """
    if posture is ServeAuth.DHIS2:
        return f'{BROWSER_SAFE_BASIC_SCHEME} realm="{AUTHENTICATION_REALM}", charset="UTF-8"'
    if posture is ServeAuth.JWT and issuer is not None:
        return f'{BEARER_SCHEME} realm="{AUTHENTICATION_REALM}", error_description="a token from {issuer}"'
    return f'{BEARER_SCHEME} realm="{AUTHENTICATION_REALM}"'

request_identity(request)

Who this request established itself as, or None where no posture established anybody.

What the capture route reads to stamp a receipt. None is every request under none, every request under token - a static token names no person - and every request to a route this scope leaves open.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def request_identity(request: Request) -> RequestIdentity | None:
    """Who this request established itself as, or None where no posture established anybody.

    What the capture route reads to stamp a receipt. None is every request under `none`, every
    request under `token` - a static token names no person - and every request to a route this
    scope leaves open.
    """
    identity: RequestIdentity | None = getattr(request.state, REQUEST_IDENTITY_ATTRIBUTE, None)
    return identity

require_authenticated(request) async

Establish who is calling, or refuse the request - the dependency the guarded routers carry.

An embedding application that authenticates its callers some other way mounts its own dependency in this one's place: serve_routers states which routers it belongs on, and dhis2w_fhir_serve.routes.register_routes is the only thing that assumes this function. Such an application writes its own RequestIdentity onto request.state under REQUEST_IDENTITY_ATTRIBUTE if it wants the capture receipts attributed.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
async def require_authenticated(request: Request) -> None:
    """Establish who is calling, or refuse the request - the dependency the guarded routers carry.

    An embedding application that authenticates its callers some other way mounts its own dependency
    in this one's place: `serve_routers` states which routers it belongs on, and
    `dhis2w_fhir_serve.routes.register_routes` is the only thing that assumes this function. Such an
    application writes its own `RequestIdentity` onto `request.state` under
    `REQUEST_IDENTITY_ATTRIBUTE` if it wants the capture receipts attributed.
    """
    settings = serve_context(request).settings
    if settings.auth is ServeAuth.NONE:
        return
    presented = request.headers.get(AUTHORIZATION_HEADER, "").strip()
    if presented == "":
        raise UnauthenticatedError(
            f"this server takes no request without an `Authorization` header; {_expected(settings)}",
            challenge_for(settings.auth, settings.jwt.issuer),
        )
    state = auth_state(request)
    if settings.auth is ServeAuth.TOKEN:
        _establish_token_identity(request, presented, state)
        return
    if settings.auth is ServeAuth.JWT:
        await _establish_jwt_identity(request, presented, state, settings)
        return
    await _establish_dhis2_identity(request, presented, state, settings)

auth_state(request)

This app's auth state, built the first time a guarded route is reached.

Built here rather than in the lifespan for the reason the capture state is: a facade nobody ever sends a guarded request to never reads the environment and never allocates a cache. It is application state, not request state, so the cache is shared by every request of the process.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def auth_state(request: Request) -> AuthState:
    """This app's auth state, built the first time a guarded route is reached.

    Built here rather than in the lifespan for the reason the capture state is: a facade nobody ever
    sends a guarded request to never reads the environment and never allocates a cache. It is
    application state, not request state, so the cache is shared by every request of the process.
    """
    held: AuthState | None = getattr(request.app.state, AUTH_STATE_ATTRIBUTE, None)
    if held is not None:
        return held
    verifier: JwtVerifier | None = getattr(request.app.state, JWT_VERIFIER_ATTRIBUTE, None)
    state = AuthState(tokens=read_serve_tokens(), verifier=verifier)
    setattr(request.app.state, AUTH_STATE_ATTRIBUTE, state)
    return state

open_jwt_verifier(config) async

Read the issuer this run trusts, while the server starts, or refuse to serve at all.

The refusal is a ServeAuthConfigurationError so it lands beside the other posture refusals - a deployer meets one line naming the key that has to change, whether the problem was an issuer nobody wrote down or one nobody could reach.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
async def open_jwt_verifier(config: ServeJwtConfig) -> JwtVerifier:
    """Read the issuer this run trusts, while the server starts, or refuse to serve at all.

    The refusal is a `ServeAuthConfigurationError` so it lands beside the other posture refusals - a
    deployer meets one line naming the key that has to change, whether the problem was an issuer
    nobody wrote down or one nobody could reach.
    """
    try:
        return await discover_issuer(config)
    except OidcIssuerUnavailableError as error:
        raise ServeAuthConfigurationError(
            f'`auth = "jwt"` verifies every caller\'s token against the keys `{config.issuer}` publishes, '
            f"and this server could not read them: {error}. Check `[serve.jwt] issuer` names the issuer "
            "identifier its own tokens carry as `iss`, and that this machine can reach it."
        ) from error

validate_with_dhis2(base_url, header_value) async

Replay one caller's Authorization against GET /api/me, answering with the username DHIS2 gave.

A REQUEST OF ITS OWN, CARRYING THE CALLER'S HEADER AND NOTHING ELSE. The client the live store was built through holds the facade's own credentials, and reusing it here would validate every caller as whoever the server logs in as - which would be the facade handing out its own account. So this opens a plain httpx2 client, sends exactly what arrived, and closes it.

Any answer other than a 2xx is a refusal, and so is one the instance never gave: an unreachable DHIS2 means this facade cannot say who is calling, and serving the request anyway would be answering the question by giving up on it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
async def validate_with_dhis2(base_url: str, header_value: str) -> str:
    """Replay one caller's `Authorization` against `GET /api/me`, answering with the username DHIS2 gave.

    A REQUEST OF ITS OWN, CARRYING THE CALLER'S HEADER AND NOTHING ELSE. The client the live store was
    built through holds the facade's own credentials, and reusing it here would validate every caller
    as whoever the server logs in as - which would be the facade handing out its own account. So this
    opens a plain `httpx2` client, sends exactly what arrived, and closes it.

    Any answer other than a 2xx is a refusal, and so is one the instance never gave: an unreachable
    DHIS2 means this facade cannot say who is calling, and serving the request anyway would be
    answering the question by giving up on it.
    """
    challenge = challenge_for(ServeAuth.DHIS2)
    try:
        async with httpx2.AsyncClient(base_url=base_url, timeout=DHIS2_IDENTITY_TIMEOUT_SECONDS) as http:
            answer = await http.get(
                DHIS2_IDENTITY_PATH,
                headers={"Authorization": header_value, "Accept": "application/json"},
            )
    except httpx2.HTTPError as error:
        raise UnauthenticatedError(
            f"this server could not reach the DHIS2 instance to check the credentials it was given ({error})",
            challenge,
        ) from error
    if answer.status_code >= 400:
        raise UnauthenticatedError(
            f"the DHIS2 instance behind this server refused the credentials it was given "
            f"(`GET {DHIS2_IDENTITY_PATH}` answered {answer.status_code})",
            challenge,
        )
    return _username_from(answer, challenge)

preflight_auth(*, posture, host, live, stated, jwt=None, environment=None)

Refuse, before anything binds, every posture this run could not actually honour.

Five refusals, each about a run rather than about a request, which is why they are here and not in the dependency. A server that starts and then refuses every caller - or worse, serves every caller from an interface the deployment thought was closed - is a failure nobody reads until it matters.

Two of them are the jwt posture's: an issuer nobody named, and forward_bearer asked for on a run with no instance to forward to. The third thing that posture needs - an issuer this machine can actually reach - is not checked here, because reading it is a round trip and this function is a reader of values. dhis2w_fhir_serve.auth.open_jwt_verifier does it while the server starts and refuses with the same error type.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/auth.py
def preflight_auth(
    *,
    posture: ServeAuth,
    host: str,
    live: bool,
    stated: bool,
    jwt: ServeJwtConfig | None = None,
    environment: dict[str, str] | None = None,
) -> None:
    """Refuse, before anything binds, every posture this run could not actually honour.

    Five refusals, each about a run rather than about a request, which is why they are here and not
    in the dependency. A server that starts and then refuses every caller - or worse, serves every
    caller from an interface the deployment thought was closed - is a failure nobody reads until it
    matters.

    Two of them are the `jwt` posture's: an issuer nobody named, and `forward_bearer` asked for on a
    run with no instance to forward to. The third thing that posture needs - an issuer this machine
    can actually reach - is not checked here, because reading it is a round trip and this function is
    a reader of values. `dhis2w_fhir_serve.auth.open_jwt_verifier` does it while the server starts
    and refuses with the same error type.
    """
    if not stated and not _is_loopback(host):
        raise ServeAuthConfigurationError(
            f"`{host}` is not a loopback interface, and this project's fhir.toml states no `[serve] auth`. "
            "Write the posture down before serving the facade where other hosts can reach it - add one "
            'line under `[serve]` in fhir.toml: `auth = "none"` to serve every caller, `auth = "token"` '
            f'to take a static bearer token out of `{SERVE_TOKENS_VARIABLE}`, `auth = "dhis2"` to have '
            "every caller present the DHIS2 credentials this facade checks against the instance, or "
            '`auth = "jwt"` to take a token from an OpenID Connect issuer named in `[serve.jwt] issuer`. '
            "`--auth` states the same thing for one run."
        )
    if posture is ServeAuth.TOKEN and not read_serve_tokens(environment):
        raise ServeAuthConfigurationError(
            f'`auth = "token"` takes its tokens from the environment variable `{SERVE_TOKENS_VARIABLE}`, '
            "which is unset or empty. Set it to the tokens this deployment accepts, comma-separated, and "
            "serve again. The tokens are secrets and do not belong in fhir.toml, which is a file projects "
            "commit; rotating them is replacing the variable and restarting this process."
        )
    if posture is ServeAuth.DHIS2 and not live:
        raise ServeAuthConfigurationError(
            '`auth = "dhis2"` checks every caller\'s credentials against the DHIS2 instance this run reads, '
            "and this run reads a compiled implementation guide off disk instead - there is no instance to "
            'check anybody against. Serve with `--live`, or state `auth = "token"` or `auth = "none"`.'
        )
    if posture is ServeAuth.JWT:
        _preflight_jwt(jwt if jwt is not None else ServeJwtConfig(), live=live)

Who the caller is (GET /facade/whoami)

The one address whose whole answer is who this server just decided the caller is. It carries the authentication check in every scope - write guards one route and all guards all but /metadata, and this one is guarded under both - so a client can get a verdict on a credential without doing anything with it. Wrong credentials get the same 401, the same OperationOutcome, and the same WWW-Authenticate challenge every other refusal on this facade gets.

It names a caller only where [serve] auth names a posture. Under auth = "none" the address answers its own 404 - "this server authenticates nobody, so it names nobody: /facade/whoami answers a caller only where [serve] auth states a posture" - rather than falling through to the read catch-all, which would call whoami a resource type nobody asked for.

$ curl -su clerk:the-right-password http://127.0.0.1:8095/facade/whoami
{"posture":"dhis2","username":"clerk","name":"clerk"}

$ curl -su clerk:wrong -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8095/facade/whoami
401

username is the DHIS2 username under dhis2, the claim [serve.jwt] username_claim names under jwt - the same value a receipt is stamped with - and null under token, which names a deployment rather than a person. name is what to call the caller in a sentence: the username where there is one, and a stated constant where there is not.

The capture UI's sign-in panel is the first caller: it asks here with what was typed before it holds on to anything, so a wrong password is refused at the prompt rather than at the first submission. The path is fixed rather than discovered, because the UI is served same-origin by the very process that answers it.

whoami

GET /facade/whoami - the one address whose whole answer is who this server just decided the caller is.

WHY AN ADDRESS FOR IT. Every other route on this facade answers a question about the guide or about the instance, and who is asking is a fact the route needs rather than the fact it reports. A caller holding a credential has no way to learn whether this server accepts it except by doing something with it, and under [serve] auth_scope = "write" the only thing that would answer is a capture - which means the first honest verdict on a credential arrives after somebody has filled in a form. This address is that verdict, on its own, before anything is typed. The capture UI's sign-in panel is the first caller and every HTTP client is welcome to the same answer.

IT CARRIES THE CHECK IN EVERY SCOPE, and that is the whole of what makes it useful. write guards one route and all guards all but /metadata; this one is guarded under both, because a route that answered "nobody" instead of refusing would turn a wrong password into a shrug. So the answer is either 200 naming a caller or the 401 dhis2w_fhir_serve.auth refuses everything else with - the same OperationOutcome, the same WWW-Authenticate challenge, no second vocabulary to read.

IT NAMES A CALLER ONLY WHERE A POSTURE IS CONFIGURED, and under auth = "none" it says so in those words. A server that checks nobody has nobody to name, and one that answered with an anonymous caller would be inventing an identity to report - so the answer is a 404 that states the posture it is missing rather than one naming an invented person. It is a route of its own rather than a path left unmounted, because an unmounted path answers the facade mount's own 404 and says only that there is nothing here - and whoami is a fixed path this project documents rather than one nobody asked for. serve_routers picks which of the two routers is mounted, beside every other mount-time decision.

WHAT IT NAMES, PER POSTURE. dhis2 answers the username the instance gave GET /api/me; jwt answers the claim [serve.jwt] username_claim names, which is the same value a receipt is stamped with; token answers no username at all, because a static token names a deployment rather than a person, and name states that in words rather than inventing one. Nothing else crosses - not the credential, not the roles the instance holds, not the claims beside the username. This says who, and who is all it says.

Classes

NoPostureNamesNobodyError

Bases: ServeError

/whoami was asked of a server running [serve] auth = "none", which establishes no caller.

A 404 with the reason stated, rather than the read catch-all's "does not serve the resource type whoami": the address exists in this facade's vocabulary, and what is absent is the posture that would give it something to say.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
class NoPostureNamesNobodyError(ServeError):
    """`/whoami` was asked of a server running `[serve] auth = "none"`, which establishes no caller.

    A 404 with the reason stated, rather than the read catch-all's "does not serve the resource type
    `whoami`": the address exists in this facade's vocabulary, and what is absent is the posture that
    would give it something to say.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self) -> None:
        """State what is missing: a posture, not a resource type."""
        super().__init__(
            "this server authenticates nobody, so it names nobody: `/whoami` answers a caller only "
            "where `[serve] auth` states a posture"
        )
Methods:
__init__()

State what is missing: a posture, not a resource type.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
def __init__(self) -> None:
    """State what is missing: a posture, not a resource type."""
    super().__init__(
        "this server authenticates nobody, so it names nobody: `/whoami` answers a caller only "
        "where `[serve] auth` states a posture"
    )

AuthenticatedCaller

Bases: BaseModel

Who this server established one request to be, under the posture that established it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
class AuthenticatedCaller(BaseModel):
    """Who this server established one request to be, under the posture that established it."""

    model_config = ConfigDict(frozen=True)

    posture: ServeAuth
    """Which check ran - `dhis2`, `token`, or `jwt`. Never `none`, which mounts no route here."""

    username: str | None = None
    """The person the credential named: the DHIS2 username under `dhis2`, the claim `[serve.jwt]
    username_claim` names under `jwt`. None under `token`, which names no person."""

    name: str
    """What to call this caller in a sentence - the username where there is one, and `TOKEN_CALLER_NAME`
    where the credential named a deployment rather than anybody."""
Attributes
posture instance-attribute

Which check ran - dhis2, token, or jwt. Never none, which mounts no route here.

username = None class-attribute instance-attribute

The person the credential named: the DHIS2 username under dhis2, the claim [serve.jwt] username_claim names under jwt. None under token, which names no person.

name instance-attribute

What to call this caller in a sentence - the username where there is one, and TOKEN_CALLER_NAME where the credential named a deployment rather than anybody.

Functions:

read_authenticated_caller(request) async

Name the caller the authentication check just established, or refuse as that check refuses.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
@router.get(
    WHOAMI_PATH,
    tags=[WHOAMI_TAG],
    summary="Name the caller",
    description=(
        "Answers who this server established the caller to be, under the posture that established "
        "them. Guarded in every scope, so a credential this server does not accept is refused here "
        "with 401 rather than answered with nobody - which is what makes this the place to check a "
        'credential before doing anything with it. Under `[serve] auth = "none"` the address '
        "answers 404 saying which posture is missing, because a server that checks nobody has "
        "nobody to name."
    ),
    response_description="Who the credential named, and what to call them in a sentence.",
)
async def read_authenticated_caller(request: Request) -> AuthenticatedCaller:
    """Name the caller the authentication check just established, or refuse as that check refuses."""
    identity = request_identity(request)
    if identity is None:
        # Unreachable through a server this package started: this router is mounted only where a
        # posture is configured, and it carries the check in every scope, so a request reaching the
        # handler has an identity. Stated anyway, because an embedding application mounting these
        # routers under a check of its own could reach it - and a 200 naming nobody would be this
        # address answering the one question it exists to answer by giving up on it.
        settings = serve_context(request).settings
        raise UnauthenticatedError(
            "this server established no caller for this request: the routers were mounted under a "
            "check that records no identity",
            challenge_for(settings.auth, settings.jwt.issuer),
        )
    return authenticated_caller(identity)

refuse_to_name_a_caller() async

Refuse the address under auth = "none", naming the posture that is missing rather than a caller.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
@refusal_router.get(
    WHOAMI_PATH,
    tags=[WHOAMI_TAG],
    summary="Name the caller",
    description=(
        'This process runs `[serve] auth = "none"` and establishes no caller, so the address '
        "answers 404 naming the posture that is missing rather than an invented identity. A process "
        "started under any other posture answers this address with the caller it established."
    ),
    response_description="Never answered under this posture; the refusal names the missing posture.",
)
async def refuse_to_name_a_caller() -> AuthenticatedCaller:
    """Refuse the address under `auth = "none"`, naming the posture that is missing rather than a caller."""
    raise NoPostureNamesNobodyError

authenticated_caller(identity)

One established identity as this address answers it, naming the person where a person was named.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/whoami.py
def authenticated_caller(identity: RequestIdentity) -> AuthenticatedCaller:
    """One established identity as this address answers it, naming the person where a person was named."""
    if identity.username is None:
        return AuthenticatedCaller(posture=identity.posture, name=TOKEN_CALLER_NAME)
    return AuthenticatedCaller(posture=identity.posture, username=identity.username, name=identity.username)

The external issuer

Under [serve] auth = "jwt", callers arrive with a token an OpenID Connect issuer this facade does not run has minted. The issuer's discovery document and its JWKS are read once while the server starts - an issuer this machine cannot reach refuses the run - and every token is then verified in memory against those public keys: signature over the asymmetric algorithms only, iss, exp, nbf, and aud where one is configured. The keys are held for as long as their own Cache-Control asks, never below a floor, and an unknown kid forces one refetch so a key rotation is not an outage.

oidc

The jwt posture's machinery: one external OIDC issuer, its published keys, and what a token proves.

WHAT THIS POSTURE IS FOR. A ministry that already runs an identity provider has already answered "who is this person" for every system it fronts, and [serve] auth = "jwt" is this facade taking that answer instead of asking the question a second time. No authorization server is run here, no client secret is held here, and no token is minted here: a caller arrives with a token their own IdP gave them, and this module decides whether it is genuine, current, and meant for this server.

VALIDATION IS LOCAL AND OFFLINE-AFTER-THE-FIRST-FETCH. The issuer publishes its signing keys as a JWKS, this process reads that document, and every token is verified against those keys in memory. There is no introspection call, so a request costs no round trip to the IdP and the IdP is not in the path of every read. The cost of that is the one property token introspection would have bought: a token revoked before it expires stays valid here until it expires. That is the standard trade every JWKS validator makes, and the answer to it is short token lifetimes at the issuer.

THE KEYS ARE FETCHED ONCE AND HELD. Cache-Control: max-age on the JWKS answer is honoured, with a floor of JWKS_MINIMUM_CACHE_SECONDS: an issuer that sends max-age=0 would otherwise make every verification a round trip, which is the thing this design exists to avoid. There is no ceiling, and there does not need to be one - a key rotation shows up as a token signed by a kid this process does not hold, and an unknown kid forces one refetch on the spot. That refetch is itself floored by JWKS_REFETCH_FLOOR_SECONDS, so a caller sending nonsense kids cannot turn this facade into a load generator pointed at somebody's identity provider.

ONLY ASYMMETRIC SIGNATURES ARE ACCEPTED. JWT_ALGORITHMS is the RSA and ECDSA family and nothing else. A JWKS holds public keys, and accepting a symmetric algorithm beside them is the algorithm confusion attack in one line: a caller signs HS256 using the public modulus as the shared secret and the verifier, asked to accept "whatever the header says", agrees. The header is not asked.

WHAT A TOKEN HAS TO CARRY. A signature this issuer's keys verify; iss equal to the configured issuer; exp in the future; nbf in the past where it is stated; aud containing the configured audience where one is configured; and the claim [serve.jwt] username_claim names. That last one is the whole point of the exercise - it becomes the request identity, so a receipt captured under this posture records a person rather than a deployment.

CLOCK_LEEWAY_SECONDS is what a token's time claims are read with. Two machines that have never agreed on the second would otherwise refuse each other's perfectly good tokens for a minute either side of every boundary, and a minute of leeway is the standard allowance for that.

Classes

OidcIssuerUnavailableError

Bases: RuntimeError

The issuer this run was told to trust could not be read, so the posture cannot be honoured.

A startup failure rather than a request failure. dhis2w_fhir_serve.auth.open_jwt_verifier turns it into the same ServeAuthConfigurationError the other posture refusals raise, so a deployer meets one line naming the key rather than a server that starts and 401s everybody.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
class OidcIssuerUnavailableError(RuntimeError):
    """The issuer this run was told to trust could not be read, so the posture cannot be honoured.

    A startup failure rather than a request failure. `dhis2w_fhir_serve.auth.open_jwt_verifier`
    turns it into the same `ServeAuthConfigurationError` the other posture refusals raise, so a
    deployer meets one line naming the key rather than a server that starts and 401s everybody.
    """

TokenRefusedError

Bases: ValueError

One presented token this issuer's keys, or this facade's rules, would not accept.

Carries the sentence a caller is told and nothing else. dhis2w_fhir_serve.auth is what turns it into the 401 and the WWW-Authenticate challenge that goes with it - this module refuses tokens and never writes HTTP.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
class TokenRefusedError(ValueError):
    """One presented token this issuer's keys, or this facade's rules, would not accept.

    Carries the sentence a caller is told and nothing else. `dhis2w_fhir_serve.auth` is what turns
    it into the 401 and the `WWW-Authenticate` challenge that goes with it - this module refuses
    tokens and never writes HTTP.
    """

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)
        self.diagnostics = diagnostics

VerifiedToken

Bases: BaseModel

What one accepted token established: who is calling, and the claims that said so.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
class VerifiedToken(BaseModel):
    """What one accepted token established: who is calling, and the claims that said so."""

    model_config = ConfigDict(frozen=True)

    username: str
    """The value of `[serve.jwt] username_claim`, which becomes the request identity."""

    subject: str | None = None
    """The token's `sub` - the issuer's own stable identifier for the caller, where it stated one."""

    expires_at: int | None = None
    """The token's `exp`, as it stood - what a deployment reads to know how long an answer stays true."""
Attributes
username instance-attribute

The value of [serve.jwt] username_claim, which becomes the request identity.

subject = None class-attribute instance-attribute

The token's sub - the issuer's own stable identifier for the caller, where it stated one.

expires_at = None class-attribute instance-attribute

The token's exp, as it stood - what a deployment reads to know how long an answer stays true.

PublishedKeys

Bases: BaseModel

One JWKS document as this process holds it, and until when it holds it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
class PublishedKeys(BaseModel):
    """One JWKS document as this process holds it, and until when it holds it."""

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    key_set: KeySet
    """The issuer's public keys, indexed by `kid` - what a signature is checked against."""

    fetched_at: float
    """A `time.monotonic` reading, so holding a document survives the machine's clock being set."""

    expires_at: float
    """When this document stops being reused, from `Cache-Control` and never below the floor."""
Attributes
key_set instance-attribute

The issuer's public keys, indexed by kid - what a signature is checked against.

fetched_at instance-attribute

A time.monotonic reading, so holding a document survives the machine's clock being set.

expires_at instance-attribute

When this document stops being reused, from Cache-Control and never below the floor.

JwtVerifier

Bases: BaseModel

One issuer's published keys, held for the life of the process, and the check they answer.

Built while the server starts, by discover_issuer, so a run whose issuer cannot be read is a line in a terminal rather than a 401 on every caller. It holds public keys and no secret of any kind, which is why it is safe to keep on the application where every request reaches it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
class JwtVerifier(BaseModel):
    """One issuer's published keys, held for the life of the process, and the check they answer.

    Built while the server starts, by `discover_issuer`, so a run whose issuer cannot be read is a
    line in a terminal rather than a 401 on every caller. It holds public keys and no secret of any
    kind, which is why it is safe to keep on the application where every request reaches it.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    config: ServeJwtConfig
    """The `[serve.jwt]` table this run resolved - the issuer, the audience, and the claim to read."""

    issuer: str
    """The issuer identifier every token's `iss` is compared against, as the issuer publishes it."""

    jwks_uri: str
    """Where the keys come from, as `/.well-known/openid-configuration` named it."""

    held: PublishedKeys | None = Field(default=None, repr=False)
    """The keys this process is currently verifying against, or None until the first fetch."""

    forced_at: float | None = None
    """When an unknown `kid` last sent this process back to the issuer, or None while none has."""

    async def verify(self, token: str) -> VerifiedToken:
        """Accept one token, or say in one sentence why it is refused.

        The order is the order a reader would check it in: the signature first, because an unsigned
        assertion's claims are not evidence of anything; then what the claims say; then the claim
        that names the caller. An unknown `kid` is the one case that goes back to the issuer -
        a rotation is the ordinary reason a signature arrives under a key this process does not
        hold, and refusing it without looking would make every rotation an outage.
        """
        decoded = await self._decoded(token)
        self._claims_hold(decoded.claims)
        return VerifiedToken(
            username=self._named_caller(decoded.claims),
            subject=_string_claim(decoded.claims, "sub"),
            expires_at=_integer_claim(decoded.claims, "exp"),
        )

    async def keys(self, *, force: bool = False) -> PublishedKeys:
        """The issuer's keys, fetched when this process holds none, holds stale ones, or is forced.

        `force` is what an unknown `kid` asks for, and it is floored against the last FORCED read
        rather than against the last read of any kind: the first unknown `kid` after a rotation has
        to be able to see the new keys immediately, and every one after it within
        `JWKS_REFETCH_FLOOR_SECONDS` gets what is held. So a rotation costs one request and a stream
        of tokens naming keys that never existed costs this issuer one request a minute.
        """
        now = time.monotonic()
        held = self.held
        if held is not None and not force and held.expires_at > now:
            return held
        if (
            held is not None
            and force
            and self.forced_at is not None
            and now - self.forced_at < JWKS_REFETCH_FLOOR_SECONDS
        ):
            return held
        if force:
            self.forced_at = now
        fetched = await fetch_published_keys(self.jwks_uri)
        self.held = fetched
        return fetched

    async def _decoded(self, token: str) -> jose_jwt.Token:
        """Check one token's signature against the issuer's keys, refetching once for an unknown `kid`."""
        held = await self.keys()
        try:
            return self._decoded_against(token, held)
        except InvalidKeyIdError:
            pass
        except MissingKeyError:
            pass
        refetched = await self.keys(force=True)
        try:
            return self._decoded_against(token, refetched)
        except (InvalidKeyIdError, MissingKeyError) as error:
            raise TokenRefusedError(
                "the token this request carried is signed with a key this server could not find among "
                f"the ones `{self.issuer}` publishes, even after reading them again"
            ) from error

    def _decoded_against(self, token: str, held: PublishedKeys) -> jose_jwt.Token:
        """One decode against one set of keys, with every refusal but an unknown `kid` answered here."""
        try:
            return jose_jwt.decode(token, held.key_set, algorithms=list(JWT_ALGORITHMS))
        except (InvalidKeyIdError, MissingKeyError):
            raise
        except BadSignatureError as error:
            raise TokenRefusedError(
                "the token this request carried is not signed by the key it names, so this server "
                "cannot tell who issued it"
            ) from error
        except (JoseError, ValueError) as error:
            raise TokenRefusedError(
                "the value in `Authorization` is not a JSON Web Token this server can read "
                f"({error}); this server takes a token `{self.issuer}` minted"
            ) from error

    def _claims_hold(self, claims: dict[str, Any]) -> None:
        """Check what one token says about itself: who issued it, when it is good for, and for whom.

        `aud` is an option only where an audience is configured, and that is the difference between
        "this token is for somebody else" and "this server takes whatever this issuer signed". A
        registry that always required it would refuse every issuer that mints audience-less tokens.
        """
        required: dict[str, ClaimsOption] = {
            "iss": {"essential": True, "value": self.issuer},
            "exp": {"essential": True},
        }
        if self.config.audience is not None:
            required["aud"] = {"essential": True, "value": self.config.audience}
        registry = jose_jwt.JWTClaimsRegistry(now=int(time.time()), leeway=CLOCK_LEEWAY_SECONDS, **required)
        try:
            registry.validate(claims)
        except JoseError as error:
            raise TokenRefusedError(self._claim_refusal(claims, error)) from error

    def _claim_refusal(self, claims: dict[str, Any], error: JoseError) -> str:
        """What a caller is told about a token whose signature held and whose claims did not."""
        stated_issuer = _string_claim(claims, "iss")
        if stated_issuer is not None and stated_issuer.rstrip("/") != self.issuer:
            return (
                f"the token this request carried was issued by `{stated_issuer}`, and this server takes "
                f"tokens from `{self.issuer}`"
            )
        if self.config.audience is not None and not _audience_holds(claims, self.config.audience):
            return (
                f"the token this request carried is not for this server: it names no audience `{self.config.audience}`"
            )
        return f"the token this request carried is not currently valid ({error})"

    def _named_caller(self, claims: dict[str, Any]) -> str:
        """The claim `[serve.jwt] username_claim` names, refusing a token that names nobody."""
        named = _string_claim(claims, self.config.username_claim)
        if named is None or named.strip() == "":
            raise TokenRefusedError(
                f"the token this request carried carries no `{self.config.username_claim}` claim, and this "
                "server records who captured every response; state the claim your issuer puts the "
                "username in as `[serve.jwt] username_claim`"
            )
        return named.strip()
Attributes
config instance-attribute

The [serve.jwt] table this run resolved - the issuer, the audience, and the claim to read.

issuer instance-attribute

The issuer identifier every token's iss is compared against, as the issuer publishes it.

jwks_uri instance-attribute

Where the keys come from, as /.well-known/openid-configuration named it.

held = Field(default=None, repr=False) class-attribute instance-attribute

The keys this process is currently verifying against, or None until the first fetch.

forced_at = None class-attribute instance-attribute

When an unknown kid last sent this process back to the issuer, or None while none has.

Methods:
verify(token) async

Accept one token, or say in one sentence why it is refused.

The order is the order a reader would check it in: the signature first, because an unsigned assertion's claims are not evidence of anything; then what the claims say; then the claim that names the caller. An unknown kid is the one case that goes back to the issuer - a rotation is the ordinary reason a signature arrives under a key this process does not hold, and refusing it without looking would make every rotation an outage.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
async def verify(self, token: str) -> VerifiedToken:
    """Accept one token, or say in one sentence why it is refused.

    The order is the order a reader would check it in: the signature first, because an unsigned
    assertion's claims are not evidence of anything; then what the claims say; then the claim
    that names the caller. An unknown `kid` is the one case that goes back to the issuer -
    a rotation is the ordinary reason a signature arrives under a key this process does not
    hold, and refusing it without looking would make every rotation an outage.
    """
    decoded = await self._decoded(token)
    self._claims_hold(decoded.claims)
    return VerifiedToken(
        username=self._named_caller(decoded.claims),
        subject=_string_claim(decoded.claims, "sub"),
        expires_at=_integer_claim(decoded.claims, "exp"),
    )
keys(*, force=False) async

The issuer's keys, fetched when this process holds none, holds stale ones, or is forced.

force is what an unknown kid asks for, and it is floored against the last FORCED read rather than against the last read of any kind: the first unknown kid after a rotation has to be able to see the new keys immediately, and every one after it within JWKS_REFETCH_FLOOR_SECONDS gets what is held. So a rotation costs one request and a stream of tokens naming keys that never existed costs this issuer one request a minute.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
async def keys(self, *, force: bool = False) -> PublishedKeys:
    """The issuer's keys, fetched when this process holds none, holds stale ones, or is forced.

    `force` is what an unknown `kid` asks for, and it is floored against the last FORCED read
    rather than against the last read of any kind: the first unknown `kid` after a rotation has
    to be able to see the new keys immediately, and every one after it within
    `JWKS_REFETCH_FLOOR_SECONDS` gets what is held. So a rotation costs one request and a stream
    of tokens naming keys that never existed costs this issuer one request a minute.
    """
    now = time.monotonic()
    held = self.held
    if held is not None and not force and held.expires_at > now:
        return held
    if (
        held is not None
        and force
        and self.forced_at is not None
        and now - self.forced_at < JWKS_REFETCH_FLOOR_SECONDS
    ):
        return held
    if force:
        self.forced_at = now
    fetched = await fetch_published_keys(self.jwks_uri)
    self.held = fetched
    return fetched

Functions:

discover_issuer(config) async

Read one issuer's discovery document and its keys, or refuse the run that asked for it.

Both reads happen while the server starts, and both have to succeed. An issuer that cannot be reached is a posture this process cannot honour, and a facade that started anyway would refuse every caller for a reason none of them could act on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
async def discover_issuer(config: ServeJwtConfig) -> JwtVerifier:
    """Read one issuer's discovery document and its keys, or refuse the run that asked for it.

    Both reads happen while the server starts, and both have to succeed. An issuer that cannot be
    reached is a posture this process cannot honour, and a facade that started anyway would refuse
    every caller for a reason none of them could act on.
    """
    if config.issuer is None:
        raise OidcIssuerUnavailableError("no issuer is configured")
    discovery = await fetch_issuer_discovery(config.issuer)
    verifier = JwtVerifier(config=config, issuer=discovery.issuer.rstrip("/"), jwks_uri=discovery.jwks_uri)
    verifier.held = await fetch_published_keys(discovery.jwks_uri)
    return verifier

fetch_issuer_discovery(issuer) async

Read {issuer}/.well-known/openid-configuration, which is where the keys are named.

The issuer the document states is taken over the one that was configured, because that is the value its tokens carry as iss - an issuer reached at one URL and identifying itself as another is a deployment behind a proxy, and comparing against what it says about itself is what makes that work. It has to identify itself as something, and a document that names no jwks_uri names no keys, so both are required fields on the model this parses into.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
async def fetch_issuer_discovery(issuer: str) -> OidcDiscovery:
    """Read `{issuer}/.well-known/openid-configuration`, which is where the keys are named.

    The issuer the document states is taken over the one that was configured, because that is the
    value its tokens carry as `iss` - an issuer reached at one URL and identifying itself as another
    is a deployment behind a proxy, and comparing against what it says about itself is what makes
    that work. It has to identify itself as something, and a document that names no `jwks_uri` names
    no keys, so both are required fields on the model this parses into.
    """
    url = issuer.rstrip("/") + DISCOVERY_PATH
    try:
        async with httpx2.AsyncClient(timeout=ISSUER_TIMEOUT_SECONDS, follow_redirects=True) as http:
            answer = await http.get(url, headers={"Accept": "application/json"})
    except httpx2.HTTPError as error:
        raise OidcIssuerUnavailableError(f"`{url}` could not be read ({error})") from error
    if answer.status_code >= 400:
        raise OidcIssuerUnavailableError(f"`{url}` answered {answer.status_code}")
    try:
        return OidcDiscovery.model_validate(answer.json())
    except ValueError as error:
        raise OidcIssuerUnavailableError(
            f"`{url}` did not answer with an OpenID Connect configuration naming an issuer and a `jwks_uri` ({error})"
        ) from error

fetch_published_keys(jwks_uri) async

Read one JWKS document and hold it for as long as its own Cache-Control asks, within the floor.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
async def fetch_published_keys(jwks_uri: str) -> PublishedKeys:
    """Read one JWKS document and hold it for as long as its own `Cache-Control` asks, within the floor."""
    try:
        async with httpx2.AsyncClient(timeout=ISSUER_TIMEOUT_SECONDS, follow_redirects=True) as http:
            answer = await http.get(jwks_uri, headers={"Accept": "application/json"})
    except httpx2.HTTPError as error:
        raise OidcIssuerUnavailableError(f"`{jwks_uri}` could not be read ({error})") from error
    if answer.status_code >= 400:
        raise OidcIssuerUnavailableError(f"`{jwks_uri}` answered {answer.status_code}")
    try:
        key_set = KeySet.import_key_set(answer.json())
    except (ValueError, KeyError, JoseError) as error:
        raise OidcIssuerUnavailableError(f"`{jwks_uri}` did not answer with a JSON Web Key Set ({error})") from error
    now = time.monotonic()
    return PublishedKeys(key_set=key_set, fetched_at=now, expires_at=now + cache_seconds(answer.headers))

cache_seconds(headers)

How long one JWKS answer is held: what it asked for, never below JWKS_MINIMUM_CACHE_SECONDS.

The floor is the whole of the policy. An issuer that sends max-age=0 - which several do, out of caution about a document that is not secret - would otherwise make every verified request a round trip to that issuer, which is slower than the introspection call this design exists to avoid. There is no ceiling, because a rotation is caught by the unknown-kid refetch rather than by an expiry nobody set.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/oidc.py
def cache_seconds(headers: httpx2.Headers) -> float:
    """How long one JWKS answer is held: what it asked for, never below `JWKS_MINIMUM_CACHE_SECONDS`.

    The floor is the whole of the policy. An issuer that sends `max-age=0` - which several do, out
    of caution about a document that is not secret - would otherwise make every verified request a
    round trip to that issuer, which is slower than the introspection call this design exists to
    avoid. There is no ceiling, because a rotation is caught by the unknown-`kid` refetch rather
    than by an expiry nobody set.
    """
    stated = _max_age(headers.get("cache-control", ""))
    if stated is None:
        return JWKS_DEFAULT_CACHE_SECONDS
    return max(stated, JWKS_MINIMUM_CACHE_SECONDS)

Credential pass-through

Under [serve] auth = "dhis2" on a live run - and under "jwt" with [serve.jwt] forward_bearer on - a register read carries the caller's own Authorization header to the instance, opaque and unparsed, so DHIS2 applies its sharing, organisation unit scopes, ownership, and access levels to the person who actually asked. The reads share one pooled connection held on the runtime for the life of the process, and that pool carries no credential of its own - CallerCredentialReader is the per-request pairing of it with one caller's header, and nothing on the path is cached. The startup store build, /facade/uiconfig, and the forward drain are not on it: none of them acts on behalf of a request. Under jwt with forward_bearer off there is no caller channel at all, and the register answers 501 rather than falling back to the facade's own profile.

passthrough

Reading DHIS2 as the caller: the header a register read forwards, and the reads that forward it.

THE POINT OF THE dhis2 POSTURE IS NOT THE 401. Checking a caller against GET /api/me says who is asking; it says nothing about what they may see. A facade that authenticates every caller and then reads the instance as its own configured profile answers every one of them with that profile's rights - so a deployment whose facade profile is an administrator hands each caller the whole register, and DHIS2 skips its ownership and access-level model for a superuser without writing the break-the-glass audit entry that would have recorded it.

So under dhis2, on a live run, a register read carries THE CALLER'S OWN Authorization HEADER to the instance, verbatim. DHIS2 then enforces its five gates - authority, sharing, the data-element bits, the three organisation-unit scopes, and ownership with access levels - against the person who actually asked, which is the only place those gates can be enforced correctly. This facade computes no permissions of its own and reimplements none of DHIS2's.

THE HEADER IS OPAQUE. It is read out of the request, put on the outgoing request, and never parsed, never logged, and never held anywhere a later request can reach. Basic and ApiToken are what dhis2w_fhir_serve.auth accepts, and this module does not care which arrived - what DHIS2 accepts is DHIS2's business, and a facade that inspected the credential would be a facade that could get the inspection wrong.

THE jwt POSTURE RIDES THE SAME PATH, AND ONLY WHEN IT WAS TOLD TO. A token an external issuer minted is a credential DHIS2 accepts exactly when the instance was configured to trust that same issuer, through DHIS2's own oidc.jwt.token.authentication.enabled - which is a fact about the instance that this facade cannot read and must not guess. So [serve.jwt] forward_bearer states it. True and the Bearer header is forwarded exactly as Basic is: the same opaque header, the same credential-free pool, the same DHIS2 gates enforced against the person who asked. False - the default - and the live register is not answered at all, with RegisterNotForwardableError naming what would make it answerable. THE ONE THING IT NEVER DOES IS FALL BACK TO THE FACADE'S OWN PROFILE: that would answer every caller with that profile's rights, and under an administrator profile DHIS2 skips its ownership and access-level model outright without writing a break-the-glass audit entry. A loud refusal is the only honest answer to "this caller cannot be authorized here".

WHICH READS. Exactly the register reads a caller asks for: the tracked entity read, the identifier search, the listing and its counts, the enrollment listing, and the entity /facade/evaluate names as its context. dhis2w_fhir_serve.register.wire takes a RegisterReader rather than a Dhis2Client precisely so that one channel can be swapped per request.

WHICH READS ARE NOT. The startup store build, /facade/uiconfig's instance address, and d2w fhir forward's drain read and write as the facade's own profile, and that is correct: none of them acts on behalf of a request. The store is one snapshot of the published guide, shared by every caller and holding no tracked entity data; the drain is the deployment's own act under the forwarding profile. Those are the paths docs/fhir/301-serving.md still asks for a least-privilege DHIS2 user for.

NOTHING ON THIS PATH IS CACHED. One caller's page is never another caller's page, so there is nothing to share and the reader keeps nothing between requests. The one cache the dhis2 posture holds is dhis2w_fhir_serve.auth's, it is keyed by a hash of the header, and what it holds is a username - an identity, never a resource.

THE CONNECTION IS POOLED, THE CREDENTIAL IS NOT. Opening a TCP connection and a TLS session per register read would make the facade slower than the instance it fronts, so one httpx2.AsyncClient is held open for the life of the process, pointed at the instance, WITH NO AUTHENTICATION OF ITS OWN. CallerCredentialReader is the per-request pairing of that pool with one caller's header, and the pool has no credential to fall back to if a request ever arrived without one.

THE FACADE NAMES ITSELF ON THE WAY THROUGH. Every pass-through read carries X-DHIS2W-Facade, this software and its version, as a default header on the pool. It is provenance for whoever reads the DHIS2 access log - "this arrived through the FHIR facade" - and it is deliberately not the username: the caller's own header already carries the identity, and a second copy of it in a header nobody authenticates would be an assertion this server has no business making.

Attributes

Classes

UpstreamRefusalError

Bases: ServeError

DHIS2 refused the caller's own credentials on a pass-through read, and its verdict is answered as it stands.

Not an UpstreamError: a 502 would say this server could not reach the instance, when the instance answered clearly and the answer was about the caller. Inventing a status DHIS2 never sent would be worse still, so the status is carried rather than chosen.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
class UpstreamRefusalError(ServeError):
    """DHIS2 refused the caller's own credentials on a pass-through read, and its verdict is answered as it stands.

    Not an `UpstreamError`: a 502 would say this server could not reach the instance, when the
    instance answered clearly and the answer was about the caller. Inventing a status DHIS2 never
    sent would be worse still, so the status is carried rather than chosen.
    """

    def __init__(self, status_code: int, issue_code: IssueCode, challenge: str | None = None) -> None:
        super().__init__(
            f"the DHIS2 instance behind this server refused this read under the credentials this request "
            f"carried (it answered {status_code})"
        )
        self.status_code = status_code
        self.issue_code = issue_code
        self.challenge = challenge

    def response_headers(self) -> dict[str, str]:
        """The `WWW-Authenticate` challenge a 401 has to carry, and nothing on a 403."""
        return {} if self.challenge is None else {"WWW-Authenticate": self.challenge}
Methods:
response_headers()

The WWW-Authenticate challenge a 401 has to carry, and nothing on a 403.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
def response_headers(self) -> dict[str, str]:
    """The `WWW-Authenticate` challenge a 401 has to carry, and nothing on a 403."""
    return {} if self.challenge is None else {"WWW-Authenticate": self.challenge}

PassThroughUnavailableError

Bases: ServeError

The posture answers the register under each caller's credentials, and this process holds no way to.

Loud rather than silent, because the silent alternative is reading the instance as the facade's own profile - which is the whole thing this posture exists to stop.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
class PassThroughUnavailableError(ServeError):
    """The posture answers the register under each caller's credentials, and this process holds no way to.

    Loud rather than silent, because the silent alternative is reading the instance as the facade's
    own profile - which is the whole thing this posture exists to stop.
    """

    def __init__(self) -> None:
        super().__init__(
            "this server answers the register under each caller's own DHIS2 credentials and holds no "
            "connection to do it over: the runtime was attached without the pass-through client "
            "`open_serve_runtime` opens for the `dhis2` posture"
        )

RegisterNotForwardableError

Bases: ServeError

The jwt posture is running with forward_bearer off, so the live register is not answered.

501 rather than 401 or 404, because none of those is what happened. The caller authenticated perfectly well, the resource type exists, and this server simply does not implement reading the register under this configuration - which is what 501 says. The one status it must never be is a 200 read as somebody else, and the one behaviour it must never have is a silent fall back to the facade's profile; the module docstring above says why in full.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
class RegisterNotForwardableError(ServeError):
    """The `jwt` posture is running with `forward_bearer` off, so the live register is not answered.

    501 rather than 401 or 404, because none of those is what happened. The caller authenticated
    perfectly well, the resource type exists, and this server simply does not implement reading the
    register under this configuration - which is what 501 says. The one status it must never be is a
    200 read as somebody else, and the one behaviour it must never have is a silent fall back to the
    facade's profile; the module docstring above says why in full.
    """

    status_code = 501
    issue_code = "not-supported"

    def __init__(self) -> None:
        super().__init__(
            "this server takes a token from an OpenID Connect issuer and will not read the register as "
            "anybody but the caller who asked. Answering it needs two things stated together: "
            "`[serve.jwt] forward_bearer = true` here, and a DHIS2 instance configured to trust the same "
            "issuer (`oidc.jwt.token.authentication.enabled`), so the token you presented is one DHIS2 "
            "resolves to a user of its own. Until both are true, the published guide, the received "
            "responses, and every operation over them are served as they always were."
        )

RegisterReader

Bases: Protocol

What a register read needs of whatever it reads DHIS2 through: one raw GET answering parsed JSON.

Dhis2Client satisfies it as it stands, which is what lets the none and token postures keep reading through the runtime's own connection with nothing in register.wire branching on posture.

Runtime-checkable so a model can hold one as a field: dhis2w_fhir_serve.projection names a backend's connection in its shape, and a pydantic field over an arbitrary type is validated by isinstance, which a plain Protocol cannot answer.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
@runtime_checkable
class RegisterReader(Protocol):
    """What a register read needs of whatever it reads DHIS2 through: one raw GET answering parsed JSON.

    `Dhis2Client` satisfies it as it stands, which is what lets the `none` and `token` postures keep
    reading through the runtime's own connection with nothing in `register.wire` branching on posture.

    Runtime-checkable so a model can hold one as a field: `dhis2w_fhir_serve.projection` names a
    backend's connection in its shape, and a pydantic field over an arbitrary type is validated by
    `isinstance`, which a plain Protocol cannot answer.
    """

    async def get_raw(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        """Read one DHIS2 path, answering the parsed JSON body."""
        ...
Methods:
get_raw(path, params=None) async

Read one DHIS2 path, answering the parsed JSON body.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
async def get_raw(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
    """Read one DHIS2 path, answering the parsed JSON body."""
    ...

CallerCredentialReader

Bases: BaseModel

One caller's Authorization header paired with the process's pooled connection, for one request.

Built per request and dropped with it. The header is repr=False so a model that ends up in a log line or a traceback frame prints no credential.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
class CallerCredentialReader(BaseModel):
    """One caller's `Authorization` header paired with the process's pooled connection, for one request.

    Built per request and dropped with it. The header is `repr=False` so a model that ends up in a
    log line or a traceback frame prints no credential.
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    connection: httpx2.AsyncClient
    """The pool this facade holds open against the instance, which carries no credential of its own."""

    authorization: str = Field(repr=False)
    """The caller's header value, verbatim - never parsed, never logged, never held past this request."""

    challenge: str
    """What a 401 the instance gives back is answered with, which is this run's own posture's challenge.

    Carried rather than derived, because the credential itself is opaque here: this reader cannot
    tell a forwarded `Basic` from a forwarded `Bearer` without parsing the one thing it promises
    never to parse. The posture that built it knows, so the posture that built it states it.
    """

    async def get_raw(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
        """Read one DHIS2 path as the caller, forwarding their header and none of the runtime's.

        The refusals are shaped so every caller of `register.wire` keeps working unchanged: a 404 is
        a `Dhis2ApiError` the read folds into "nothing here", a 400 naming an absent tracked entity
        type is the one the surface folds into an empty answer, and a transport failure is a
        `Dhis2ClientError` the routes answer 502 to. The two verdicts about the CALLER - 401 and 403 -
        leave as themselves.
        """
        try:
            answer = await self.connection.get(path, params=params, headers=self._headers())
        except httpx2.HTTPError as error:
            raise Dhis2ClientError(f"the DHIS2 instance could not be read as the caller ({error})") from error
        issue_code = PASSED_THROUGH_REFUSALS.get(answer.status_code)
        if issue_code is not None:
            raise UpstreamRefusalError(
                answer.status_code,
                issue_code,
                self.challenge if answer.status_code == 401 else None,
            )
        if answer.status_code >= 400:
            raise Dhis2ApiError(status_code=answer.status_code, message=answer.reason_phrase, body=_error_body(answer))
        return _parsed_body(answer)

    def _headers(self) -> dict[str, str]:
        """What one read carries beyond the pool's own: the caller's credential, and what it wants back."""
        return {"Authorization": self.authorization, "Accept": PASS_THROUGH_ACCEPT}
Attributes
connection instance-attribute

The pool this facade holds open against the instance, which carries no credential of its own.

authorization = Field(repr=False) class-attribute instance-attribute

The caller's header value, verbatim - never parsed, never logged, never held past this request.

challenge instance-attribute

What a 401 the instance gives back is answered with, which is this run's own posture's challenge.

Carried rather than derived, because the credential itself is opaque here: this reader cannot tell a forwarded Basic from a forwarded Bearer without parsing the one thing it promises never to parse. The posture that built it knows, so the posture that built it states it.

Methods:
get_raw(path, params=None) async

Read one DHIS2 path as the caller, forwarding their header and none of the runtime's.

The refusals are shaped so every caller of register.wire keeps working unchanged: a 404 is a Dhis2ApiError the read folds into "nothing here", a 400 naming an absent tracked entity type is the one the surface folds into an empty answer, and a transport failure is a Dhis2ClientError the routes answer 502 to. The two verdicts about the CALLER - 401 and 403 - leave as themselves.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
async def get_raw(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
    """Read one DHIS2 path as the caller, forwarding their header and none of the runtime's.

    The refusals are shaped so every caller of `register.wire` keeps working unchanged: a 404 is
    a `Dhis2ApiError` the read folds into "nothing here", a 400 naming an absent tracked entity
    type is the one the surface folds into an empty answer, and a transport failure is a
    `Dhis2ClientError` the routes answer 502 to. The two verdicts about the CALLER - 401 and 403 -
    leave as themselves.
    """
    try:
        answer = await self.connection.get(path, params=params, headers=self._headers())
    except httpx2.HTTPError as error:
        raise Dhis2ClientError(f"the DHIS2 instance could not be read as the caller ({error})") from error
    issue_code = PASSED_THROUGH_REFUSALS.get(answer.status_code)
    if issue_code is not None:
        raise UpstreamRefusalError(
            answer.status_code,
            issue_code,
            self.challenge if answer.status_code == 401 else None,
        )
    if answer.status_code >= 400:
        raise Dhis2ApiError(status_code=answer.status_code, message=answer.reason_phrase, body=_error_body(answer))
    return _parsed_body(answer)

Functions:

open_pass_through_client(base_url, *, provenance) async

Open the connection pass-through reads share, pointed at the instance and holding no credential.

No auth= and no Authorization in the default headers, so a request that somehow reached this pool without a caller's header would be an anonymous request to DHIS2 rather than a request as the facade. The provenance header is a property of the process rather than of a request, so it rides on the pool.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
@asynccontextmanager
async def open_pass_through_client(base_url: str, *, provenance: str) -> AsyncGenerator[httpx2.AsyncClient]:
    """Open the connection pass-through reads share, pointed at the instance and holding no credential.

    No `auth=` and no `Authorization` in the default headers, so a request that somehow reached this
    pool without a caller's header would be an anonymous request to DHIS2 rather than a request as
    the facade. The provenance header is a property of the process rather than of a request, so it
    rides on the pool.
    """
    async with httpx2.AsyncClient(
        base_url=base_url,
        timeout=PASS_THROUGH_TIMEOUT_SECONDS,
        headers={FACADE_PROVENANCE_HEADER: provenance},
    ) as connection:
        yield connection

register_reader(request) async

What one register read runs over: the caller's own credentials where they can be, else the runtime's client.

None is the compiled run, and it is the same None live_client answers - a process with no instance behind it has nothing to read a register from, whatever posture it serves under.

Three answers, and the middle one is the one worth reading twice. Under dhis2 - and under jwt with [serve.jwt] forward_bearer = true - the read is answered as the caller. Under jwt with forward_bearer off it is refused rather than answered as the facade. Under none and token it runs on the runtime's own client, which is the posture those two always were: they decide who may ask and nothing about what an answer contains.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
async def register_reader(request: Request) -> RegisterReader | None:
    """What one register read runs over: the caller's own credentials where they can be, else the runtime's client.

    None is the compiled run, and it is the same None `live_client` answers - a process with no
    instance behind it has nothing to read a register from, whatever posture it serves under.

    Three answers, and the middle one is the one worth reading twice. Under `dhis2` - and under `jwt`
    with `[serve.jwt] forward_bearer = true` - the read is answered as the caller. Under `jwt` with
    `forward_bearer` off it is refused rather than answered as the facade. Under `none` and `token`
    it runs on the runtime's own client, which is the posture those two always were: they decide who
    may ask and nothing about what an answer contains.
    """
    runtime_connection = live_client(request)
    if runtime_connection is None:
        return None
    settings = serve_context(request).settings
    if settings.auth is ServeAuth.JWT:
        if not settings.jwt.forward_bearer:
            raise RegisterNotForwardableError
        return await caller_reader(request)
    if settings.auth is not ServeAuth.DHIS2:
        return runtime_connection
    return await caller_reader(request)

caller_reader(request) async

Bind one request's own Authorization to the pooled connection, refusing a request that carries none.

A register read under a forwarding posture is answered under the credentials of whoever asked, so a request that presented none has nothing to be answered under. That is a 401 rather than a fall back to the runtime's client: falling back would answer an anonymous caller with the facade profile's rights, which is exactly the read these postures exist to stop.

[serve] auth_scope = "write" leaves reads unguarded, so a register read can be the first thing on a request that has established nobody. It establishes them here, through the same check the guarded routes use - and the same brief cache, where the posture has one - rather than refusing a caller who presented perfectly good credentials on an address that had not been told to look at them.

The identity has to have been established by the posture this run serves. A dhis2 identity on a jwt run is not a thing that can happen, and checking it is how this function stays true when a fourth posture arrives: the credential it forwards is only ever one this server itself accepted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/passthrough.py
async def caller_reader(request: Request) -> CallerCredentialReader:
    """Bind one request's own `Authorization` to the pooled connection, refusing a request that carries none.

    A register read under a forwarding posture is answered under the credentials of whoever asked, so
    a request that presented none has nothing to be answered under. That is a 401 rather than a fall
    back to the runtime's client: falling back would answer an anonymous caller with the facade
    profile's rights, which is exactly the read these postures exist to stop.

    `[serve] auth_scope = "write"` leaves reads unguarded, so a register read can be the first thing
    on a request that has established nobody. It establishes them here, through the same check the
    guarded routes use - and the same brief cache, where the posture has one - rather than refusing a
    caller who presented perfectly good credentials on an address that had not been told to look at
    them.

    The identity has to have been established by the posture this run serves. A `dhis2` identity on a
    `jwt` run is not a thing that can happen, and checking it is how this function stays true when a
    fourth posture arrives: the credential it forwards is only ever one this server itself accepted.
    """
    posture = serve_context(request).settings.auth
    if request_identity(request) is None:
        await require_authenticated(request)
    identity = request_identity(request)
    presented = request.headers.get(AUTHORIZATION_HEADER, "").strip()
    if identity is None or identity.posture is not posture or presented == "":
        raise UnauthenticatedError(
            "this server answers the register under the DHIS2 authorization of whoever asks, and this "
            "request presented no credential to answer it under",
            challenge_for(posture, serve_context(request).settings.jwt.issuer),
        )
    connection = caller_client(request)
    if connection is None:
        raise PassThroughUnavailableError
    return CallerCredentialReader(
        connection=connection,
        authorization=presented,
        challenge=challenge_for(posture, serve_context(request).settings.jwt.issuer),
    )

Resource store

The compiled IG merged with the predefined resource tree, indexed by (resourceType, id) and by canonical url. Resources are held as the bytes they were written as, so what a client reads back is what the project publishes.

store

The IG resource store: every resource the facade serves, loaded once and indexed for read and search.

A store holds two trees merged into one collection. ig/fsh-generated/resources is what SUSHI compiled from the emitted FSH, and ig/input/resources is the predefined registry, terminology, and category tree the project committed by hand - SUSHI never re-emits those, so a store built from the compiled tree alone would serve a partial IG.

Resources are held as the bytes they were written as. The store parses just enough of each one to index it (resourceType, id, url, identifier[]) and passes the rest through untouched, so what a FHIR client reads back is exactly what the project publishes. ConceptMap is the one type read further than its index: $translate answers off mappings, not off a document, so the stored maps are parsed into their R4 models at load and held alongside the entries.

GUIDE_CONFORMANCE_RESOURCE_TYPES is the one set of types this module names for a reason other than indexing. They ride the compiled tree like everything else, and load_compiled_conformance_entries reads them out of it on their own so that a live store - built from a DHIS2 instance, holding no definitional layer of its own - hosts the same guide the compiled one does.

This module knows nothing about DHIS2 - a live store is built elsewhere and lands in the same ResourceStore shape.

Attributes

BUILTIN_CONFORMANCE_DIRECTORY = 'conformance' module-attribute

Where this package keeps the conformance resources that are the facade's own, not any guide's.

Classes

CompiledIgMissingError

Bases: LookupError

Raised when a project has no compiled IG to serve.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class CompiledIgMissingError(LookupError):
    """Raised when a project has no compiled IG to serve."""

    def __init__(self) -> None:
        super().__init__(
            "no compiled IG at ig/fsh-generated/resources - run `d2w fhir generate`, "
            "then `make sushi` in the project, and serve again."
        )

IdentifierToken

Bases: BaseModel

One system|value search token, with system=None standing for the value in any system.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class IdentifierToken(BaseModel):
    """One `system|value` search token, with `system=None` standing for the value in any system."""

    model_config = ConfigDict(frozen=True)

    system: str | None = None
    value: str

StoreEntry

Bases: BaseModel

One served resource: the index fields the facade reads it by, plus the resource itself.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class StoreEntry(BaseModel):
    """One served resource: the index fields the facade reads it by, plus the resource itself."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    resource_id: str
    canonical_url: str | None = None
    identifiers: tuple[IdentifierToken, ...] = ()
    source: str
    """Posix path of the file this entry was read from, or the `live` marker for a built store."""

    body: dict[str, Any]
    """The resource verbatim - the one deliberate `dict[str, Any]` in this package.

    An IG holds resource types this repo has no models for (StructureDefinition, ImplementationGuide,
    whatever a project hand-writes into `input/resources`), and the server's contract is byte-faithful
    passthrough: modelling a subset would silently drop the rest. That is what lets the conformance
    resources be served without a model apiece - a profile is answered as the bytes SUSHI wrote. The
    dict leaves the store only as an HTTP response body, never as an argument another layer reads
    fields off.
    """
Attributes
source instance-attribute

Posix path of the file this entry was read from, or the live marker for a built store.

body instance-attribute

The resource verbatim - the one deliberate dict[str, Any] in this package.

An IG holds resource types this repo has no models for (StructureDefinition, ImplementationGuide, whatever a project hand-writes into input/resources), and the server's contract is byte-faithful passthrough: modelling a subset would silently drop the rest. That is what lets the conformance resources be served without a model apiece - a profile is answered as the bytes SUSHI wrote. The dict leaves the store only as an HTTP response body, never as an argument another layer reads fields off.

SearchQuery

Bases: BaseModel

The search parameters the facade supports, in FHIR's combination semantics.

Within one field the values OR (_id=a,b matches either), across fields they AND (_id=a&url=x matches only the resource that is both). An empty query matches every resource of the searched type. An identifier token with system=None matches the value in any system, mirroring a bare identifier=value search.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class SearchQuery(BaseModel):
    """The search parameters the facade supports, in FHIR's combination semantics.

    Within one field the values OR (`_id=a,b` matches either), across fields they AND
    (`_id=a&url=x` matches only the resource that is both). An empty query matches every
    resource of the searched type. An identifier token with `system=None` matches the value
    in any system, mirroring a bare `identifier=value` search.
    """

    model_config = ConfigDict(frozen=True)

    ids: tuple[str, ...] = ()
    urls: tuple[str, ...] = ()
    identifiers: tuple[IdentifierToken, ...] = ()

    def is_empty(self) -> bool:
        """True when no parameter was given, so the query matches every resource of the type."""
        return not (self.ids or self.urls or self.identifiers)
Methods:
is_empty()

True when no parameter was given, so the query matches every resource of the type.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def is_empty(self) -> bool:
    """True when no parameter was given, so the query matches every resource of the type."""
    return not (self.ids or self.urls or self.identifiers)

StoreSummary

Bases: BaseModel

How many resources of each type the store holds - the shape a capability or status page reports.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class StoreSummary(BaseModel):
    """How many resources of each type the store holds - the shape a capability or status page reports."""

    model_config = ConfigDict(frozen=True)

    counts_by_type: dict[str, int] = Field(default_factory=dict)

    @property
    def total(self) -> int:
        """Total resources across every type."""
        return sum(self.counts_by_type.values())
Attributes
total property

Total resources across every type.

ResourceStore

Bases: BaseModel

Every resource the facade serves, indexed by (resourceType, id) and by canonical url.

The entry tuple is the load order - compiled resources first, then the predefined tree - and the indexes are built once in model_post_init. When both trees carry the same (resourceType, id) the first one loaded wins, so a compiled resource is never shadowed.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
class ResourceStore(BaseModel):
    """Every resource the facade serves, indexed by `(resourceType, id)` and by canonical url.

    The entry tuple is the load order - compiled resources first, then the predefined tree - and
    the indexes are built once in `model_post_init`. When both trees carry the same
    `(resourceType, id)` the first one loaded wins, so a compiled resource is never shadowed.
    """

    model_config = ConfigDict(frozen=True)

    entries: tuple[StoreEntry, ...] = ()

    _by_type_and_id: dict[tuple[str, str], StoreEntry] = PrivateAttr(default_factory=dict)
    _by_canonical: dict[str, StoreEntry] = PrivateAttr(default_factory=dict)
    _concept_maps: tuple[ConceptMap, ...] = PrivateAttr(default=())

    def model_post_init(self, context: Any, /) -> None:
        """Build the read indexes (private attributes stay settable on a frozen model)."""
        for entry in self.entries:
            self._by_type_and_id.setdefault((entry.resource_type, entry.resource_id), entry)
            if entry.canonical_url is not None:
                self._by_canonical.setdefault(entry.canonical_url, entry)
        self._concept_maps = self._parse_concept_maps()

    def by_type_and_id(self, resource_type: str, resource_id: str) -> StoreEntry | None:
        """The resource a `GET /{type}/{id}` read resolves to, or None."""
        return self._by_type_and_id.get((resource_type, resource_id))

    def by_canonical(self, canonical_url: str) -> StoreEntry | None:
        """The resource a canonical url resolves to, whatever its type, or None."""
        return self._by_canonical.get(canonical_url)

    def search(self, resource_type: str, query: SearchQuery) -> tuple[StoreEntry, ...]:
        """Every resource of `resource_type` matching the query, in load order."""
        candidates = [entry for entry in self.entries if entry.resource_type == resource_type]
        if query.is_empty():
            return tuple(candidates)
        return tuple(entry for entry in candidates if self._matches(entry, query))

    def concept_maps(self) -> tuple[ConceptMap, ...]:
        """Every ConceptMap the store holds, as the R4 models `$translate` reads its mappings off."""
        return self._concept_maps

    def types_present(self) -> tuple[str, ...]:
        """Every resource type the store holds, sorted."""
        return tuple(sorted({entry.resource_type for entry in self.entries}))

    def summary(self) -> StoreSummary:
        """Resource counts per type."""
        counts = Counter(entry.resource_type for entry in self.entries)
        return StoreSummary(counts_by_type=dict(sorted(counts.items())))

    def _parse_concept_maps(self) -> tuple[ConceptMap, ...]:
        """Parse the stored ConceptMaps once, at load, so `$translate` reads models rather than documents.

        A ConceptMap the R4 model cannot read - an IG is free to hand-write elements this package
        does not serve - is left out and named in the log, so one unreadable document costs its own
        mappings rather than the whole operation.
        """
        parsed: list[ConceptMap] = []
        for entry in self.entries:
            if entry.resource_type != CONCEPT_MAP_RESOURCE_TYPE:
                continue
            try:
                parsed.append(ConceptMap.model_validate(entry.body))
            except ValidationError as error:
                logger.warning("%s: ConceptMap holds elements this server cannot read (%s)", entry.source, error)
        return tuple(parsed)

    @staticmethod
    def _matches(entry: StoreEntry, query: SearchQuery) -> bool:
        """AND every given field of the query, OR the values within each one."""
        if query.ids and entry.resource_id not in query.ids:
            return False
        if query.urls and entry.canonical_url not in query.urls:
            return False
        return not query.identifiers or any(
            ResourceStore._matches_identifier(entry, token) for token in query.identifiers
        )

    @staticmethod
    def _matches_identifier(entry: StoreEntry, token: IdentifierToken) -> bool:
        """A token matches when the value matches and the system matches, or the token names no system."""
        return any(
            held.value == token.value and (token.system is None or held.system == token.system)
            for held in entry.identifiers
        )
Methods:
model_post_init(context)

Build the read indexes (private attributes stay settable on a frozen model).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def model_post_init(self, context: Any, /) -> None:
    """Build the read indexes (private attributes stay settable on a frozen model)."""
    for entry in self.entries:
        self._by_type_and_id.setdefault((entry.resource_type, entry.resource_id), entry)
        if entry.canonical_url is not None:
            self._by_canonical.setdefault(entry.canonical_url, entry)
    self._concept_maps = self._parse_concept_maps()
by_type_and_id(resource_type, resource_id)

The resource a GET /{type}/{id} read resolves to, or None.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def by_type_and_id(self, resource_type: str, resource_id: str) -> StoreEntry | None:
    """The resource a `GET /{type}/{id}` read resolves to, or None."""
    return self._by_type_and_id.get((resource_type, resource_id))
by_canonical(canonical_url)

The resource a canonical url resolves to, whatever its type, or None.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def by_canonical(self, canonical_url: str) -> StoreEntry | None:
    """The resource a canonical url resolves to, whatever its type, or None."""
    return self._by_canonical.get(canonical_url)
search(resource_type, query)

Every resource of resource_type matching the query, in load order.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def search(self, resource_type: str, query: SearchQuery) -> tuple[StoreEntry, ...]:
    """Every resource of `resource_type` matching the query, in load order."""
    candidates = [entry for entry in self.entries if entry.resource_type == resource_type]
    if query.is_empty():
        return tuple(candidates)
    return tuple(entry for entry in candidates if self._matches(entry, query))
concept_maps()

Every ConceptMap the store holds, as the R4 models $translate reads its mappings off.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def concept_maps(self) -> tuple[ConceptMap, ...]:
    """Every ConceptMap the store holds, as the R4 models `$translate` reads its mappings off."""
    return self._concept_maps
types_present()

Every resource type the store holds, sorted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def types_present(self) -> tuple[str, ...]:
    """Every resource type the store holds, sorted."""
    return tuple(sorted({entry.resource_type for entry in self.entries}))
summary()

Resource counts per type.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def summary(self) -> StoreSummary:
    """Resource counts per type."""
    counts = Counter(entry.resource_type for entry in self.entries)
    return StoreSummary(counts_by_type=dict(sorted(counts.items())))

Functions:

load_compiled_store(project)

Read a project's compiled IG plus its predefined resource tree into a store.

The load is strict: a file that is not a JSON object, or that carries no string resourceType and id, fails loudly naming the file rather than being skipped, because a resource the store silently drops reads to a client as a resource the IG never published.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def load_compiled_store(project: FhirProject) -> ResourceStore:
    """Read a project's compiled IG plus its predefined resource tree into a store.

    The load is strict: a file that is not a JSON object, or that carries no string `resourceType`
    and `id`, fails loudly naming the file rather than being skipped, because a resource the store
    silently drops reads to a client as a resource the IG never published.
    """
    compiled_directory = project.ig_directory / "fsh-generated" / "resources"
    compiled_paths = sorted(compiled_directory.glob("*.json")) if compiled_directory.is_dir() else []
    if not compiled_paths:
        raise CompiledIgMissingError

    predefined_directory = project.resources_directory
    predefined_paths = sorted(predefined_directory.rglob("*.json")) if predefined_directory.is_dir() else []

    entries = [_read_entry(path, project.project_root) for path in [*compiled_paths, *predefined_paths]]
    return ResourceStore(entries=tuple(entries))

builtin_conformance_entries()

The conformance resources this package itself publishes, served by every run.

One today: the $evaluate OperationDefinition, whose canonical the CapabilityStatement names. The facade never names a canonical it cannot answer for, so what /metadata points at is readable here and searchable by url exactly like the guide's own definitions. Product-level rather than per-guide, which is why these ride the package instead of the compiled tree.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def builtin_conformance_entries() -> tuple[StoreEntry, ...]:
    """The conformance resources this package itself publishes, served by every run.

    One today: the `$evaluate` OperationDefinition, whose canonical the CapabilityStatement names.
    The facade never names a canonical it cannot answer for, so what /metadata points at is readable
    here and searchable by `url` exactly like the guide's own definitions. Product-level rather than
    per-guide, which is why these ride the package instead of the compiled tree.
    """
    entries: list[StoreEntry] = []
    package_directory = Path(__file__).parent / BUILTIN_CONFORMANCE_DIRECTORY
    for path in sorted(package_directory.glob("*.json")):
        body = json.loads(path.read_text())
        entries.append(
            StoreEntry(
                resource_type=str(body["resourceType"]),
                resource_id=str(body["id"]),
                canonical_url=body.get("url"),
                identifiers=(),
                source=path.as_posix(),
                body=body,
            )
        )
    return tuple(entries)

attach_builtin_conformance(store)

The store with the package's own conformance resources appended, whichever mode built it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def attach_builtin_conformance(store: ResourceStore) -> ResourceStore:
    """The store with the package's own conformance resources appended, whichever mode built it."""
    return ResourceStore(entries=(*store.entries, *builtin_conformance_entries()))

load_compiled_conformance_entries(project)

Read the conformance resources out of a project's compiled IG, and nothing else from it.

This is how one guide reaches both store modes. A compiled store already holds these along with everything else the build wrote, and a live store is built from a DHIS2 instance and has no definitional layer of its own - no FSH compiler runs in the server - so a live run reads them from whatever SUSHI last compiled beside the project and hosts that.

A project with no compiled tree beside it holds none, and says so by holding none: the store has fewer types and the CapabilityStatement declares exactly the types the store has. The parse is the strict one load_compiled_store uses, for the reason stated there.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/store.py
def load_compiled_conformance_entries(project: FhirProject) -> tuple[StoreEntry, ...]:
    """Read the conformance resources out of a project's compiled IG, and nothing else from it.

    This is how one guide reaches both store modes. A compiled store already holds these along with
    everything else the build wrote, and a live store is built from a DHIS2 instance and has no
    definitional layer of its own - no FSH compiler runs in the server - so a live run reads them
    from whatever SUSHI last compiled beside the project and hosts that.

    A project with no compiled tree beside it holds none, and says so by holding none: the store has
    fewer types and the CapabilityStatement declares exactly the types the store has. The parse is
    the strict one `load_compiled_store` uses, for the reason stated there.
    """
    compiled_directory = project.ig_directory / "fsh-generated" / "resources"
    if not compiled_directory.is_dir():
        return ()
    entries = (_read_entry(path, project.project_root) for path in sorted(compiled_directory.glob("*.json")))
    return tuple(entry for entry in entries if entry.resource_type in GUIDE_CONFORMANCE_RESOURCE_TYPES)

Response spool

Every received QuestionnaireResponse, written atomically to .serve/responses/received/ and read back from whichever of received/, forwarded/, rejected/, and withdrawn/ it has since been moved to - the first three by d2w fhir forward, the fourth by d2w fhir withdraw. The directory is the index and is re-read on every call, because those commands rename files while the server runs. A stored response is a receipt - the submission as it arrived, never a live view of DHIS2 data.

spool

The response spool: every QuestionnaireResponse the facade received, and which lifecycle state it is in.

The spool is a directory tree under .serve/responses - or wherever [serve] spool_dir points this project's, resolved through dhis2w_fhir.spool.resolve_spool_root so the forwarder reads the same answer - with one subdirectory per state:

`received/`   captured, not yet forwarded - the queue.
`forwarded/`  translated, posted, and accepted by DHIS2.
`rejected/`   posted and refused; `<id>.report.json` beside it holds the import report saying why.
`withdrawn/`  it landed, and `d2w fhir withdraw` retracted it from DHIS2 afterwards;
              `<id>.report.json` beside it is the record of the delete rather than of an import.

plus one directory that is not a state at all:

`malformed/`  a holding pen for files that do not read as receipts, each with its reason beside it.

THE DIRECTORY IS THE INDEX, AND IT IS RE-READ ON EVERY CALL. That is the whole reason this module holds no state. d2w fhir forward is a separate process that moves files between those three directories while the server is running, so an index built at startup is wrong the moment the first drain finishes: it would keep answering "received" for receipts DHIS2 has already accepted, and a capture UI reading it would show a queue that never empties. Re-reading costs one scandir and one parse per receipt, which is a rounding error against a facade that serves one project.

ONE BAD FILE COSTS ONE ROW, NOT THE LISTING. A file that will not parse is moved to malformed/ with its reason written beside it, and the read carries on with everything else. The loud-not-silent principle is kept by reporting rather than by refusing: GET /facade/spool counts what is in quarantine and names each file with the error that put it there, which is strictly more than a 500 over the whole listing ever told anyone. A directory this process cannot read at all is a different failure, and still raises UnreadableReceiptError.

Writes are atomic and durable: a temporary file in the same directory, fsync, then a rename, then an fsync of the directory - so a reader never sees a half-written response, a crash leaves the directory consistent, and a 201 means the receipt survives the machine losing power. The spool assumes a single writing process for received/, which is what d2w fhir serve is; the forwarder only moves files that are already whole.

A stored response is the submission as it arrived - a receipt. It is never a live view of DHIS2 data, and reading one back tells you what a client sent, not what DHIS2 now holds. That stays true after a forward: a forwarded receipt is still readable, because "DHIS2 took this" is a fact about the receipt rather than a reason to stop serving it.

Classes

ResponseLifecycle

Bases: StrEnum

Which of the spool's four directories a receipt currently sits in.

Three of the four are the forwarder's, spelled from the reading side: a receipt is received until d2w fhir forward drains it, and then it is whatever DHIS2 said. A response the translator refused stays received and the next drain retries it - a committing drain leaves its refusal record beside the receipt, which is what refusal_record reads - except for the one refusal no change to the guide or the data could ever fix, which the forwarder files as rejected.

The fourth is an operator's, and it is the only one a receipt reaches without being posted again: d2w fhir withdraw deletes from DHIS2 what a forwarded receipt landed and files the receipt under withdrawn/, with the record of the delete beside it - which is what withdrawal_record reads. It is terminal, because DHIS2 burns the UID it deletes.

The same four states as dhis2w_fhir.spool.SpoolState, which is the drain's own name for the layout. Two enums for one directory tree, because the two packages read it for different reasons and neither depends on the other's vocabulary; tests/test_spool_directory.py pins them level.

malformed/ is not here because nothing in it is a receipt: it is bytes that would not parse as one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class ResponseLifecycle(StrEnum):
    """Which of the spool's four directories a receipt currently sits in.

    Three of the four are the forwarder's, spelled from the reading side: a receipt is `received`
    until `d2w fhir forward` drains it, and then it is whatever DHIS2 said. A response the translator
    refused stays `received` and the next drain retries it - a committing drain leaves its refusal
    record beside the receipt, which is what `refusal_record` reads - except for the one refusal no
    change to the guide or the data could ever fix, which the forwarder files as `rejected`.

    The fourth is an operator's, and it is the only one a receipt reaches without being posted again:
    `d2w fhir withdraw` deletes from DHIS2 what a forwarded receipt landed and files the receipt under
    `withdrawn/`, with the record of the delete beside it - which is what `withdrawal_record` reads.
    It is terminal, because DHIS2 burns the UID it deletes.

    The same four states as `dhis2w_fhir.spool.SpoolState`, which is the drain's own name for the
    layout. Two enums for one directory tree, because the two packages read it for different reasons
    and neither depends on the other's vocabulary; `tests/test_spool_directory.py` pins them level.

    `malformed/` is not here because nothing in it is a receipt: it is bytes that would not parse
    as one.
    """

    RECEIVED = "received"
    FORWARDED = "forwarded"
    REJECTED = "rejected"
    WITHDRAWN = "withdrawn"

UnreadableReceiptError

Bases: ServeError

A spool directory cannot be read at all - a permission, a device, a broken mount.

Not what one unreadable file raises: that file is moved to malformed/, named in the listing, and the read carries on, because a submission the facade silently drops looks to its sender exactly like one that never arrived - and so does a listing that 500s over one bad byte.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class UnreadableReceiptError(ServeError):
    """A spool directory cannot be read at all - a permission, a device, a broken mount.

    Not what one unreadable *file* raises: that file is moved to `malformed/`, named in the listing,
    and the read carries on, because a submission the facade silently drops looks to its sender
    exactly like one that never arrived - and so does a listing that 500s over one bad byte.
    """

    status_code = 500
    issue_code = "exception"

StoredResponseEnvelope

Bases: BaseModel

One received QuestionnaireResponse plus the receipt metadata the facade recorded around it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class StoredResponseEnvelope(BaseModel):
    """One received QuestionnaireResponse plus the receipt metadata the facade recorded around it."""

    model_config = ConfigDict(frozen=True)

    response_id: str
    received_at: str
    """The instant the facade accepted the response, as a FHIR `instant` (UTC, `Z`-suffixed)."""

    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.

    Set only under `[serve] auth = "dhis2"`, where a caller presents credentials the instance answers
    for and the username DHIS2 gives back is who submitted. Under `none` there is nobody to name, and
    under `token` a static token names nobody either.

    FACADE-SIDE PROVENANCE, AND NOTHING MORE. It says who handed this receipt to this server. It does
    not say who the values reach DHIS2 as: `d2w fhir forward` posts as the forwarding profile, and
    `storedBy` on the instance is DHIS2's own stamp of that profile. The receipt is where "who
    captured this" is answered, and the instance is where "who wrote this" is.
    """

    warnings: tuple[str, ...] = ()
    response: dict[str, Any]
    """The QuestionnaireResponse as received, stamped with its `id` - the same escape hatch `StoreEntry.body` documents.

    The facade's contract is byte-faithful: a receipt has to read back as what the client sent, so the
    resource is held verbatim rather than round-tripped through a model that would drop the extensions
    and answer types this repo has no schema for. The dict leaves the spool only as an HTTP response body.
    """
Attributes
received_at instance-attribute

The instant the facade accepted the response, as a FHIR instant (UTC, Z-suffixed).

submitted_by = None class-attribute instance-attribute

The DHIS2 username the facade validated this submission under, or None when it validated none.

Set only under [serve] auth = "dhis2", where a caller presents credentials the instance answers for and the username DHIS2 gives back is who submitted. Under none there is nobody to name, and under token a static token names nobody either.

FACADE-SIDE PROVENANCE, AND NOTHING MORE. It says who handed this receipt to this server. It does not say who the values reach DHIS2 as: d2w fhir forward posts as the forwarding profile, and storedBy on the instance is DHIS2's own stamp of that profile. The receipt is where "who captured this" is answered, and the instance is where "who wrote this" is.

response instance-attribute

The QuestionnaireResponse as received, stamped with its id - the same escape hatch StoreEntry.body documents.

The facade's contract is byte-faithful: a receipt has to read back as what the client sent, so the resource is held verbatim rather than round-tripped through a model that would drop the extensions and answer types this repo has no schema for. The dict leaves the spool only as an HTTP response body.

StoredReceipt

Bases: StoredResponseEnvelope

One receipt as the spool answers it: the envelope on disk, plus which state its file sits in.

The lifecycle is not in the envelope because it is not written into it - it is which directory the file is in, which is what makes a forward run a rename with no bookkeeping at all.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class StoredReceipt(StoredResponseEnvelope):
    """One receipt as the spool answers it: the envelope on disk, plus which state its file sits in.

    The lifecycle is not in the envelope because it is not written into it - it is which directory
    the file is in, which is what makes a forward run a rename with no bookkeeping at all.
    """

    lifecycle: ResponseLifecycle

SpoolReading

Bases: BaseModel

What one read of the spool found: the receipts it parsed, and the files it moved to malformed/.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class SpoolReading(BaseModel):
    """What one read of the spool found: the receipts it parsed, and the files it moved to `malformed/`."""

    model_config = ConfigDict(frozen=True)

    receipts: tuple[StoredReceipt, ...] = ()
    quarantined: tuple[QuarantinedFile, ...] = ()

SpoolCursor

Bases: BaseModel

Where one page of a spool listing starts, and the total counted when the walk began.

The total rides the cursor rather than being recounted per page, so a walk states one number throughout even though d2w fhir forward is moving files between the directories underneath it. A page is located by an offset into the newest-first order because that order is a fact about the receipts rather than about the filesystem: file names are uuid4 hex and say nothing about time.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class SpoolCursor(BaseModel):
    """Where one page of a spool listing starts, and the total counted when the walk began.

    The total rides the cursor rather than being recounted per page, so a walk states one number
    throughout even though `d2w fhir forward` is moving files between the directories underneath it.
    A page is located by an offset into the newest-first order because that order is a fact about the
    receipts rather than about the filesystem: file names are uuid4 hex and say nothing about time.
    """

    model_config = ConfigDict(frozen=True)

    offset: int = 0
    counted_total: int | None = None
    """How many receipts the listing held when it was first counted; absent on the cursor a client starts from."""

    @classmethod
    def from_token(cls, token: str) -> SpoolCursor:
        """Read one `page` token, refusing anything this server did not mint."""
        try:
            decoded = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode("ascii")
        except (binascii.Error, UnicodeDecodeError, ValueError) as error:
            raise BadSearchError(_UNREADABLE_CURSOR) from error
        match = _CURSOR_PATTERN.match(decoded)
        if match is None:
            raise BadSearchError(_UNREADABLE_CURSOR)
        counted = match.group(2)
        return cls(offset=int(match.group(1)), counted_total=None if counted is None else int(counted))

    def token(self) -> str:
        """This cursor as the `page` parameter carries it."""
        counted = "" if self.counted_total is None else f"n{self.counted_total}"
        return base64.urlsafe_b64encode(f"o{self.offset}{counted}".encode("ascii")).decode().rstrip("=")
Attributes
counted_total = None class-attribute instance-attribute

How many receipts the listing held when it was first counted; absent on the cursor a client starts from.

Methods:
from_token(token) classmethod

Read one page token, refusing anything this server did not mint.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
@classmethod
def from_token(cls, token: str) -> SpoolCursor:
    """Read one `page` token, refusing anything this server did not mint."""
    try:
        decoded = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode("ascii")
    except (binascii.Error, UnicodeDecodeError, ValueError) as error:
        raise BadSearchError(_UNREADABLE_CURSOR) from error
    match = _CURSOR_PATTERN.match(decoded)
    if match is None:
        raise BadSearchError(_UNREADABLE_CURSOR)
    counted = match.group(2)
    return cls(offset=int(match.group(1)), counted_total=None if counted is None else int(counted))
token()

This cursor as the page parameter carries it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def token(self) -> str:
    """This cursor as the `page` parameter carries it."""
    counted = "" if self.counted_total is None else f"n{self.counted_total}"
    return base64.urlsafe_b64encode(f"o{self.offset}{counted}".encode("ascii")).decode().rstrip("=")

SpoolPage

Bases: BaseModel

One page of a spool listing: what is on it, where it sits, and where the pages either side are.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class SpoolPage(BaseModel):
    """One page of a spool listing: what is on it, where it sits, and where the pages either side are."""

    model_config = ConfigDict(frozen=True)

    receipts: tuple[StoredReceipt, ...] = ()
    total: int = 0
    """How many receipts the whole listing holds, stated the same on every page of one walk."""

    cursor: SpoolCursor = SpoolCursor()
    next_cursor: SpoolCursor | None = None
    previous_cursor: SpoolCursor | None = None
Attributes
total = 0 class-attribute instance-attribute

How many receipts the whole listing holds, stated the same on every page of one walk.

ResponseSpool

Bases: BaseModel

The receipt tree of one project, read from disk on every call.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
class ResponseSpool(BaseModel):
    """The receipt tree of one project, read from disk on every call."""

    model_config = ConfigDict(frozen=True)

    directory: Path
    """The spool root - the four state directories, and `malformed/` beside them."""

    @classmethod
    def at(cls, project_root: Path, spool_dir: str = SPOOL_RELATIVE_PATH) -> ResponseSpool:
        """The spool of one project, creating the receiving directory so a first capture has somewhere to land.

        `spool_dir` is `[serve] spool_dir` - relative to the project root unless it is absolute - and
        it is resolved through `dhis2w_fhir.spool.resolve_spool_root`, which is the same resolution
        `d2w fhir forward` reads the key through. The layout under the root is stated in this module
        and in `dhis2w_fhir.spool` alike, for the reason that module gives; where the root is, is not.

        The orphan sweep runs here rather than per write: an abandoned temporary file is what a
        killed process leaves behind, and process start is exactly when there is a new process to
        notice it.
        """
        spool = cls(directory=resolve_spool_root(project_root, spool_dir))
        spool.directory_for(ResponseLifecycle.RECEIVED).mkdir(parents=True, exist_ok=True)
        spool.sweep_orphan_temporary_files()
        return spool

    def directory_for(self, lifecycle: ResponseLifecycle) -> Path:
        """Where receipts in one lifecycle state are read from and written to."""
        return self.directory / LIFECYCLE_DIRECTORY_NAMES[lifecycle]

    @property
    def malformed_directory(self) -> Path:
        """Where a file that does not read as a receipt is held, which is no receipt's lifecycle state."""
        return self.directory / MALFORMED_DIRECTORY_NAME

    def save(self, envelope: StoredResponseEnvelope) -> None:
        """Write the envelope atomically into the receiving directory, which is where a capture lands."""
        directory = self.directory_for(ResponseLifecycle.RECEIVED)
        directory.mkdir(parents=True, exist_ok=True)
        # The envelope's own fields and nothing else: a `StoredReceipt` handed back here would
        # otherwise write its lifecycle into the file, where it would immediately disagree with
        # the directory the file is in.
        payload = envelope.model_dump_json(indent=2, include=set(StoredResponseEnvelope.model_fields)) + "\n"
        _write_atomically(directory / f"{envelope.response_id}.json", payload)

    def get(self, response_id: str) -> StoredReceipt | None:
        """The receipt a `GET /QuestionnaireResponse/{id}` read resolves to, in whichever state it now sits.

        A forwarded receipt still reads back. Serving 404 the moment `d2w fhir forward` renamed the
        file would make the id a client was handed at capture time expire on a schedule nothing told
        it. A file that will not parse is quarantined and answers as absent, which is what it now is.
        """
        for lifecycle in ResponseLifecycle:
            path = self.directory_for(lifecycle) / f"{response_id}.json"
            if not path.is_file():
                continue
            try:
                return _read_receipt(path, lifecycle)
            except _MalformedReceiptError as error:
                self._quarantine(path, str(error))
                return None
        return None

    def search(
        self,
        questionnaire: str | None = None,
        form_kind: str | None = None,
        ids: tuple[str, ...] = (),
        lifecycles: tuple[ResponseLifecycle, ...] = (),
    ) -> SpoolReading:
        """Every receipt matching the given filters, newest received first, with what was quarantined."""
        reading = self.read(lifecycles)
        matches = [
            receipt
            for receipt in reading.receipts
            if (questionnaire is None or receipt.questionnaire == questionnaire)
            and (form_kind is None or receipt.form_kind == form_kind)
            and (not ids or receipt.response_id in ids)
        ]
        ordered = sorted(matches, key=lambda receipt: (receipt.received_at, receipt.response_id), reverse=True)
        return SpoolReading(receipts=tuple(ordered), quarantined=reading.quarantined)

    def read(self, lifecycles: tuple[ResponseLifecycle, ...] = ()) -> SpoolReading:
        """Read every receipt in the named states off disk, moving aside whatever will not parse as one.

        The quarantine listing is the whole holding pen rather than what this read moved into it: a
        file that stopped being a receipt an hour ago is exactly as unactioned as one that stopped
        being one just now, and a listing that named only the latter would go quiet about the former.
        """
        selected = lifecycles or tuple(ResponseLifecycle)
        found: list[StoredReceipt] = []
        for lifecycle in selected:
            directory = self.directory_for(lifecycle)
            if not directory.is_dir():
                continue
            for path in _receipt_paths(directory):
                try:
                    found.append(_read_receipt(path, lifecycle))
                except _MalformedReceiptError as error:
                    self._quarantine(path, str(error))
        return SpoolReading(receipts=tuple(found), quarantined=self.malformed())

    def receipts(self, lifecycles: tuple[ResponseLifecycle, ...] = ()) -> tuple[StoredReceipt, ...]:
        """Read every receipt in the named states off disk, in file-name order per state."""
        return self.read(lifecycles).receipts

    def malformed(self) -> tuple[QuarantinedFile, ...]:
        """Every file in the holding pen, each named with the error that put it there."""
        directory = self.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)
        )

    def import_report(self, response_id: str, lifecycle: ResponseLifecycle) -> ForwardImportOutcome | None:
        """What DHIS2 said about one drained receipt, read off the report the forwarder left beside it.

        Either drained state answers: `rejected/` holds why the payload was refused and `forwarded/`
        holds what the import counted. A receipt still in `received/` has no report because nothing
        has been asked about it yet, and answers None like any receipt whose report is not there. A
        withdrawn receipt is read through `withdrawal_record`, because the file beside it answers a
        different question - what DHIS2 did when it was asked to let the object go.

        A report that will not parse answers None rather than raising: it is the diagnostic that got
        corrupted, not the receipt that got lost, so a listing still names the receipt and simply
        says nothing about what DHIS2 made of it.
        """
        path = self.directory_for(lifecycle) / f"{response_id}{IMPORT_REPORT_SUFFIX}"
        if not path.is_file():
            return None
        try:
            return ForwardImportOutcome.model_validate_json(path.read_text(encoding="utf-8"))
        except (OSError, ValidationError, ValueError):
            logger.warning("%s is not a readable import report; the receipt is listed without one", path)
            return None

    def withdrawal_record(self, response_id: str) -> WithdrawalRecord | None:
        """What DHIS2 answered when `d2w fhir withdraw` asked it to take one receipt's event back.

        The sidecar of a withdrawn receipt is not an import report: it names the event that was
        deleted, the instant the delete was posted, and what the instance keeps afterwards - which is
        a hidden copy rather than nothing. The import report that said what the receipt landed stays
        in `forwarded/`, so the two answers to the two questions are two files.

        Only a `withdrawn` receipt has one. A record that will not parse answers None, for the reason
        `import_report` gives.
        """
        path = self.directory_for(ResponseLifecycle.WITHDRAWN) / f"{response_id}{IMPORT_REPORT_SUFFIX}"
        if not path.is_file():
            return None
        try:
            return WithdrawalRecord.model_validate_json(path.read_text(encoding="utf-8"))
        except (OSError, ValidationError, ValueError):
            logger.warning("%s is not a readable withdrawal record; the receipt is listed without one", path)
            return None

    def refusal_record(self, response_id: str) -> ForwardRefusalRecord | None:
        """The refusal the last committing drain left beside one still-queued receipt.

        Only a `received` receipt can have one - the move that drains a receipt deletes the marker -
        and a receipt no committing drain has refused answers None, exactly like one no drain has
        seen. A record that will not parse also answers None, for the reason `import_report` gives.
        """
        return read_refusal_record(self.directory_for(ResponseLifecycle.RECEIVED), response_id)

    def count(self) -> int:
        """How many receipts the spool holds, across every lifecycle state."""
        return len(self.receipts())

    def count_by_lifecycle(self) -> dict[ResponseLifecycle, int]:
        """How many receipts sit in each state, which is the one number a queue is read by."""
        counts = dict.fromkeys(ResponseLifecycle, 0)
        for receipt in self.receipts():
            counts[receipt.lifecycle] += 1
        return counts

    def sweep_orphan_temporary_files(
        self, *, 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 - so
        the only thing separating them is how long ago the file was touched. 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 = [
            self.directory,
            *(self.directory_for(lifecycle) for lifecycle in ResponseLifecycle),
            self.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)

    def _quarantine(self, path: Path, reason: str) -> QuarantinedFile:
        """Move one unreadable file into the holding pen, its reason written down beside it first."""
        logger.warning("%s does not read as a receipt (%s); moved to %s", path, reason, self.malformed_directory)
        record = QuarantinedFile(file_name=path.name, reason=reason)
        directory = self.malformed_directory
        directory.mkdir(parents=True, exist_ok=True)
        _write_atomically(directory / f"{path.name}{QUARANTINE_REASON_SUFFIX}", record.model_dump_json(indent=2) + "\n")
        try:
            os.replace(path, directory / path.name)
            _fsync_directory(directory)
        except OSError as error:
            return QuarantinedFile(file_name=path.name, reason=f"{reason}; the file could not be moved aside ({error})")
        return record
Attributes
directory instance-attribute

The spool root - the four state directories, and malformed/ beside them.

malformed_directory property

Where a file that does not read as a receipt is held, which is no receipt's lifecycle state.

Methods:
at(project_root, spool_dir=SPOOL_RELATIVE_PATH) classmethod

The spool of one project, creating the receiving directory so a first capture has somewhere to land.

spool_dir is [serve] spool_dir - relative to the project root unless it is absolute - and it is resolved through dhis2w_fhir.spool.resolve_spool_root, which is the same resolution d2w fhir forward reads the key through. The layout under the root is stated in this module and in dhis2w_fhir.spool alike, for the reason that module gives; where the root is, is not.

The orphan sweep runs here rather than per write: an abandoned temporary file is what a killed process leaves behind, and process start is exactly when there is a new process to notice it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
@classmethod
def at(cls, project_root: Path, spool_dir: str = SPOOL_RELATIVE_PATH) -> ResponseSpool:
    """The spool of one project, creating the receiving directory so a first capture has somewhere to land.

    `spool_dir` is `[serve] spool_dir` - relative to the project root unless it is absolute - and
    it is resolved through `dhis2w_fhir.spool.resolve_spool_root`, which is the same resolution
    `d2w fhir forward` reads the key through. The layout under the root is stated in this module
    and in `dhis2w_fhir.spool` alike, for the reason that module gives; where the root is, is not.

    The orphan sweep runs here rather than per write: an abandoned temporary file is what a
    killed process leaves behind, and process start is exactly when there is a new process to
    notice it.
    """
    spool = cls(directory=resolve_spool_root(project_root, spool_dir))
    spool.directory_for(ResponseLifecycle.RECEIVED).mkdir(parents=True, exist_ok=True)
    spool.sweep_orphan_temporary_files()
    return spool
directory_for(lifecycle)

Where receipts in one lifecycle state are read from and written to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def directory_for(self, lifecycle: ResponseLifecycle) -> Path:
    """Where receipts in one lifecycle state are read from and written to."""
    return self.directory / LIFECYCLE_DIRECTORY_NAMES[lifecycle]
save(envelope)

Write the envelope atomically into the receiving directory, which is where a capture lands.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def save(self, envelope: StoredResponseEnvelope) -> None:
    """Write the envelope atomically into the receiving directory, which is where a capture lands."""
    directory = self.directory_for(ResponseLifecycle.RECEIVED)
    directory.mkdir(parents=True, exist_ok=True)
    # The envelope's own fields and nothing else: a `StoredReceipt` handed back here would
    # otherwise write its lifecycle into the file, where it would immediately disagree with
    # the directory the file is in.
    payload = envelope.model_dump_json(indent=2, include=set(StoredResponseEnvelope.model_fields)) + "\n"
    _write_atomically(directory / f"{envelope.response_id}.json", payload)
get(response_id)

The receipt a GET /QuestionnaireResponse/{id} read resolves to, in whichever state it now sits.

A forwarded receipt still reads back. Serving 404 the moment d2w fhir forward renamed the file would make the id a client was handed at capture time expire on a schedule nothing told it. A file that will not parse is quarantined and answers as absent, which is what it now is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def get(self, response_id: str) -> StoredReceipt | None:
    """The receipt a `GET /QuestionnaireResponse/{id}` read resolves to, in whichever state it now sits.

    A forwarded receipt still reads back. Serving 404 the moment `d2w fhir forward` renamed the
    file would make the id a client was handed at capture time expire on a schedule nothing told
    it. A file that will not parse is quarantined and answers as absent, which is what it now is.
    """
    for lifecycle in ResponseLifecycle:
        path = self.directory_for(lifecycle) / f"{response_id}.json"
        if not path.is_file():
            continue
        try:
            return _read_receipt(path, lifecycle)
        except _MalformedReceiptError as error:
            self._quarantine(path, str(error))
            return None
    return None
search(questionnaire=None, form_kind=None, ids=(), lifecycles=())

Every receipt matching the given filters, newest received first, with what was quarantined.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def search(
    self,
    questionnaire: str | None = None,
    form_kind: str | None = None,
    ids: tuple[str, ...] = (),
    lifecycles: tuple[ResponseLifecycle, ...] = (),
) -> SpoolReading:
    """Every receipt matching the given filters, newest received first, with what was quarantined."""
    reading = self.read(lifecycles)
    matches = [
        receipt
        for receipt in reading.receipts
        if (questionnaire is None or receipt.questionnaire == questionnaire)
        and (form_kind is None or receipt.form_kind == form_kind)
        and (not ids or receipt.response_id in ids)
    ]
    ordered = sorted(matches, key=lambda receipt: (receipt.received_at, receipt.response_id), reverse=True)
    return SpoolReading(receipts=tuple(ordered), quarantined=reading.quarantined)
read(lifecycles=())

Read every receipt in the named states off disk, moving aside whatever will not parse as one.

The quarantine listing is the whole holding pen rather than what this read moved into it: a file that stopped being a receipt an hour ago is exactly as unactioned as one that stopped being one just now, and a listing that named only the latter would go quiet about the former.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def read(self, lifecycles: tuple[ResponseLifecycle, ...] = ()) -> SpoolReading:
    """Read every receipt in the named states off disk, moving aside whatever will not parse as one.

    The quarantine listing is the whole holding pen rather than what this read moved into it: a
    file that stopped being a receipt an hour ago is exactly as unactioned as one that stopped
    being one just now, and a listing that named only the latter would go quiet about the former.
    """
    selected = lifecycles or tuple(ResponseLifecycle)
    found: list[StoredReceipt] = []
    for lifecycle in selected:
        directory = self.directory_for(lifecycle)
        if not directory.is_dir():
            continue
        for path in _receipt_paths(directory):
            try:
                found.append(_read_receipt(path, lifecycle))
            except _MalformedReceiptError as error:
                self._quarantine(path, str(error))
    return SpoolReading(receipts=tuple(found), quarantined=self.malformed())
receipts(lifecycles=())

Read every receipt in the named states off disk, in file-name order per state.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def receipts(self, lifecycles: tuple[ResponseLifecycle, ...] = ()) -> tuple[StoredReceipt, ...]:
    """Read every receipt in the named states off disk, in file-name order per state."""
    return self.read(lifecycles).receipts
malformed()

Every file in the holding pen, each named with the error that put it there.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def malformed(self) -> tuple[QuarantinedFile, ...]:
    """Every file in the holding pen, each named with the error that put it there."""
    directory = self.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)
    )
import_report(response_id, lifecycle)

What DHIS2 said about one drained receipt, read off the report the forwarder left beside it.

Either drained state answers: rejected/ holds why the payload was refused and forwarded/ holds what the import counted. A receipt still in received/ has no report because nothing has been asked about it yet, and answers None like any receipt whose report is not there. A withdrawn receipt is read through withdrawal_record, because the file beside it answers a different question - what DHIS2 did when it was asked to let the object go.

A report that will not parse answers None rather than raising: it is the diagnostic that got corrupted, not the receipt that got lost, so a listing still names the receipt and simply says nothing about what DHIS2 made of it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def import_report(self, response_id: str, lifecycle: ResponseLifecycle) -> ForwardImportOutcome | None:
    """What DHIS2 said about one drained receipt, read off the report the forwarder left beside it.

    Either drained state answers: `rejected/` holds why the payload was refused and `forwarded/`
    holds what the import counted. A receipt still in `received/` has no report because nothing
    has been asked about it yet, and answers None like any receipt whose report is not there. A
    withdrawn receipt is read through `withdrawal_record`, because the file beside it answers a
    different question - what DHIS2 did when it was asked to let the object go.

    A report that will not parse answers None rather than raising: it is the diagnostic that got
    corrupted, not the receipt that got lost, so a listing still names the receipt and simply
    says nothing about what DHIS2 made of it.
    """
    path = self.directory_for(lifecycle) / f"{response_id}{IMPORT_REPORT_SUFFIX}"
    if not path.is_file():
        return None
    try:
        return ForwardImportOutcome.model_validate_json(path.read_text(encoding="utf-8"))
    except (OSError, ValidationError, ValueError):
        logger.warning("%s is not a readable import report; the receipt is listed without one", path)
        return None
withdrawal_record(response_id)

What DHIS2 answered when d2w fhir withdraw asked it to take one receipt's event back.

The sidecar of a withdrawn receipt is not an import report: it names the event that was deleted, the instant the delete was posted, and what the instance keeps afterwards - which is a hidden copy rather than nothing. The import report that said what the receipt landed stays in forwarded/, so the two answers to the two questions are two files.

Only a withdrawn receipt has one. A record that will not parse answers None, for the reason import_report gives.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def withdrawal_record(self, response_id: str) -> WithdrawalRecord | None:
    """What DHIS2 answered when `d2w fhir withdraw` asked it to take one receipt's event back.

    The sidecar of a withdrawn receipt is not an import report: it names the event that was
    deleted, the instant the delete was posted, and what the instance keeps afterwards - which is
    a hidden copy rather than nothing. The import report that said what the receipt landed stays
    in `forwarded/`, so the two answers to the two questions are two files.

    Only a `withdrawn` receipt has one. A record that will not parse answers None, for the reason
    `import_report` gives.
    """
    path = self.directory_for(ResponseLifecycle.WITHDRAWN) / f"{response_id}{IMPORT_REPORT_SUFFIX}"
    if not path.is_file():
        return None
    try:
        return WithdrawalRecord.model_validate_json(path.read_text(encoding="utf-8"))
    except (OSError, ValidationError, ValueError):
        logger.warning("%s is not a readable withdrawal record; the receipt is listed without one", path)
        return None
refusal_record(response_id)

The refusal the last committing drain left beside one still-queued receipt.

Only a received receipt can have one - the move that drains a receipt deletes the marker - and a receipt no committing drain has refused answers None, exactly like one no drain has seen. A record that will not parse also answers None, for the reason import_report gives.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def refusal_record(self, response_id: str) -> ForwardRefusalRecord | None:
    """The refusal the last committing drain left beside one still-queued receipt.

    Only a `received` receipt can have one - the move that drains a receipt deletes the marker -
    and a receipt no committing drain has refused answers None, exactly like one no drain has
    seen. A record that will not parse also answers None, for the reason `import_report` gives.
    """
    return read_refusal_record(self.directory_for(ResponseLifecycle.RECEIVED), response_id)
count()

How many receipts the spool holds, across every lifecycle state.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def count(self) -> int:
    """How many receipts the spool holds, across every lifecycle state."""
    return len(self.receipts())
count_by_lifecycle()

How many receipts sit in each state, which is the one number a queue is read by.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def count_by_lifecycle(self) -> dict[ResponseLifecycle, int]:
    """How many receipts sit in each state, which is the one number a queue is read by."""
    counts = dict.fromkeys(ResponseLifecycle, 0)
    for receipt in self.receipts():
        counts[receipt.lifecycle] += 1
    return counts
sweep_orphan_temporary_files(*, 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 - so the only thing separating them is how long ago the file was touched. 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-serve/src/dhis2w_fhir_serve/spool.py
def sweep_orphan_temporary_files(
    self, *, 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 - so
    the only thing separating them is how long ago the file was touched. 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 = [
        self.directory,
        *(self.directory_for(lifecycle) for lifecycle in ResponseLifecycle),
        self.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)

Functions:

page_of(receipts, cursor, count)

Slice one page out of an ordered listing, and name the pages either side of it.

The total is whatever the first page of a walk counted, carried forward on every link it hands out - so a client paging through a spool the forwarder is draining underneath it reads one number rather than a different one per page. An offset past the end is an empty page rather than a refusal: a link minted before the spool was drained has become a page with nothing on it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def page_of(receipts: tuple[StoredReceipt, ...], cursor: SpoolCursor, count: int) -> SpoolPage:
    """Slice one page out of an ordered listing, and name the pages either side of it.

    The total is whatever the first page of a walk counted, carried forward on every link it hands
    out - so a client paging through a spool the forwarder is draining underneath it reads one
    number rather than a different one per page. An offset past the end is an empty page rather than
    a refusal: a link minted before the spool was drained has become a page with nothing on it.
    """
    total = cursor.counted_total if cursor.counted_total is not None else len(receipts)
    offset = min(cursor.offset, len(receipts))
    reached = SpoolCursor(offset=offset, counted_total=total)
    page = receipts[offset : offset + count]
    following = offset + len(page)
    return SpoolPage(
        receipts=page,
        total=total,
        cursor=reached,
        next_cursor=SpoolCursor(offset=following, counted_total=total) if following < len(receipts) else None,
        previous_cursor=SpoolCursor(offset=max(offset - count, 0), counted_total=total) if offset > 0 else None,
    )

requested_page_size(stated)

How many receipts one page carries: what the client asked for, bounded by what this server serves.

A _count above the limit is served the limit rather than refused - R4 says a server may return fewer resources than were asked for - while a _count that is not a positive number is a malformed query rather than an ambitious one, and is refused as such.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def requested_page_size(stated: str | None) -> int:
    """How many receipts one page carries: what the client asked for, bounded by what this server serves.

    A `_count` above the limit is served the limit rather than refused - R4 says a server may return
    fewer resources than were asked for - while a `_count` that is not a positive number is a
    malformed query rather than an ambitious one, and is refused as such.
    """
    if stated is None:
        return DEFAULT_SPOOL_PAGE_SIZE
    try:
        count = int(stated)
    except ValueError as error:
        raise BadSearchError(f"`{COUNT_PARAMETER}` was given `{stated}`, which is not a number of rows") from error
    if count < 1:
        raise BadSearchError(f"`{COUNT_PARAMETER}` was given `{stated}`: a page carries at least one row")
    return min(count, SPOOL_PAGE_SIZE_LIMIT)

requested_cursor(stated)

Which page was asked for - the first one when the request names none.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def requested_cursor(stated: str | None) -> SpoolCursor:
    """Which page was asked for - the first one when the request names none."""
    return SpoolCursor() if stated is None else SpoolCursor.from_token(stated)

new_response_id()

Mint a receipt id: a uuid4 hex, which is 32 characters of [a-f0-9] and so a valid FHIR id.

Deliberately not a DHIS2 UID (dhis2w_client.v43.uids.generate_uid). A receipt is a resource the facade owns, not a DHIS2 object, and an 11-character DHIS2-shaped id would read as one. The hex form drops the dashes a uuid string carries so the id is safe in a path segment and a file name.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def new_response_id() -> str:
    """Mint a receipt id: a uuid4 hex, which is 32 characters of `[a-f0-9]` and so a valid FHIR id.

    Deliberately not a DHIS2 UID (`dhis2w_client.v43.uids.generate_uid`). A receipt is a resource the
    facade owns, not a DHIS2 object, and an 11-character DHIS2-shaped id would read as one. The hex
    form drops the dashes a uuid string carries so the id is safe in a path segment and a file name.
    """
    return uuid.uuid4().hex

current_instant()

The current UTC time as a FHIR instant (Z-suffixed, millisecond precision).

Milliseconds because this stamp is what orders a drain: two submissions of the same cell inside one second have to order on something a submission means, and a receipt id is a random hex string that orders on nothing. Every stamp carries three fractional digits, so the plain string comparison the spool and the forwarded-cell index both use reads as chronological order.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
def current_instant() -> str:
    """The current UTC time as a FHIR `instant` (`Z`-suffixed, millisecond precision).

    Milliseconds because this stamp is what orders a drain: two submissions of the same cell inside
    one second have to order on something a submission means, and a receipt id is a random hex
    string that orders on nothing. Every stamp carries three fractional digits, so the plain string
    comparison the spool and the forwarded-cell index both use reads as chronological order.
    """
    return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")

Spool listing

GET /facade/spool - the receipt envelopes with their lifecycle state, and the DHIS2 import report behind a rejection. Typed JSON rather than FHIR, because a receipt envelope has no FHIR analogue; the module docstring states the reasoning in full.

spool

GET /facade/spool - the receipt tree with its lifecycle, for the capture UI's Responses page.

WHY THIS IS NOT FHIR, STATED ONCE SO NOBODY HAS TO RE-DERIVE IT.

Almost everything this facade answers has a FHIR spelling, and where one exists it is used - the receipts themselves are GET /QuestionnaireResponse, and that search covers all three lifecycle states, so a client that only speaks FHIR still sees every receipt. What that search cannot carry is the receipt envelope: when the facade accepted the submission, which DHIS2 form kind it was validated as, what the server had to warn about, which of the spool's four directories the file now sits in, and - for a rejection - the DHIS2 import report d2w fhir forward left beside it.

None of those are elements of a QuestionnaireResponse. Two of them could be forced into meta (lastUpdated for the receipt instant, a tag for the lifecycle), and a DHIS2 ImportSummary could be bent into an OperationOutcome, but that would spread one record across a FHIR resource, a tag system this IG does not publish, and a second operation - and the DHIS2 import counts would still have nowhere honest to go. A facade-owned record with no FHIR analogue is better served as what it is. So this is a plain typed JSON endpoint, application/json, and its shape is Pydantic models rather than a Bundle.

AND SO IT LIVES UNDER /facade, which is where every answer of that kind lives. The base URL is FHIR's and its contract is the CapabilityStatement; this facade's own API is a different contract at a different address, published as OpenAPI at /facade/openapi.json. Two APIs, two documents, one process. dhis2w_fhir_serve.routes states the mounting and docs/fhir/design/endpoint-naming.md states the rule a new endpoint is placed by.

Every read re-reads the directory. d2w fhir forward moves files while this server runs, so a listing built from anything else is stale by design; see dhis2w_fhir_serve.spool.

THE LISTING IS PAGED, with the same two parameters the register listing uses: _count for how many rows a page carries and page for an opaque cursor a client only ever gets from a next or previous link. total is the whole listing on every page of one walk, and the per-state counts are the whole spool rather than the page - a queue depth that changed with the page you were looking at would be no queue depth at all.

Classes

SpoolRejectionIssue

Bases: BaseModel

One row DHIS2 named as a reason it would not take the payload.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolRejectionIssue(BaseModel):
    """One row DHIS2 named as a reason it would not take the payload."""

    model_config = ConfigDict(frozen=True)

    error_code: str | None = None
    subject: str | None = None
    message: str | None = None

SpoolRejection

Bases: BaseModel

What DHIS2 said about one refused receipt, projected out of the report the forwarder stored.

A projection rather than the report itself: ForwardImportOutcome carries the endpoint's whole generated document alongside its rollup, and a listing that shipped those would send a TrackerImportReport per rejected row to a browser that renders four fields of it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolRejection(BaseModel):
    """What DHIS2 said about one refused receipt, projected out of the report the forwarder stored.

    A projection rather than the report itself: `ForwardImportOutcome` carries the endpoint's whole
    generated document alongside its rollup, and a listing that shipped those would send a
    `TrackerImportReport` per rejected row to a browser that renders four fields of it.
    """

    model_config = ConfigDict(frozen=True)

    status: str | None = None
    message: str | None = None
    created: int = 0
    updated: int = 0
    ignored: int = 0
    issues: tuple[SpoolRejectionIssue, ...] = ()

    @classmethod
    def from_outcome(cls, outcome: ForwardImportOutcome) -> SpoolRejection:
        """Reduce one stored import report to the rollup a rejected row shows."""
        return cls(
            status=outcome.status,
            message=outcome.message,
            created=outcome.created,
            updated=outcome.updated,
            ignored=outcome.ignored,
            issues=tuple(
                SpoolRejectionIssue(error_code=issue.error_code, subject=issue.subject, message=issue.message)
                for issue in outcome.issues
            ),
        )
Methods:
from_outcome(outcome) classmethod

Reduce one stored import report to the rollup a rejected row shows.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
@classmethod
def from_outcome(cls, outcome: ForwardImportOutcome) -> SpoolRejection:
    """Reduce one stored import report to the rollup a rejected row shows."""
    return cls(
        status=outcome.status,
        message=outcome.message,
        created=outcome.created,
        updated=outcome.updated,
        ignored=outcome.ignored,
        issues=tuple(
            SpoolRejectionIssue(error_code=issue.error_code, subject=issue.subject, message=issue.message)
            for issue in outcome.issues
        ),
    )

SpoolImport

Bases: BaseModel

What DHIS2 counted when it took one receipt, projected out of the report the forwarder stored.

The counts without the issue rows a rejection carries, because an import DHIS2 accepted named no rows against the payload - created, updated, ignored and deleted are the whole of what it said. They are what answers "the receipt is forwarded, but did any of it land": an import that ignored every value is an accepted receipt that changed nothing in the instance.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolImport(BaseModel):
    """What DHIS2 counted when it took one receipt, projected out of the report the forwarder stored.

    The counts without the issue rows a rejection carries, because an import DHIS2 accepted named no
    rows against the payload - `created`, `updated`, `ignored` and `deleted` are the whole of what it
    said. They are what answers "the receipt is forwarded, but did any of it land": an import that
    ignored every value is an accepted receipt that changed nothing in the instance.
    """

    model_config = ConfigDict(frozen=True)

    status: str | None = None
    message: str | None = None
    created: int = 0
    updated: int = 0
    ignored: int = 0
    deleted: int = 0

    @classmethod
    def from_outcome(cls, outcome: ForwardImportOutcome) -> SpoolImport:
        """Reduce one stored import report to the counts an accepted row shows."""
        return cls(
            status=outcome.status,
            message=outcome.message,
            created=outcome.created,
            updated=outcome.updated,
            ignored=outcome.ignored,
            deleted=outcome.deleted,
        )
Methods:
from_outcome(outcome) classmethod

Reduce one stored import report to the counts an accepted row shows.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
@classmethod
def from_outcome(cls, outcome: ForwardImportOutcome) -> SpoolImport:
    """Reduce one stored import report to the counts an accepted row shows."""
    return cls(
        status=outcome.status,
        message=outcome.message,
        created=outcome.created,
        updated=outcome.updated,
        ignored=outcome.ignored,
        deleted=outcome.deleted,
    )

SpoolRefusal

Bases: BaseModel

What the last committing drain said when it refused to translate one still-queued receipt.

The receipt stays received and the next drain retries it, so this is the queue's own history rather than a DHIS2 answer: when the drain looked, how many drains have refused the receipt so far, and why. Projected out of the refusal record the forwarder stored beside the receipt.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolRefusal(BaseModel):
    """What the last committing drain said when it refused to translate one still-queued receipt.

    The receipt stays `received` and the next drain retries it, so this is the queue's own history
    rather than a DHIS2 answer: when the drain looked, how many drains have refused the receipt so
    far, and why. Projected out of the refusal record the forwarder stored beside the receipt.
    """

    model_config = ConfigDict(frozen=True)

    refused_at: str
    attempt_count: int = 1
    reasons: tuple[SpoolRejectionIssue, ...] = ()

    @classmethod
    def from_record(cls, record: ForwardRefusalRecord) -> SpoolRefusal:
        """Reduce one stored refusal record to the rollup a still-queued row shows."""
        return cls(
            refused_at=record.refused_at,
            attempt_count=record.attempt_count,
            reasons=tuple(
                SpoolRejectionIssue(error_code=reason.category, subject=reason.element, message=reason.reason)
                for reason in record.reasons
            ),
        )
Methods:
from_record(record) classmethod

Reduce one stored refusal record to the rollup a still-queued row shows.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
@classmethod
def from_record(cls, record: ForwardRefusalRecord) -> SpoolRefusal:
    """Reduce one stored refusal record to the rollup a still-queued row shows."""
    return cls(
        refused_at=record.refused_at,
        attempt_count=record.attempt_count,
        reasons=tuple(
            SpoolRejectionIssue(error_code=reason.category, subject=reason.element, message=reason.reason)
            for reason in record.reasons
        ),
    )

SpoolWithdrawal

Bases: BaseModel

What DHIS2 answered when it was asked to take one receipt's event back, and what it keeps afterwards.

Projected out of the withdrawal record d2w fhir withdraw stored beside the receipt. The note is the record's own sentence rather than a phrasing invented here: DHIS2 soft-deletes, so the row stays in the instance carrying its value and is gone from every ordinary read, and a listing that said "deleted" would be claiming more than the toolkit can stand behind.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolWithdrawal(BaseModel):
    """What DHIS2 answered when it was asked to take one receipt's event back, and what it keeps afterwards.

    Projected out of the withdrawal record `d2w fhir withdraw` stored beside the receipt. The note is
    the record's own sentence rather than a phrasing invented here: DHIS2 soft-deletes, so the row
    stays in the instance carrying its value and is gone from every ordinary read, and a listing that
    said "deleted" would be claiming more than the toolkit can stand behind.
    """

    model_config = ConfigDict(frozen=True)

    withdrawn_at: str
    """The instant the withdrawal was posted, as a FHIR `instant` (UTC)."""

    event_uid: str
    """The DHIS2 event the withdrawal named, derived from the receipt's own logical id."""

    note: str
    """What remains in the instance, in the record's own words."""

    status: str | None = None
    deleted: int = 0
    """How many objects DHIS2 counted as deleted when it took the retraction."""

    @classmethod
    def from_record(cls, record: WithdrawalRecord) -> SpoolWithdrawal:
        """Reduce one stored withdrawal record to what a withdrawn row shows."""
        return cls(
            withdrawn_at=record.withdrawn_at,
            event_uid=record.event_uid,
            note=record.note,
            status=record.status,
            deleted=record.deleted,
        )
Attributes
withdrawn_at instance-attribute

The instant the withdrawal was posted, as a FHIR instant (UTC).

event_uid instance-attribute

The DHIS2 event the withdrawal named, derived from the receipt's own logical id.

note instance-attribute

What remains in the instance, in the record's own words.

deleted = 0 class-attribute instance-attribute

How many objects DHIS2 counted as deleted when it took the retraction.

Methods:
from_record(record) classmethod

Reduce one stored withdrawal record to what a withdrawn row shows.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
@classmethod
def from_record(cls, record: WithdrawalRecord) -> SpoolWithdrawal:
    """Reduce one stored withdrawal record to what a withdrawn row shows."""
    return cls(
        withdrawn_at=record.withdrawn_at,
        event_uid=record.event_uid,
        note=record.note,
        status=record.status,
        deleted=record.deleted,
    )

SpoolResponseSummary

Bases: BaseModel

One receipt as a listing row: when it arrived, what it answers, where it is, and what DHIS2 said.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolResponseSummary(BaseModel):
    """One receipt as a listing row: when it arrived, what it answers, where it is, and what DHIS2 said."""

    model_config = ConfigDict(frozen=True)

    response_id: str
    received_at: str
    lifecycle: ResponseLifecycle
    form_kind: str
    questionnaire: str
    """The canonical of the Questionnaire the submission answered."""

    questionnaire_id: str | None = None
    """The last segment of that canonical - the id the form is served under, for joining to a title."""

    submitted_by: str | None = None
    """The DHIS2 username the facade validated this submission under, or None when it validated none."""

    status: str | None = None
    authored: str | None = None
    answer_count: int = 0
    warnings: tuple[str, ...] = ()
    period: str | None = None
    """The ISO period an aggregate submission reports for."""

    period_type: str | None = None
    organisation_unit: str | None = None
    """The DHIS2 organisation-unit uid the capture happened at."""

    tracked_entity: str | None = None
    tracker_enrollment: str | None = None
    rejection: SpoolRejection | None = None
    """Why DHIS2 refused this receipt, on a rejected row that has a readable report beside it."""

    imported: SpoolImport | None = None
    """What DHIS2 counted for this receipt, on a forwarded row that has a readable report beside it."""

    refusal: SpoolRefusal | None = None
    """The last committing drain's refusal, on a received row that has a record beside it."""

    withdrawal: SpoolWithdrawal | None = None
    """What DHIS2 answered the retraction, on a withdrawn row that has a readable record beside it."""
Attributes
questionnaire instance-attribute

The canonical of the Questionnaire the submission answered.

questionnaire_id = None class-attribute instance-attribute

The last segment of that canonical - the id the form is served under, for joining to a title.

submitted_by = None class-attribute instance-attribute

The DHIS2 username the facade validated this submission under, or None when it validated none.

period = None class-attribute instance-attribute

The ISO period an aggregate submission reports for.

organisation_unit = None class-attribute instance-attribute

The DHIS2 organisation-unit uid the capture happened at.

rejection = None class-attribute instance-attribute

Why DHIS2 refused this receipt, on a rejected row that has a readable report beside it.

imported = None class-attribute instance-attribute

What DHIS2 counted for this receipt, on a forwarded row that has a readable report beside it.

refusal = None class-attribute instance-attribute

The last committing drain's refusal, on a received row that has a record beside it.

withdrawal = None class-attribute instance-attribute

What DHIS2 answered the retraction, on a withdrawn row that has a readable record beside it.

SpoolCounts

Bases: BaseModel

How many receipts sit in each lifecycle state - the queue depth, and what became of the rest.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolCounts(BaseModel):
    """How many receipts sit in each lifecycle state - the queue depth, and what became of the rest."""

    model_config = ConfigDict(frozen=True)

    received: int = 0
    forwarded: int = 0
    rejected: int = 0
    withdrawn: int = 0
    """Receipts DHIS2 took and `d2w fhir withdraw` retracted afterwards. Terminal: nothing leaves this state."""

    malformed: int = 0
    """Files in the holding pen. Not receipts and not a lifecycle state - bytes that would not parse as one."""
Attributes
withdrawn = 0 class-attribute instance-attribute

Receipts DHIS2 took and d2w fhir withdraw retracted afterwards. Terminal: nothing leaves this state.

malformed = 0 class-attribute instance-attribute

Files in the holding pen. Not receipts and not a lifecycle state - bytes that would not parse as one.

SpoolListing

Bases: BaseModel

One page of this project's receipts, newest first, with the whole listing's counts beside them.

total is the whole searchset rather than the page, and it is the same number on every page of one walk. next and previous are the links a client follows; the page token is this server's business and is not a number a client composes.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
class SpoolListing(BaseModel):
    """One page of this project's receipts, newest first, with the whole listing's counts beside them.

    `total` is the whole searchset rather than the page, and it is the same number on every page of
    one walk. `next` and `previous` are the links a client follows; the page token is this server's
    business and is not a number a client composes.
    """

    model_config = ConfigDict(frozen=True)

    total: int
    counts: SpoolCounts
    responses: tuple[SpoolResponseSummary, ...]
    malformed: tuple[QuarantinedFile, ...] = ()
    """Every file the spool moved aside because it does not read as a receipt, with what stopped it."""

    self_url: str = ""
    """This page, as a client may ask for it again and be handed the same page."""

    previous_url: str | None = None
    next_url: str | None = None
Attributes
malformed = () class-attribute instance-attribute

Every file the spool moved aside because it does not read as a receipt, with what stopped it.

self_url = '' class-attribute instance-attribute

This page, as a client may ask for it again and be handed the same page.

Functions:

read_spool(request, count_parameter=None, page_parameter=None) async

Answer one page of the receipts, re-reading the spool directory as it does.

The read runs off the event loop. Every fact in a listing comes from stating and parsing files, which is blocking work, and a facade doing it inline would stall every other request it is serving - including the capture that is trying to write into the same directory.

Both parameters are read as text rather than as numbers on purpose: an unreadable _count is a client asking for a page size this server does not offer, and it is answered with the default page rather than with a 422 about a listing that has one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/spool.py
@router.get(
    SPOOL_PATH,
    tags=[SPOOL_TAG],
    summary="List the stored receipts",
    description=(
        "One page of this project's receipts, newest first, each with the lifecycle state its file "
        "is currently in and whatever DHIS2 said about it. The spool directory is re-read on every "
        "call, so a `d2w fhir forward` run in another process shows up on the next request with "
        "nothing restarted.\n\n"
        "`total` is the whole listing on every page of one walk, and the per-state counts are the "
        "whole spool rather than the page - a queue depth that changed with the page you were "
        "looking at would be no queue depth at all. Walk the pages by following `next_url`; the page "
        "token is this server's to mint and not a number to compose."
    ),
    response_description="One page of receipts, the whole spool's counts, and the links either side of the page.",
)
async def read_spool(
    request: Request,
    count_parameter: Annotated[
        str | None,
        Query(
            alias=COUNT_PARAMETER,
            description="How many receipts this page carries. Out-of-range values are clamped.",
        ),
    ] = None,
    page_parameter: Annotated[
        str | None,
        Query(alias=PAGE_PARAMETER, description="An opaque cursor, taken from a previous page's `next_url`."),
    ] = None,
) -> SpoolListing:
    """Answer one page of the receipts, re-reading the spool directory as it does.

    The read runs off the event loop. Every fact in a listing comes from `stat`ing and parsing files,
    which is blocking work, and a facade doing it inline would stall every other request it is
    serving - including the capture that is trying to write into the same directory.

    Both parameters are read as text rather than as numbers on purpose: an unreadable `_count` is a
    client asking for a page size this server does not offer, and it is answered with the default
    page rather than with a 422 about a listing that has one.
    """
    context = serve_context(request)
    naming = capture_state(request).naming
    count = requested_page_size(count_parameter)
    cursor = requested_cursor(page_parameter)
    reading = await run_in_threadpool(context.spool.search)
    counts = SpoolCounts(
        received=sum(1 for receipt in reading.receipts if receipt.lifecycle is ResponseLifecycle.RECEIVED),
        forwarded=sum(1 for receipt in reading.receipts if receipt.lifecycle is ResponseLifecycle.FORWARDED),
        rejected=sum(1 for receipt in reading.receipts if receipt.lifecycle is ResponseLifecycle.REJECTED),
        withdrawn=sum(1 for receipt in reading.receipts if receipt.lifecycle is ResponseLifecycle.WITHDRAWN),
        malformed=len(reading.quarantined),
    )
    page = page_of(reading.receipts, cursor, count)
    responses = await run_in_threadpool(_summaries, context.spool, page.receipts, naming)
    return SpoolListing(
        total=page.total,
        counts=counts,
        responses=responses,
        malformed=reading.quarantined,
        self_url=_page_url(request, page.cursor, count),
        previous_url=None if page.previous_cursor is None else _page_url(request, page.previous_cursor, count),
        next_url=None if page.next_cursor is None else _page_url(request, page.next_cursor, count),
    )

Capture

Receiving a QuestionnaireResponse: the naming the capture contract is written in, the questionnaire index an answer is checked against, the terminology resolver behind a coded answer, the phase machine that runs the whole thing, and the OperationOutcome vocabulary every answer is spoken in.

naming

The URLs and identifier systems one project's capture contract is written in.

Every name here is derived from fhir.toml - the IG canonical for the extensions the profiles pin, and [generate] identifier_system_base for the DHIS2 identifier systems a response names its tracked entity, its enrollment, and its program under. Nothing is hard-coded: a project that renames its prefix token renames its extensions, and the capture path follows without an edit.

period_extension is here rather than beside either of its two callers, because both of them are writing the same D2Period out of these three sub-extension urls: $generate when it drafts an aggregate response, and the data set read-back when it serves one out of the instance. One spelling of the period is the whole point of putting it here - a draft and a served document that dated themselves differently would be two contracts wearing one profile.

Classes

CaptureNaming

Bases: BaseModel

What a QuestionnaireResponse is read against here: the extension urls, identifier systems, and profiles.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/naming.py
class CaptureNaming(BaseModel):
    """What a QuestionnaireResponse is read against here: the extension urls, identifier systems, and profiles."""

    model_config = ConfigDict(frozen=True)

    form_type_url: str
    period_url: str
    period_type_url: str
    """Extension url an aggregate form declares the DHIS2 period type its data set reports on."""

    organisation_unit_url: str
    organisation_unit_assignment_url: str
    attribute_option_combos_url: str
    """Extension url a Questionnaire declares the attribute-option-combo ValueSet its responses key from."""

    attribute_option_combo_url: str
    """Extension url a QuestionnaireResponse names the attribute option combo its values are keyed under."""

    tracker_enrollment_url: str
    enrolled_at_url: str
    """Extension url a registration response dates the enrollment it mints from."""

    incident_at_url: str
    """Extension url a registration response dates the incident that enrollment follows."""

    collects_incident_date_url: str
    """Extension url a registration form declares whether its program collects an incident date on."""

    program_rule_url: str
    """Extension url a form lists the DHIS2 program rules its instance enforces on import under, one repeat per rule."""

    subject_exists_url: str
    """Extension url a registration response states that the person it is subject to is already held on."""

    tracked_entity_system: str
    tracker_enrollment_system: str
    data_set_identifier_system: str
    """Identifier system a served Questionnaire names the DHIS2 data set it was generated from under.

    What finds the form one data set's values are read back through, exactly as
    `program_stage_identifier_system` finds a stage's: a read names a data set's UID, and the data
    set's form is the served Questionnaire carrying that UID under this system. The join is by
    identifier rather than by canonical, because what a form is called follows `[generate.naming]
    source` and what it is about does not.
    """

    program_identifier_system: str
    program_stage_identifier_system: str
    """Identifier system a served Questionnaire names the DHIS2 program stage it was generated from under.

    What finds the form one recorded event answers: an event states its stage's UID, and the stage's
    form is the served Questionnaire carrying that UID under this system. The join is by identifier
    rather than by canonical, because what a form is called follows `[generate.naming] source` and
    what it is about does not.
    """

    generate_seed_system: str
    """Identifier system the seed a `$generate` response was drawn from is stated under."""

    aggregate_response_profile_url: str
    event_response_profile_url: str
    tracker_registration_response_profile_url: str
    tracker_event_response_profile_url: str
    tracked_entity_response_profile_url: str

    @classmethod
    def from_project(cls, project: FhirProject) -> CaptureNaming:
        """Derive every capture name from the project's canonical, naming tokens, and identifier base."""
        names = FoundationNaming.from_naming(project.config.generate.naming)
        canonical = project.config.ig.canonical
        base = project.config.generate.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),
            period_type_url=_definition_url(canonical, names.period_type_extension_id),
            organisation_unit_url=_definition_url(canonical, names.organisation_unit_extension_id),
            organisation_unit_assignment_url=_definition_url(
                canonical, names.organisation_unit_assignment_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),
            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),
            collects_incident_date_url=_definition_url(canonical, names.collects_incident_date_extension_id),
            program_rule_url=_definition_url(canonical, join_id_tokens(names.definition_prefix, "program", "rule")),
            subject_exists_url=_definition_url(canonical, names.subject_exists_extension_id),
            tracked_entity_system=_identifier_system(base, "TrackedEntity"),
            tracker_enrollment_system=_identifier_system(base, "TrackerEnrollment"),
            data_set_identifier_system=_identifier_system(base, "DataSet"),
            program_identifier_system=_identifier_system(base, "Program"),
            program_stage_identifier_system=_identifier_system(base, "ProgramStage"),
            generate_seed_system=f"{canonical}/{GENERATE_SEED_IDENTIFIER_SEGMENT}",
            aggregate_response_profile_url=_definition_url(canonical, names.aggregate_response_profile_id),
            event_response_profile_url=_definition_url(canonical, names.event_response_profile_id),
            tracker_registration_response_profile_url=_definition_url(
                canonical, names.tracker_registration_response_profile_id
            ),
            tracker_event_response_profile_url=_definition_url(canonical, names.tracker_event_response_profile_id),
            tracked_entity_response_profile_url=_definition_url(canonical, names.tracked_entity_response_profile_id),
        )

    def response_profile_url(self, form_kind: FormKind) -> str:
        """The QuestionnaireResponse profile one DHIS2 form kind's complete response declares."""
        if form_kind == "aggregate":
            return self.aggregate_response_profile_url
        if form_kind == "tracker":
            return self.tracker_registration_response_profile_url
        if form_kind == "tracker-event":
            return self.tracker_event_response_profile_url
        if form_kind == "tracked-entity":
            return self.tracked_entity_response_profile_url
        return self.event_response_profile_url
Attributes
period_type_url instance-attribute

Extension url an aggregate form declares the DHIS2 period type its data set reports on.

attribute_option_combos_url instance-attribute

Extension url a Questionnaire declares the attribute-option-combo ValueSet its responses key from.

attribute_option_combo_url instance-attribute

Extension url a QuestionnaireResponse names the attribute option combo its values are keyed under.

enrolled_at_url instance-attribute

Extension url a registration response dates the enrollment it mints from.

incident_at_url instance-attribute

Extension url a registration response dates the incident that enrollment follows.

collects_incident_date_url instance-attribute

Extension url a registration form declares whether its program collects an incident date on.

program_rule_url instance-attribute

Extension url a form lists the DHIS2 program rules its instance enforces on import under, one repeat per rule.

subject_exists_url instance-attribute

Extension url a registration response states that the person it is subject to is already held on.

data_set_identifier_system instance-attribute

Identifier system a served Questionnaire names the DHIS2 data set it was generated from under.

What finds the form one data set's values are read back through, exactly as program_stage_identifier_system finds a stage's: a read names a data set's UID, and the data set's form is the served Questionnaire carrying that UID under this system. The join is by identifier rather than by canonical, because what a form is called follows [generate.naming] source and what it is about does not.

program_stage_identifier_system instance-attribute

Identifier system a served Questionnaire names the DHIS2 program stage it was generated from under.

What finds the form one recorded event answers: an event states its stage's UID, and the stage's form is the served Questionnaire carrying that UID under this system. The join is by identifier rather than by canonical, because what a form is called follows [generate.naming] source and what it is about does not.

generate_seed_system instance-attribute

Identifier system the seed a $generate response was drawn from is stated under.

Methods:
from_project(project) classmethod

Derive every capture name from the project's canonical, naming tokens, and identifier base.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/naming.py
@classmethod
def from_project(cls, project: FhirProject) -> CaptureNaming:
    """Derive every capture name from the project's canonical, naming tokens, and identifier base."""
    names = FoundationNaming.from_naming(project.config.generate.naming)
    canonical = project.config.ig.canonical
    base = project.config.generate.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),
        period_type_url=_definition_url(canonical, names.period_type_extension_id),
        organisation_unit_url=_definition_url(canonical, names.organisation_unit_extension_id),
        organisation_unit_assignment_url=_definition_url(
            canonical, names.organisation_unit_assignment_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),
        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),
        collects_incident_date_url=_definition_url(canonical, names.collects_incident_date_extension_id),
        program_rule_url=_definition_url(canonical, join_id_tokens(names.definition_prefix, "program", "rule")),
        subject_exists_url=_definition_url(canonical, names.subject_exists_extension_id),
        tracked_entity_system=_identifier_system(base, "TrackedEntity"),
        tracker_enrollment_system=_identifier_system(base, "TrackerEnrollment"),
        data_set_identifier_system=_identifier_system(base, "DataSet"),
        program_identifier_system=_identifier_system(base, "Program"),
        program_stage_identifier_system=_identifier_system(base, "ProgramStage"),
        generate_seed_system=f"{canonical}/{GENERATE_SEED_IDENTIFIER_SEGMENT}",
        aggregate_response_profile_url=_definition_url(canonical, names.aggregate_response_profile_id),
        event_response_profile_url=_definition_url(canonical, names.event_response_profile_id),
        tracker_registration_response_profile_url=_definition_url(
            canonical, names.tracker_registration_response_profile_id
        ),
        tracker_event_response_profile_url=_definition_url(canonical, names.tracker_event_response_profile_id),
        tracked_entity_response_profile_url=_definition_url(canonical, names.tracked_entity_response_profile_id),
    )
response_profile_url(form_kind)

The QuestionnaireResponse profile one DHIS2 form kind's complete response declares.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/naming.py
def response_profile_url(self, form_kind: FormKind) -> str:
    """The QuestionnaireResponse profile one DHIS2 form kind's complete response declares."""
    if form_kind == "aggregate":
        return self.aggregate_response_profile_url
    if form_kind == "tracker":
        return self.tracker_registration_response_profile_url
    if form_kind == "tracker-event":
        return self.tracker_event_response_profile_url
    if form_kind == "tracked-entity":
        return self.tracked_entity_response_profile_url
    return self.event_response_profile_url

Functions:

period_extension(period, naming)

The D2Period extension: the DHIS2 ISO identifier, its period type, and the range it resolves to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/naming.py
def period_extension(period: PeriodValue, naming: CaptureNaming) -> Extension:
    """The D2Period extension: the DHIS2 ISO identifier, its period type, and the range it resolves to."""
    return Extension(
        url=naming.period_url,
        extension=[
            Extension(url=PERIOD_ISO_SUB_EXTENSION, valueString=period.iso),
            Extension(url=PERIOD_TYPE_SUB_EXTENSION, valueCode=period.period_type),
            Extension(
                url=PERIOD_RANGE_SUB_EXTENSION,
                valuePeriod=Period(start=period.start_date.isoformat(), end=period.end_date.isoformat()),
            ),
        ],
    )

index

The capture index: one served Questionnaire, read once into what a received response is checked against.

A compiled Questionnaire is a tree; validating an answer against it is a lookup. The index does that flattening once - every question keyed by its linkId, every group link id in a set - so a submission with two thousand cells costs two thousand dictionary hits and no tree walks.

Two link-id shapes reach a question. A plain question is one DHIS2 data element - or, on a tracker registration form, one tracked entity attribute - and its link id is that object's UID, which is why CaptureQuestion.data_element_uid carries an attribute UID for the registration kind. A cell of a disaggregated aggregate form is one data element crossed with one category option combo, and its link id is <dataElement>.<categoryOptionCombo> - the form the questionnaire emitter writes and the only place the pair is carried on the wire.

Terminology binding is resolved here too. A #choice question names an answerValueSet, and the CodeSystem behind it is what a coded answer's codes are resolved against. When the value set is not in the store the binding stays open: option_system is None and the capture path validates coded answers leniently with a warning, because refusing a code against terminology this server never published would blame the client for the project's own incomplete IG.

Attributes

QuestionKind = Literal['plain', 'cell'] module-attribute

Whether a question captures a data element on its own, or one cell of its disaggregation.

Classes

UnreadableQuestionnaireError

Bases: LookupError

Raised when a canonical does not resolve to a Questionnaire this facade can check answers against.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class UnreadableQuestionnaireError(LookupError):
    """Raised when a canonical does not resolve to a Questionnaire this facade can check answers against."""

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)
        self.diagnostics = diagnostics

CaptureBound

Bases: BaseModel

One end of the range a question admits, as its minValue / maxValue extension states it.

The element the bound was written on is kept rather than flattened onto one number, because the three carry different facts: an integer bound belongs to a whole-number question, a decimal one to a measured quantity, and a date one to a calendar day - and 2026-01-01 is not a quantity at all. Every reader takes the end it can compare against and leaves the rest alone.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureBound(BaseModel):
    """One end of the range a question admits, as its `minValue` / `maxValue` extension states it.

    The element the bound was written on is kept rather than flattened onto one number, because the
    three carry different facts: an integer bound belongs to a whole-number question, a decimal one
    to a measured quantity, and a date one to a calendar day - and `2026-01-01` is not a quantity at
    all. Every reader takes the end it can compare against and leaves the rest alone.
    """

    model_config = ConfigDict(frozen=True)

    integer: int | None = None
    decimal: float | None = None
    date: str | None = None

    @property
    def number(self) -> float | None:
        """The bound as a quantity, or None when it bounds a calendar day rather than a number."""
        return float(self.integer) if self.integer is not None else self.decimal

    @property
    def stated(self) -> str:
        """The bound spelled the way the form states it - the literal a refusal names back to a client."""
        if self.integer is not None:
            return str(self.integer)
        if self.decimal is not None:
            return str(self.decimal)
        return self.date or ""
Attributes
number property

The bound as a quantity, or None when it bounds a calendar day rather than a number.

stated property

The bound spelled the way the form states it - the literal a refusal names back to a client.

CaptureBounds

Bases: BaseModel

The inclusive range one question admits, either end of it open.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureBounds(BaseModel):
    """The inclusive range one question admits, either end of it open."""

    model_config = ConfigDict(frozen=True)

    minimum: CaptureBound | None = None
    maximum: CaptureBound | None = None

CaptureProgramRule

Bases: BaseModel

One DHIS2 program rule the form declares its instance enforces when a submission is imported.

The whole of it is a claim about the instance rather than about this server: nothing here is evaluated at capture, because DHIS2 evaluates its own rules on import and answers a violation with E1300. What the declaration buys is that a client can say which rules are waiting, and that a rejection naming a rule UID can be read back as the rule's own name.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureProgramRule(BaseModel):
    """One DHIS2 program rule the form declares its instance enforces when a submission is imported.

    The whole of it is a claim about the instance rather than about this server: nothing here is
    evaluated at capture, because DHIS2 evaluates its own rules on import and answers a violation
    with `E1300`. What the declaration buys is that a client can say which rules are waiting, and
    that a rejection naming a rule UID can be read back as the rule's own name.
    """

    model_config = ConfigDict(frozen=True)

    rule_uid: str
    name: str
    description: str | None = None
    condition: str
    """The rule's DHIS2 expression, in the machine spelling the instance holds it in."""

    action: str | None = None
    """The DHIS2 program rule action type, as `SHOWWARNING` or `ERRORONCOMPLETE`, when the form states one."""
Attributes
condition instance-attribute

The rule's DHIS2 expression, in the machine spelling the instance holds it in.

action = None class-attribute instance-attribute

The DHIS2 program rule action type, as SHOWWARNING or ERRORONCOMPLETE, when the form states one.

CaptureGate

Bases: BaseModel

What one item of a served form is asked under: where it sits, and the conditions that show it.

Every item gets one of these, groups included, because a group's enableWhen decides every question beneath it and a lookup keyed only by question would lose that. An item the form always asks carries no conditions, which is what almost every DHIS2-generated item is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureGate(BaseModel):
    """What one item of a served form is asked under: where it sits, and the conditions that show it.

    Every item gets one of these, groups included, because a group's `enableWhen` decides every
    question beneath it and a lookup keyed only by question would lose that. An item the form always
    asks carries no conditions, which is what almost every DHIS2-generated item is.
    """

    model_config = ConfigDict(frozen=True)

    link_id: str
    parent_link_id: str | None = None
    conditions: tuple[QuestionnaireItemEnableWhen, ...] = ()
    behavior: Literal["all", "any"] = "all"
    """How several conditions combine. `all` is the reading that asks fewer questions, which is the safe one."""
Attributes
behavior = 'all' class-attribute instance-attribute

How several conditions combine. all is the reading that asks fewer questions, which is the safe one.

CaptureQuestion

Bases: BaseModel

One answerable question of a served form, as a received answer is checked against it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureQuestion(BaseModel):
    """One answerable question of a served form, as a received answer is checked against it."""

    model_config = ConfigDict(frozen=True)

    link_id: str
    kind: QuestionKind
    data_element_uid: str
    """The DHIS2 object the question asks: a data element, or a tracked entity attribute on the registration kind."""

    category_option_combo_uid: str | None = None
    item_type: str
    answer_element: str
    repeats: bool = False
    required: bool = False
    read_only: bool = False
    """Whether the form states that DHIS2 owns this question's value, as `item.readOnly`.

    True on a tracked entity attribute DHIS2 generates: the instance mints the value on import, so
    nothing a client sends for it is used. A generated attribute is answered by DHIS2 and therefore
    left unanswered by `$generate`, and its absence is admitted even when the form marks it required.
    """

    bounds: CaptureBounds | None = None
    option_system: str | None = None
    """Canonical of the CodeSystem a coded answer is resolved against, or None when the binding is open."""

    value_type: str | None = None
    """The DHIS2 value type the served support CodeSystem states for the question's object, when it serves one."""

    unique: bool = False
    """Whether the served terminology declares the question's tracked entity attribute a unique business identifier."""

    display: str | None = None
    """What the question's object is called - the support CodeSystem's display, else the item's own coding's."""
Attributes
data_element_uid instance-attribute

The DHIS2 object the question asks: a data element, or a tracked entity attribute on the registration kind.

read_only = False class-attribute instance-attribute

Whether the form states that DHIS2 owns this question's value, as item.readOnly.

True on a tracked entity attribute DHIS2 generates: the instance mints the value on import, so nothing a client sends for it is used. A generated attribute is answered by DHIS2 and therefore left unanswered by $generate, and its absence is admitted even when the form marks it required.

option_system = None class-attribute instance-attribute

Canonical of the CodeSystem a coded answer is resolved against, or None when the binding is open.

value_type = None class-attribute instance-attribute

The DHIS2 value type the served support CodeSystem states for the question's object, when it serves one.

unique = False class-attribute instance-attribute

Whether the served terminology declares the question's tracked entity attribute a unique business identifier.

display = None class-attribute instance-attribute

What the question's object is called - the support CodeSystem's display, else the item's own coding's.

QuestionFacts

Bases: BaseModel

The DHIS2 facts a support-CodeSystem concept states about one question's object.

A question's item codes its data element or tracked entity attribute from the generated support pair, and that CodeSystem's concept carries what the compiled Questionnaire itself does not: the DHIS2 value type, and - for a tracked entity attribute - whether DHIS2 declares it unique. Everything defaults to the absence a store without the pair serves.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class QuestionFacts(BaseModel):
    """The DHIS2 facts a support-CodeSystem concept states about one question's object.

    A question's item codes its data element or tracked entity attribute from the generated
    support pair, and that CodeSystem's concept carries what the compiled Questionnaire itself
    does not: the DHIS2 value type, and - for a tracked entity attribute - whether DHIS2 declares
    it unique. Everything defaults to the absence a store without the pair serves.
    """

    model_config = ConfigDict(frozen=True)

    value_type: str | None = None
    unique: bool = False
    display: str | None = None

CaptureAssignment

Bases: BaseModel

The organisation units one form may be captured against, as its published assignment List names them.

references holds the literal Location/<id> references the List entries carry, which is the exact spelling a subject, a tracker organisation-unit extension, and an ORGANISATION_UNIT answer are written in - so membership is a set lookup rather than a resolution.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureAssignment(BaseModel):
    """The organisation units one form may be captured against, as its published assignment List names them.

    `references` holds the literal `Location/<id>` references the List entries carry, which is the
    exact spelling a subject, a tracker organisation-unit extension, and an `ORGANISATION_UNIT`
    answer are written in - so membership is a set lookup rather than a resolution.
    """

    model_config = ConfigDict(frozen=True)

    list_id: str
    references: frozenset[str] = frozenset()

    def admits(self, reference: str) -> bool:
        """Whether one Location reference is inside the assignment."""
        return reference in self.references
Methods:
admits(reference)

Whether one Location reference is inside the assignment.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
def admits(self, reference: str) -> bool:
    """Whether one Location reference is inside the assignment."""
    return reference in self.references

CaptureAttributeOptionCombos

Bases: BaseModel

The attribute-option-combo vocabulary one form declares, and the CodeSystem a coding into it names.

system is None when the facade serves no readable ValueSet for the declared canonical. The declaration still stands - the form says its responses carry one - but which concepts are in it cannot be checked, exactly as an unpublished answerValueSet leaves a coded answer's binding open.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureAttributeOptionCombos(BaseModel):
    """The attribute-option-combo vocabulary one form declares, and the CodeSystem a coding into it names.

    `system` is None when the facade serves no readable ValueSet for the declared canonical. The
    declaration still stands - the form says its responses carry one - but which concepts are in it
    cannot be checked, exactly as an unpublished `answerValueSet` leaves a coded answer's binding open.
    """

    model_config = ConfigDict(frozen=True)

    value_set: str
    system: str | None = None

CaptureIndex

Bases: BaseModel

One served Questionnaire flattened into the lookups a received response is validated with.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureIndex(BaseModel):
    """One served Questionnaire flattened into the lookups a received response is validated with."""

    model_config = ConfigDict(frozen=True)

    canonical: str
    form_kind: FormKind
    target_uid: str
    """The DHIS2 data set, program, or program stage UID the form was generated from."""

    program_uid: str | None = None
    subject_type: str = DEFAULT_SUBJECT_RESOURCE_TYPE
    """The resource type the form declares its responses are about - `Questionnaire.subjectType`.

    A DHIS2 tracked entity type is not always a person, so a project maps its types onto the FHIR
    resource types they are and the generated form states the answer. The served form is the only
    place this facade reads it from: `fhir.toml` is the generator's input and is not on the server
    at all, so what a capture is checked against and what `$generate` mints is what the compiled
    Questionnaire says.
    """

    collects_incident_date: bool = False
    """Whether the program a registration form enrols into collects the date of the incident it follows.

    The form's own `D2CollectsIncidentDate` declaration, which is the only place this facade reads
    the fact from - a compiled store and a `--live` one publish the same declaration, so both
    generate the same registration envelope. A form declaring nothing reads as false, which is what
    the contract makes of a response carrying no `D2IncidentAt`: complete, the extension being 0..1.
    """

    period_type: str | None = None
    """The DHIS2 period type an aggregate form's data set reports on, or None on a form declaring none.

    The form's own `D2PeriodType` declaration, which every served form carries: the FSH template and
    the JSON builder both write it, so a compiled store and a `--live` one state the same type and
    `$generate` reports for the same period in either mode. None means the form declares nothing,
    which is what a non-aggregate form declares and what an aggregate form whose data set states no
    period type declares.
    """

    program_rules: tuple[CaptureProgramRule, ...] = ()
    """The DHIS2 program rules the form declares, in the order it lists them - none on a form that lists none."""

    questions: dict[str, CaptureQuestion] = Field(default_factory=dict)
    group_link_ids: frozenset[str] = frozenset()
    gates: dict[str, CaptureGate] = Field(default_factory=dict)
    """Every item of the form, group and question alike, keyed by link id - what each one is asked under."""

    item_link_ids: tuple[str, ...] = ()
    """Every item's link id in document order, which is the order the gates resolve in - a parent before its child."""
    assignment: CaptureAssignment | None = None
    """The form's organisation-unit assignment, or None - which means every published unit may report it."""

    attribute_option_combos: CaptureAttributeOptionCombos | None = None
    """The vocabulary the form's responses key their values from, or None on the default category combo."""
Attributes
target_uid instance-attribute

The DHIS2 data set, program, or program stage UID the form was generated from.

subject_type = DEFAULT_SUBJECT_RESOURCE_TYPE class-attribute instance-attribute

The resource type the form declares its responses are about - Questionnaire.subjectType.

A DHIS2 tracked entity type is not always a person, so a project maps its types onto the FHIR resource types they are and the generated form states the answer. The served form is the only place this facade reads it from: fhir.toml is the generator's input and is not on the server at all, so what a capture is checked against and what $generate mints is what the compiled Questionnaire says.

collects_incident_date = False class-attribute instance-attribute

Whether the program a registration form enrols into collects the date of the incident it follows.

The form's own D2CollectsIncidentDate declaration, which is the only place this facade reads the fact from - a compiled store and a --live one publish the same declaration, so both generate the same registration envelope. A form declaring nothing reads as false, which is what the contract makes of a response carrying no D2IncidentAt: complete, the extension being 0..1.

period_type = None class-attribute instance-attribute

The DHIS2 period type an aggregate form's data set reports on, or None on a form declaring none.

The form's own D2PeriodType declaration, which every served form carries: the FSH template and the JSON builder both write it, so a compiled store and a --live one state the same type and $generate reports for the same period in either mode. None means the form declares nothing, which is what a non-aggregate form declares and what an aggregate form whose data set states no period type declares.

program_rules = () class-attribute instance-attribute

The DHIS2 program rules the form declares, in the order it lists them - none on a form that lists none.

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

Every item of the form, group and question alike, keyed by link id - what each one is asked under.

Every item's link id in document order, which is the order the gates resolve in - a parent before its child.

assignment = None class-attribute instance-attribute

The form's organisation-unit assignment, or None - which means every published unit may report it.

attribute_option_combos = None class-attribute instance-attribute

The vocabulary the form's responses key their values from, or None on the default category combo.

CaptureIndexCache

Bases: BaseModel

The per-canonical index cache one running facade keeps, built on first use and held for the process.

The store is immutable for the life of the process, so an index built from it never goes stale. The cache is the facade's second stateful object after the spool, and like the spool it assumes the single writing process d2w fhir serve is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
class CaptureIndexCache(BaseModel):
    """The per-canonical index cache one running facade keeps, built on first use and held for the process.

    The store is immutable for the life of the process, so an index built from it never goes
    stale. The cache is the facade's second stateful object after the spool, and like the spool
    it assumes the single writing process `d2w fhir serve` is.
    """

    model_config = ConfigDict(frozen=True)

    _indexes: dict[str, CaptureIndex] = PrivateAttr(default_factory=dict)

    def resolve(self, canonical: str, naming: CaptureNaming, store: ResourceStore) -> CaptureIndex:
        """The index for one questionnaire canonical, building it the first time it is asked for."""
        cached = self._indexes.get(canonical)
        if cached is not None:
            return cached
        entry = store.by_canonical(canonical)
        if entry is None:
            raise UnreadableQuestionnaireError(f"no Questionnaire with canonical `{canonical}` is served here")
        if entry.resource_type != QUESTIONNAIRE_RESOURCE_TYPE:
            raise UnreadableQuestionnaireError(
                f"`{canonical}` is served here as a {entry.resource_type}, not a Questionnaire"
            )
        index = build_capture_index(entry.body, naming, store)
        self._indexes[canonical] = index
        return index

    def count(self) -> int:
        """How many questionnaires have been indexed so far."""
        return len(self._indexes)
Methods:
resolve(canonical, naming, store)

The index for one questionnaire canonical, building it the first time it is asked for.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
def resolve(self, canonical: str, naming: CaptureNaming, store: ResourceStore) -> CaptureIndex:
    """The index for one questionnaire canonical, building it the first time it is asked for."""
    cached = self._indexes.get(canonical)
    if cached is not None:
        return cached
    entry = store.by_canonical(canonical)
    if entry is None:
        raise UnreadableQuestionnaireError(f"no Questionnaire with canonical `{canonical}` is served here")
    if entry.resource_type != QUESTIONNAIRE_RESOURCE_TYPE:
        raise UnreadableQuestionnaireError(
            f"`{canonical}` is served here as a {entry.resource_type}, not a Questionnaire"
        )
    index = build_capture_index(entry.body, naming, store)
    self._indexes[canonical] = index
    return index
count()

How many questionnaires have been indexed so far.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
def count(self) -> int:
    """How many questionnaires have been indexed so far."""
    return len(self._indexes)

Functions:

build_capture_index(questionnaire_body, naming, store)

Flatten one compiled Questionnaire into its capture index, resolving every terminology binding.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
def build_capture_index(
    questionnaire_body: dict[str, Any],
    naming: CaptureNaming,
    store: ResourceStore,
) -> CaptureIndex:
    """Flatten one compiled Questionnaire into its capture index, resolving every terminology binding."""
    try:
        questionnaire = Questionnaire.model_validate(questionnaire_body)
    except ValidationError as error:
        raise UnreadableQuestionnaireError(f"the served Questionnaire could not be read ({error})") from error
    canonical = questionnaire.url
    if not canonical:
        raise UnreadableQuestionnaireError("the served Questionnaire carries no canonical url")
    form_kind = _form_kind(questionnaire, naming, canonical)

    questions: dict[str, CaptureQuestion] = {}
    group_link_ids: set[str] = set()
    facts_cache: dict[str, dict[str, QuestionFacts]] = {}
    gates: dict[str, CaptureGate] = {}
    _walk(questionnaire.item or [], None, store, questions, group_link_ids, gates, facts_cache)

    return CaptureIndex(
        canonical=canonical,
        form_kind=form_kind,
        target_uid=canonical.rsplit("/", 1)[-1],
        program_uid=_program_uid(questionnaire, naming),
        subject_type=_subject_type(questionnaire),
        collects_incident_date=_collects_incident_date(questionnaire, naming),
        period_type=_period_type(questionnaire, naming),
        program_rules=_program_rules(questionnaire, naming),
        questions=questions,
        group_link_ids=frozenset(group_link_ids),
        gates=gates,
        item_link_ids=tuple(gates),
        assignment=_assignment(questionnaire, naming, store),
        attribute_option_combos=_attribute_option_combos(questionnaire, naming, store),
    )

Every item the form is asking, given the answers on hand - R4 enableWhen, ancestors included.

THE UNANSWERED RULE. A condition names a question, and a question with no answer satisfies no comparison: =, !=, and the four orderings are all false against nothing, because there is no value to compare. exists is the one operator that reads absence as a fact, and it holds when what it found matches the sense it states - exists=false against an unanswered question is true. A condition naming a question this form does not have never holds, which hides the item rather than showing it unconditionally: the conservative direction for a capture form.

A HIDDEN ITEM CARRIES NO ANSWER. What the set leaves out is what a submission must not answer - a stale answer under a question the form stopped asking is exactly the state DHIS2's own program rules exist to prevent, and it would be forwarded as a real data value. Callers drop what falls outside the set rather than keeping it for later.

The pass runs in document order, so an ancestor's verdict is settled before its children are reached and a group's conditions decide everything beneath it in one sweep.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/index.py
def asked_link_ids(
    index: CaptureIndex,
    answers: Mapping[str, Sequence[QuestionnaireResponseAnswer]],
) -> frozenset[str]:
    """Every item the form is asking, given the answers on hand - R4 `enableWhen`, ancestors included.

    THE UNANSWERED RULE. A condition names a question, and a question with no answer satisfies no
    comparison: `=`, `!=`, and the four orderings are all false against nothing, because there is no
    value to compare. `exists` is the one operator that reads absence as a fact, and it holds when
    what it found matches the sense it states - `exists=false` against an unanswered question is
    true. A condition naming a question this form does not have never holds, which hides the item
    rather than showing it unconditionally: the conservative direction for a capture form.

    A HIDDEN ITEM CARRIES NO ANSWER. What the set leaves out is what a submission must not answer -
    a stale answer under a question the form stopped asking is exactly the state DHIS2's own program
    rules exist to prevent, and it would be forwarded as a real data value. Callers drop what falls
    outside the set rather than keeping it for later.

    The pass runs in document order, so an ancestor's verdict is settled before its children are
    reached and a group's conditions decide everything beneath it in one sweep.
    """
    asked: set[str] = set()
    for link_id in index.item_link_ids:
        gate = index.gates.get(link_id)
        if gate is None:
            continue
        if gate.parent_link_id is not None and gate.parent_link_id not in asked:
            continue
        if _conditions_hold(index, gate, answers):
            asked.add(link_id)
    return frozenset(asked)

resolve

Resolving a received code back to the DHIS2 option it names, against the terminology this facade serves.

The generated CodeSystem carries every DHIS2 option twice. concept_code_source = "id" publishes the option UID as the concept code and rides the option code along as the dhis2-code property; concept_code_source = "code" publishes the option code and rides the UID along as dhis2-id. Both spellings therefore identify the same option, and both are in the served document - which is why a client that sends the wrong one is answered with a warning rather than a refusal.

Resolution is tiered, strictly in that order:

  1. concept code - what the contract asks for, and the only tier a strict server accepts.
  2. option UID - a client that sent the DHIS2 UID against a code-mode CodeSystem.
  3. DHIS2 code - a client that sent the DHIS2 option code against an id-mode CodeSystem.

Two matches inside one tier is not something leniency can paper over: the server cannot say which option was meant, and the phase that writes to DHIS2 would have to guess. That is an ambiguity, reported as such, whatever the strictness setting.

Attributes

CodingMatchTier = Literal['concept-code', 'option-uid', 'dhis2-code'] module-attribute

Which spelling of an option a received code matched on.

Classes

CodingResolutionError

Bases: Exception

A received code the served terminology could not be resolved to exactly one option.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class CodingResolutionError(Exception):
    """A received code the served terminology could not be resolved to exactly one option."""

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)
        self.diagnostics = diagnostics

AmbiguousCodingError

Bases: CodingResolutionError

The code names more than one option at the same tier, so the server cannot say which was meant.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class AmbiguousCodingError(CodingResolutionError):
    """The code names more than one option at the same tier, so the server cannot say which was meant."""

    def __init__(self, code: str, candidates: tuple[ResolvedCoding, ...]) -> None:
        named = ", ".join(candidate.option_uid for candidate in candidates)
        super().__init__(f"code `{code}` matches more than one option in the served terminology ({named})")
        self.code = code
        self.candidates = candidates

UnresolvableCodingError

Bases: CodingResolutionError

The code names no option of the code system the question is bound to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class UnresolvableCodingError(CodingResolutionError):
    """The code names no option of the code system the question is bound to."""

    def __init__(self, code: str, system: str) -> None:
        super().__init__(f"code `{code}` is not in the served terminology `{system}`")
        self.code = code
        self.system = system

ResolvedCoding

Bases: BaseModel

One DHIS2 option a received code resolved to, and which spelling of it matched.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class ResolvedCoding(BaseModel):
    """One DHIS2 option a received code resolved to, and which spelling of it matched."""

    model_config = ConfigDict(frozen=True)

    option_uid: str
    dhis2_code: str | None = None
    concept_code: str
    display: str | None = None
    """The concept's display, as the published CodeSystem states it - what a generated coding carries."""

    matched_by: CodingMatchTier = "concept-code"

    def as_matched(self, tier: CodingMatchTier) -> ResolvedCoding:
        """The same option, recording which tier the received code matched it on."""
        return self.model_copy(update={"matched_by": tier})
Attributes
display = None class-attribute instance-attribute

The concept's display, as the published CodeSystem states it - what a generated coding carries.

Methods:
as_matched(tier)

The same option, recording which tier the received code matched it on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
def as_matched(self, tier: CodingMatchTier) -> ResolvedCoding:
    """The same option, recording which tier the received code matched it on."""
    return self.model_copy(update={"matched_by": tier})

CodingResolver

Bases: BaseModel

Every option of one served CodeSystem, resolvable by each of the three spellings it can be sent as.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class CodingResolver(BaseModel):
    """Every option of one served CodeSystem, resolvable by each of the three spellings it can be sent as."""

    model_config = ConfigDict(frozen=True)

    system: str
    options: tuple[ResolvedCoding, ...] = ()

    @classmethod
    def from_code_system_json(cls, body: dict[str, Any]) -> CodingResolver | None:
        """Read one served CodeSystem into a resolver, or None when it is not one this facade can read."""
        try:
            code_system = CodeSystem.model_validate(body)
        except ValidationError:
            return None
        if not code_system.url:
            return None
        options = tuple(_option(concept) for concept in code_system.concept or [] if concept.code)
        return cls(system=code_system.url, options=options)

    def resolve(self, code: str, strict: bool) -> ResolvedCoding:
        """Resolve one received code to its option, refusing anything but the concept code when strict."""
        matched = self._tier(code, lambda option: option.concept_code)
        if matched is not None:
            return matched.as_matched("concept-code")
        if strict:
            raise UnresolvableCodingError(code, self.system)
        by_uid = self._tier(code, lambda option: option.option_uid)
        if by_uid is not None:
            return by_uid.as_matched("option-uid")
        by_code = self._tier(code, lambda option: option.dhis2_code)
        if by_code is not None:
            return by_code.as_matched("dhis2-code")
        raise UnresolvableCodingError(code, self.system)

    def _tier(self, code: str, spelling: Callable[[ResolvedCoding], str | None]) -> ResolvedCoding | None:
        """The single option matching the code on one spelling, or None when that tier holds no match."""
        matches = [option for option in self.options if spelling(option) == code]
        if len(matches) > 1:
            raise AmbiguousCodingError(code, tuple(matches))
        return matches[0] if matches else None
Methods:
from_code_system_json(body) classmethod

Read one served CodeSystem into a resolver, or None when it is not one this facade can read.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
@classmethod
def from_code_system_json(cls, body: dict[str, Any]) -> CodingResolver | None:
    """Read one served CodeSystem into a resolver, or None when it is not one this facade can read."""
    try:
        code_system = CodeSystem.model_validate(body)
    except ValidationError:
        return None
    if not code_system.url:
        return None
    options = tuple(_option(concept) for concept in code_system.concept or [] if concept.code)
    return cls(system=code_system.url, options=options)
resolve(code, strict)

Resolve one received code to its option, refusing anything but the concept code when strict.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
def resolve(self, code: str, strict: bool) -> ResolvedCoding:
    """Resolve one received code to its option, refusing anything but the concept code when strict."""
    matched = self._tier(code, lambda option: option.concept_code)
    if matched is not None:
        return matched.as_matched("concept-code")
    if strict:
        raise UnresolvableCodingError(code, self.system)
    by_uid = self._tier(code, lambda option: option.option_uid)
    if by_uid is not None:
        return by_uid.as_matched("option-uid")
    by_code = self._tier(code, lambda option: option.dhis2_code)
    if by_code is not None:
        return by_code.as_matched("dhis2-code")
    raise UnresolvableCodingError(code, self.system)

CodingResolverSet

Bases: BaseModel

The resolvers one running facade has built, one per option system, on first use.

A form binds a handful of option sets and a submission answers against the same ones over and over, so the CodeSystem is parsed once per system for the life of the process rather than once per coded answer. A system the store does not hold caches as a miss, which is what leaves the capture path validating that question's answers leniently.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
class CodingResolverSet(BaseModel):
    """The resolvers one running facade has built, one per option system, on first use.

    A form binds a handful of option sets and a submission answers against the same ones over and
    over, so the CodeSystem is parsed once per system for the life of the process rather than once
    per coded answer. A system the store does not hold caches as a miss, which is what leaves the
    capture path validating that question's answers leniently.
    """

    model_config = ConfigDict(frozen=True)

    store: ResourceStore

    _resolvers: dict[str, CodingResolver | None] = PrivateAttr(default_factory=dict)

    def for_system(self, system: str) -> CodingResolver | None:
        """The resolver for one option system, or None when this facade serves no readable CodeSystem for it."""
        if system in self._resolvers:
            return self._resolvers[system]
        resolver = self._build(system)
        self._resolvers[system] = resolver
        return resolver

    def _build(self, system: str) -> CodingResolver | None:
        """Read the served CodeSystem behind one option system, if there is one."""
        entry = self.store.by_canonical(system)
        if entry is None or entry.resource_type != CODE_SYSTEM_RESOURCE_TYPE:
            return None
        return CodingResolver.from_code_system_json(entry.body)
Methods:
for_system(system)

The resolver for one option system, or None when this facade serves no readable CodeSystem for it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/resolve.py
def for_system(self, system: str) -> CodingResolver | None:
    """The resolver for one option system, or None when this facade serves no readable CodeSystem for it."""
    if system in self._resolvers:
        return self._resolvers[system]
    resolver = self._build(system)
    self._resolvers[system] = resolver
    return resolver

validate

The capture phase machine: what a received QuestionnaireResponse has to clear before it is stored.

Validation runs in phases, and a phase that finds an error is the last one to run. That ordering is what makes the rejection readable: there is no point telling a client its answers are wrong when the server could not tell which form kind the submission claims to be, and no point checking a period against a questionnaire that is not served here. Inside a phase every issue is collected, so one round trip reports every problem at that level rather than one at a time.

0. Read the body      - JSON, `resourceType`, and the R4 shape of a QuestionnaireResponse (400).
1. Read the contract  - the D2FormType kind, the lifecycle status this project accepts, then the
                        invariants that kind's profile pins (422).
2. Resolve the form   - the questionnaire canonical, its served Questionnaire, its index, and
                        the resource type that form declares its subject is (422).
3. Read the assignment - the organisation unit the response reports for, against the form's List (422).
4. Read the third key - the attribute option combo, against the vocabulary the form declares (422).
5. Read the period    - an aggregate response's ISO period, its type, and the range it claims (422).
6. Walk the answers   - every item against the index: link ids, cardinality, types, terminology (422).

Four of those grade against the strictness dial rather than absolutely. A coded answer in a spelling the contract does not ask for, an organisation unit outside the form's published assignment, an attribute option combo that disagrees with what the form declares, and a subject typed as a resource the form is not answered about are all warnings by default and refusals under --strict-codes.

Phase 1 is where the tracker registration contract is read, and it is the one contract whose identifiers the client mints: the enrollment its extension names, and - unless the response states D2SubjectExists - the tracked entity it is subject to, neither of which exists on any instance yet. Two things follow. The identifiers are checked for shape - a DHIS2 UID, one ASCII letter and ten alphanumeric places - because that is everything a server without an instance can honestly say about an identifier whether it was minted here or read off the instance. And a unique tracked entity attribute is not checked for uniqueness: this facade holds no instance data, so a global uniqueness claim it cannot verify would be a lie. DHIS2 enforces uniqueness at import time, and the receipt is refused there rather than here.

D2SubjectExists itself is graded on shape alone. Whether the UID it marks names a person the instance holds is the instance's answer, and what an enrollment-only import can carry is a translation question the forwarder settles - so capture admits the marker and grades the envelope exactly as it grades one without it.

The incident date is graded the same honest way. D2IncidentAt is 0..1 on the registration profile, so capture checks that a carried incident date reads as an R4 dateTime and never refuses one for being absent - refusing what the published profile admits would be the server arguing with its own contract. The form does say whether its program collects one, on D2CollectsIncidentDate, and a registration that leaves the date out where the form says it belongs is warned about rather than refused, naming the E1023 DHIS2 answers such an enrollment with.

Warnings never reject. They record what the server had to interpret or could not check - a code sent in a spelling the contract does not ask for, a required question left unanswered, a date range that disagrees with the ISO period it was derived from - and they ride back on the OperationOutcome of the accepted capture and into the stored receipt.

A CORRECTION AND A WITHDRAWAL ARE POSTURES, NOT SHAPES. R4 spells both on the response itself - status = "amended" says the submission corrects one this project already sent, and status = "entered-in-error" says it retracts one - and whether a deployment receives either is [forward] corrections and [forward] withdrawals in its fhir.toml. Both default to off, and with the dial off the submission is refused here rather than spooled for a drain that would never act on it: a receipt accepted and then never forwarded is a client told "kept" about a fact that never reaches the instance. With the dial on the submission is stored like any other receipt, status and all - what a drain then does with it is docs/fhir/design/data-lifecycle.md.

Nothing here talks to DHIS2. A capture is validated against the served IG and stored; translating a receipt into DHIS2 data values, events, and enrollments is a later phase.

Attributes

DEFAULT_STRICT_CODES = False module-attribute

Whether a code outside the served terminology rejects the capture, unless a project says otherwise.

Roadmap decision 5.1: provisionally lenient at serve. A generated IG is compiled from a DHIS2 instance at a point in time, and an option added since is a fact about the instance, not a mistake by the client - so the default records the drift as a warning and stores the submission. This constant is the single flip point for that decision; ServeSettings.strict_codes is the runtime source a request is actually validated against.

Classes

CaptureLifecyclePostures

Bases: BaseModel

Whether this project receives a submission that corrects, or one that retracts, a forwarded receipt.

The [forward] dials of fhir.toml, read at capture time by the server that receives the submission rather than only by the drain that would act on it. Both default to off, which is what a project that says nothing gets: publishing forms and forwarding them is not the same decision as letting a submitter reach back into what DHIS2 already holds.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/validate.py
class CaptureLifecyclePostures(BaseModel):
    """Whether this project receives a submission that corrects, or one that retracts, a forwarded receipt.

    The `[forward]` dials of `fhir.toml`, read at capture time by the server that receives the
    submission rather than only by the drain that would act on it. Both default to off, which is what
    a project that says nothing gets: publishing forms and forwarding them is not the same decision as
    letting a submitter reach back into what DHIS2 already holds.
    """

    model_config = ConfigDict(frozen=True)

    corrections: CorrectionPosture = CorrectionPosture.OFF
    withdrawals: WithdrawalPosture = WithdrawalPosture.OFF

    @classmethod
    def from_project(cls, project: FhirProject) -> CaptureLifecyclePostures:
        """The two dials as this project's `fhir.toml` states them - the same values `d2w fhir forward` reads."""
        return cls(corrections=project.config.forward.corrections, withdrawals=project.config.forward.withdrawals)
Methods:
from_project(project) classmethod

The two dials as this project's fhir.toml states them - the same values d2w fhir forward reads.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/validate.py
@classmethod
def from_project(cls, project: FhirProject) -> CaptureLifecyclePostures:
    """The two dials as this project's `fhir.toml` states them - the same values `d2w fhir forward` reads."""
    return cls(corrections=project.config.forward.corrections, withdrawals=project.config.forward.withdrawals)

ValidatedCapture

Bases: BaseModel

A submission that cleared every phase: what it is, what it answers, and what the server noted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/validate.py
class ValidatedCapture(BaseModel):
    """A submission that cleared every phase: what it is, what it answers, and what the server noted."""

    model_config = ConfigDict(frozen=True)

    form_kind: FormKind
    canonical: str
    warnings: tuple[CaptureIssue, ...] = ()
    response: dict[str, Any]
    """The submission as it arrived - the same byte-faithful escape hatch `StoredResponseEnvelope.response` documents.

    What is stored has to read back as what the client sent, so the parsed document is carried
    verbatim rather than re-serialised from the model it was validated through. The dict leaves
    this model only into the spool and out again as an HTTP response body.
    """
Attributes
response instance-attribute

The submission as it arrived - the same byte-faithful escape hatch StoredResponseEnvelope.response documents.

What is stored has to read back as what the client sent, so the parsed document is carried verbatim rather than re-serialised from the model it was validated through. The dict leaves this model only into the spool and out again as an HTTP response body.

Functions:

validate_response(raw_body, indexes, naming, store, strict_codes=DEFAULT_STRICT_CODES, postures=DEFAULT_LIFECYCLE_POSTURES)

Run every phase over one received body, answering with what to store or raising what to refuse.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/validate.py
def validate_response(
    raw_body: bytes,
    indexes: CaptureIndexCache,
    naming: CaptureNaming,
    store: ResourceStore,
    strict_codes: bool = DEFAULT_STRICT_CODES,
    postures: CaptureLifecyclePostures = DEFAULT_LIFECYCLE_POSTURES,
) -> ValidatedCapture:
    """Run every phase over one received body, answering with what to store or raising what to refuse."""
    payload = _read_body(raw_body)
    response = _read_response(payload)
    warnings: list[CaptureIssue] = []

    form_kind = _declared_form_kind(response, naming)
    _refuse_unreceived_lifecycle_status(response, postures)
    _settle(_profile_issues(response, naming, form_kind), warnings)

    resolvers = CodingResolverSet(store=store)
    index = _resolve_index(response.questionnaire or "", form_kind, indexes, naming, store)
    _settle(_subject_type_issues(response, index, form_kind, strict=strict_codes), warnings)
    _settle(_incident_date_issues(response, index, naming, form_kind), warnings)
    _settle(_assignment_issues(response, index, naming, form_kind, strict=strict_codes), warnings)
    _settle(_attribute_option_combo_issues(response, index, naming, resolvers, strict=strict_codes), warnings)
    if form_kind == "aggregate":
        _settle(_period_issues(response, naming), warnings)
    _settle(_ItemValidator(index=index, resolvers=resolvers, strict=strict_codes).run(response), warnings)

    return ValidatedCapture(
        form_kind=form_kind,
        canonical=index.canonical,
        warnings=tuple(warnings),
        response=payload,
    )

outcome

What the capture path says back: one issue vocabulary, and the two OperationOutcomes it builds.

A capture answers with an OperationOutcome whether it succeeded or not, because a response the server accepted with warnings is the interesting case: the submission is stored, and the client still has to be told what the server could not check or had to interpret. Rejections and acceptances therefore share one issue type, and differ only in the severity they carry.

The issue codes are R4's own (OperationOutcome.issue.code), narrowed to the ones a capture can raise. They are wider than the read path's vocabulary in errors.py on purpose: a read either finds a resource or does not, while a capture fails against a profile, a terminology, or a questionnaire's own item tree, and R4 names those failures separately.

Attributes

CaptureIssueSeverity = Literal['error', 'warning', 'information'] module-attribute

The OperationOutcome.issue.severity values a capture reports.

CaptureIssueCode = Literal['invalid', 'structure', 'required', 'value', 'invariant', 'code-invalid', 'multiple-matches', 'not-found', 'not-supported', 'business-rule', 'informational'] module-attribute

The OperationOutcome.issue.code values a capture reports.

Classes

CaptureIssue

Bases: BaseModel

One thing the capture path found: where it is, what it is, and how badly it went.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/outcome.py
class CaptureIssue(BaseModel):
    """One thing the capture path found: where it is, what it is, and how badly it went."""

    model_config = ConfigDict(frozen=True)

    severity: CaptureIssueSeverity
    code: CaptureIssueCode
    expression: str | None = None
    diagnostics: str | None = None

    def is_error(self) -> bool:
        """Whether this issue rejects the submission rather than annotating it."""
        return self.severity == "error"
Methods:
is_error()

Whether this issue rejects the submission rather than annotating it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/outcome.py
def is_error(self) -> bool:
    """Whether this issue rejects the submission rather than annotating it."""
    return self.severity == "error"

CaptureRejection

Bases: Exception

A submission the facade refused, carrying the status it answers with and every issue found.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/outcome.py
class CaptureRejection(Exception):
    """A submission the facade refused, carrying the status it answers with and every issue found."""

    def __init__(self, http_status: int, issues: tuple[CaptureIssue, ...]) -> None:
        super().__init__(f"capture rejected with {http_status}: {len(issues)} issue(s)")
        self.http_status = http_status
        self.issues = issues

Functions:

rejection_outcome(issues)

The body a refused capture answers with: every issue the failing phase found, in the order found.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/outcome.py
def rejection_outcome(issues: tuple[CaptureIssue, ...]) -> OperationOutcome:
    """The body a refused capture answers with: every issue the failing phase found, in the order found."""
    return OperationOutcome(issue=[_issue(issue) for issue in issues])

success_outcome(response_id, warnings)

The body an accepted capture answers with: what was stored, then whatever the server had to note.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capture/outcome.py
def success_outcome(response_id: str, warnings: tuple[CaptureIssue, ...]) -> OperationOutcome:
    """The body an accepted capture answers with: what was stored, then whatever the server had to note."""
    stored = OperationOutcomeIssue(
        severity="information",
        code="informational",
        diagnostics=f"stored response {response_id}; {RECEIPT_NOTE}",
    )
    return OperationOutcome(issue=[stored, *(_issue(warning) for warning in warnings)])

The register

GET /{resourceType} and GET /{resourceType}/{id}, answered from the DHIS2 instance rather than from the store, and only by a process started with --live. Which resource types those are is the published D2TET_CM's to say - one per FHIR resource the map takes a registered tracked entity type onto - so a project tracking people alone serves Patient and one that also registers samples serves Specimen beside it. The index reads what the guide publishes about the instance's subjects, the surface narrows that by [serve.tracked_entities], the wire module holds the empirical /api/tracker/trackedEntities contract the search obeys, and the projection turns one tracked entity into the resource its type is registered as, carrying identity and no claim the target resource otherwise defines - each module docstring states why. The filtering module is the register's value filter, d2-attribute={attributeUid}|{value}: which attributes each register answers it on, how the two backends run it, and why it answers equality and nothing else.

index

What the published guide already says about the instance's tracked entities, read once at startup.

An identifier search arrives as system|value and has to become a DHIS2 query. Everything needed for that translation is already in the store, put there by the generate targets:

  • A registration Questionnaire carries the tracked entity type it registers, under {base}/id/tracked-entity-type. The set of those values is the set of types this project's forms register into, and one of them is what /api/tracker/trackedEntities requires - the endpoint refuses a query naming neither a type nor a program (E1003).
  • That same form's questions ARE that type's attributes: one question per attribute, keyed by the attribute's DHIS2 UID, and a #choice question naming the ValueSet DHIS2's option set is published as. So the form answers two things nothing else does - which attributes each type collects, and which of them have a vocabulary a caller could enumerate - and those are exactly what the register declares as filterable (register.filtering). D2TEA_CS cannot answer either: it publishes every attribute the project's forms ask anywhere, without saying whose.
  • D2TET_CM maps each published type onto the FHIR resource type its registrations are served as: one row per type, the row's code the type UID, its display the name the instance holds, its target the resource. That map is the contract. [generate.tracked_entity_types] is what generated it, but a running facade never reads config to decide what a type is - it reads the artifact the guide published, so a served resource and a published subjectType cannot disagree. A published type the map says nothing about is a Patient, which is the same default the map itself was built from.
  • D2TEA_CS publishes one concept per tracked entity attribute the forms ask, carrying the DHIS2 code, a unique boolean, and a searchable boolean. Those two together are the search key set: uniqueness is what makes a value name a subject rather than describe one, and searchability is what DHIS2 itself declares a clerk may look someone up by. A woman whose first name is searchable but not unique is found by her first name in DHIS2, and a facade that keyed on uniqueness alone would refuse the one lookup the clinic actually performs.
  • The published registry names organisation units, and a registration form's title is the program's name, so an enrollment listing can say "Antenatal care at Ngelehun CHC" instead of two UIDs. Both are joins onto what the guide published, and both stay silent when it published nothing: a program outside this project's selection gets no name rather than a guessed one.

A project that publishes no registration form has no tracked entity type, and every lookup here answers empty. That is the honest reading of a guide with nobody in it, and the route says so.

ONE FACT HERE COMES FROM fhir.toml RATHER THAN FROM THE GUIDE. [ips.identity] nominates which tracked entity attribute carries a person's name, birth date, and sex, and the guide publishes that nomination nowhere: D2TEA_CS states what an attribute is called and what type its values are, and no artifact states what one means. So the nominations are read from the project here and checked against the vocabulary while the server starts - a nomination whose published value type cannot fill the element it was nominated for refuses the run, naming the key and the type it found (docs/fhir/design/ips.md section 4, "Value-shape validation"). The map from a sex value onto administrative-gender is published as a ConceptMap by d2w fhir generate, so a consumer audits the translation without holding this file, but what the server performs it reads from the file - the nominations the map depends on are not published, and half a dial read from an artifact and half from a file would be worse than either. [ips.sections] is read the same way and for the same reason, by dhis2w_fhir_serve.routes.summary rather than here: a section mapping says what a summary carries and nothing about the register.

Classes

NominatedAttributeError

Bases: ValueError

A [ips.identity] nomination the published vocabulary says cannot fill the element it names.

Raised while the index is built, so a run that would publish a birth date read off a phone number never opens its socket. It is the failure mode [generate.tracked_entity_types] has for a resource type that is not one, moved to the one check that needs the guide in hand.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
class NominatedAttributeError(ValueError):
    """A `[ips.identity]` nomination the published vocabulary says cannot fill the element it names.

    Raised while the index is built, so a run that would publish a birth date read off a phone number
    never opens its socket. It is the failure mode `[generate.tracked_entity_types]` has for a
    resource type that is not one, moved to the one check that needs the guide in hand.
    """

PublishedAttribute

Bases: BaseModel

One tracked entity attribute the guide publishes, and what it says about it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
class PublishedAttribute(BaseModel):
    """One tracked entity attribute the guide publishes, and what it says about it."""

    model_config = ConfigDict(frozen=True)

    attribute_uid: str
    display: str | None = None
    code: str | None = None
    unique: bool = False
    searchable: bool = False
    """Whether DHIS2 declares the attribute searchable in any context this guide publishes."""

    value_type: str | None = None
    """The DHIS2 value type `D2TEA_CS` publishes for the attribute, or None where it publishes none."""

    identifier_system: str
    """The system a value of this attribute is carried under when DHIS2 declares the attribute unique."""

    value_set: str | None = None
    """The ValueSet a registration form binds the attribute's answers to, where DHIS2 binds an option set.

    Read off the published form rather than off `D2TEA_CS`, because the option set is a binding
    rather than a property of the attribute: the vocabulary already rides the question as
    `answerValueSet`, and the concepts in it are already published as a CodeSystem beside it. A
    consumer offered the value filter reads this canonical to draw the values as a choice; an
    attribute nothing binds carries None, and its values are whatever text the instance holds.
    """

    def is_search_key(self) -> bool:
        """Whether a bare identifier value is looked for under this attribute by default."""
        return self.unique or self.searchable

    def can_hold(self, value: str) -> bool:
        """Whether the instance could hold one typed value under this attribute's declared value type.

        A search fans one `filter=<uid>:eq:<value>` out per key, and DHIS2 answers a 400 naming the
        value type for a key that could not hold the value at all - "the attribute value type is
        NUMBER but the value `mgc694579` is not". A key answering False here is left out of the
        fan-out, so a person's name is looked for under the keys that carry names rather than under
        the zip code as well.

        False is a certainty and never a guess. An attribute the guide publishes no value type for
        holds anything, as does every value type `_VALUE_SCREENS` does not screen, so the screen
        never drops a key DHIS2 would have answered.
        """
        screen = _VALUE_SCREENS.get(self.value_type or "")
        return True if screen is None else screen(value)
Attributes
searchable = False class-attribute instance-attribute

Whether DHIS2 declares the attribute searchable in any context this guide publishes.

value_type = None class-attribute instance-attribute

The DHIS2 value type D2TEA_CS publishes for the attribute, or None where it publishes none.

identifier_system instance-attribute

The system a value of this attribute is carried under when DHIS2 declares the attribute unique.

value_set = None class-attribute instance-attribute

The ValueSet a registration form binds the attribute's answers to, where DHIS2 binds an option set.

Read off the published form rather than off D2TEA_CS, because the option set is a binding rather than a property of the attribute: the vocabulary already rides the question as answerValueSet, and the concepts in it are already published as a CodeSystem beside it. A consumer offered the value filter reads this canonical to draw the values as a choice; an attribute nothing binds carries None, and its values are whatever text the instance holds.

Methods:
is_search_key()

Whether a bare identifier value is looked for under this attribute by default.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def is_search_key(self) -> bool:
    """Whether a bare identifier value is looked for under this attribute by default."""
    return self.unique or self.searchable
can_hold(value)

Whether the instance could hold one typed value under this attribute's declared value type.

A search fans one filter=<uid>:eq:<value> out per key, and DHIS2 answers a 400 naming the value type for a key that could not hold the value at all - "the attribute value type is NUMBER but the value mgc694579 is not". A key answering False here is left out of the fan-out, so a person's name is looked for under the keys that carry names rather than under the zip code as well.

False is a certainty and never a guess. An attribute the guide publishes no value type for holds anything, as does every value type _VALUE_SCREENS does not screen, so the screen never drops a key DHIS2 would have answered.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def can_hold(self, value: str) -> bool:
    """Whether the instance could hold one typed value under this attribute's declared value type.

    A search fans one `filter=<uid>:eq:<value>` out per key, and DHIS2 answers a 400 naming the
    value type for a key that could not hold the value at all - "the attribute value type is
    NUMBER but the value `mgc694579` is not". A key answering False here is left out of the
    fan-out, so a person's name is looked for under the keys that carry names rather than under
    the zip code as well.

    False is a certainty and never a guess. An attribute the guide publishes no value type for
    holds anything, as does every value type `_VALUE_SCREENS` does not screen, so the screen
    never drops a key DHIS2 would have answered.
    """
    screen = _VALUE_SCREENS.get(self.value_type or "")
    return True if screen is None else screen(value)

PublishedTrackedEntityType

Bases: BaseModel

One tracked entity type this guide registers, and the FHIR resource it publishes it as.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
class PublishedTrackedEntityType(BaseModel):
    """One tracked entity type this guide registers, and the FHIR resource it publishes it as."""

    model_config = ConfigDict(frozen=True)

    uid: str
    name: str | None = None
    """The name the instance holds for the type, as `D2TET_CM` published it, or None when it published none."""

    resource_type: str = DEFAULT_SUBJECT_RESOURCE_TYPE
    """The FHIR resource a tracked entity of this type is served as - what the published map states."""
Attributes
name = None class-attribute instance-attribute

The name the instance holds for the type, as D2TET_CM published it, or None when it published none.

resource_type = DEFAULT_SUBJECT_RESOURCE_TYPE class-attribute instance-attribute

The FHIR resource a tracked entity of this type is served as - what the published map states.

TrackedEntityIndex

Bases: BaseModel

The published facts a register lookup resolves against, built once from the store at startup.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
class TrackedEntityIndex(BaseModel):
    """The published facts a register lookup resolves against, built once from the store at startup."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_system: str
    """`{base}/id/tracked-entity` - the system whose value is the tracked entity UID itself."""

    tracked_entity_type_system: str
    attribute_value_extension_url: str
    identifier_system_base: str
    tracked_entity_types: tuple[PublishedTrackedEntityType, ...] = ()
    attributes: tuple[PublishedAttribute, ...] = ()
    tracked_entity_type_attribute_uids: dict[str, tuple[str, ...]] = Field(default_factory=dict)
    """Which tracked entity attributes each registered type's own forms ask, in the order they ask them.

    The join is one registration form's `linkId`s onto the type that form registers: a registration
    Questionnaire's questions ARE the type's attributes, one question per attribute, each keyed by
    the attribute's DHIS2 UID. Two forms registering one type - the program's and the type's own -
    contribute one union, because a person registered through either holds the same attributes.

    `attributes` alone cannot answer this: `D2TEA_CS` publishes every attribute the project's forms
    ask anywhere, so a register of specimens would otherwise declare a person's date of birth
    filterable. What a type collects is the form's to say.
    """

    program_names: dict[str, str] = Field(default_factory=dict)
    organisation_unit_names: dict[str, str] = Field(default_factory=dict)
    identity: IdentityNominations = Field(default_factory=IdentityNominations)
    """The `[ips.identity]` nominations this project states - the one fact here read from `fhir.toml`."""

    _attributes_by_uid: dict[str, PublishedAttribute] = PrivateAttr(default_factory=dict)
    _attributes_by_system: dict[str, PublishedAttribute] = PrivateAttr(default_factory=dict)
    _types_by_uid: dict[str, PublishedTrackedEntityType] = PrivateAttr(default_factory=dict)

    @classmethod
    def from_store(cls, project: FhirProject, store: ResourceStore) -> TrackedEntityIndex:
        """Read every join a register lookup needs out of one loaded store."""
        base = project.config.generate.identifier_system_base
        naming = QuestionnaireNaming.from_naming(project.config.generate.naming)
        tracked_entity_type_system = f"{base}/id/{TRACKED_ENTITY_TYPE_IDENTIFIER_SEGMENT}"
        program_system = f"{base}/id/{PROGRAM_IDENTIFIER_SEGMENT}"
        organisation_unit_system = f"{base}/id/{ORGANISATION_UNIT_IDENTIFIER_SEGMENT}"
        registered_type_uids: list[str] = []
        program_names: dict[str, str] = {}
        organisation_unit_names: dict[str, str] = {}
        attributes: tuple[PublishedAttribute, ...] = ()
        mapping: dict[str, PublishedTrackedEntityType] = {}
        asked: dict[str, dict[str, None]] = {}
        bound_value_sets: dict[str, str] = {}
        for entry in store.entries:
            if entry.resource_type == QUESTIONNAIRE_RESOURCE_TYPE:
                registered = _identifier_values(entry, tracked_entity_type_system)
                if not registered:
                    continue
                registered_type_uids.extend(registered)
                _record_asked_attributes(entry, registered, asked, bound_value_sets)
                _record_name(entry, program_system, program_names, "title")
            elif entry.resource_type == CODE_SYSTEM_RESOURCE_TYPE and (
                entry.resource_id == naming.tracked_entity_attribute_code_system_id
            ):
                attributes = _published_attributes(entry, base)
            elif entry.resource_type == CONCEPT_MAP_RESOURCE_TYPE and (
                entry.resource_id == naming.tracked_entity_type_resource_map_id
            ):
                mapping = _mapped_tracked_entity_types(entry)
            elif entry.resource_type in REGISTRY_RESOURCE_TYPES:
                _record_name(entry, organisation_unit_system, organisation_unit_names, "name")
        identity = project.config.ips.identity
        attributes = _bound(attributes, bound_value_sets)
        _refuse_unfillable_nominations(identity, attributes)
        return cls(
            tracked_entity_system=f"{base}/id/{TRACKED_ENTITY_IDENTIFIER_SEGMENT}",
            tracked_entity_type_system=tracked_entity_type_system,
            attribute_value_extension_url=tracked_entity_attribute_value_extension_url(
                project.config.generate, project.config.ig.canonical
            ),
            identifier_system_base=base,
            tracked_entity_types=_registered_types(registered_type_uids, mapping),
            attributes=attributes,
            tracked_entity_type_attribute_uids={
                type_uid: tuple(attribute_uids) for type_uid, attribute_uids in asked.items()
            },
            program_names=program_names,
            organisation_unit_names=organisation_unit_names,
            identity=identity,
        )

    def model_post_init(self, context: Any, /) -> None:
        """Key the published facts by UID and by identifier system (private attributes stay settable)."""
        for attribute in self.attributes:
            self._attributes_by_uid.setdefault(attribute.attribute_uid, attribute)
            self._attributes_by_system.setdefault(attribute.identifier_system, attribute)
        for published_type in self.tracked_entity_types:
            self._types_by_uid.setdefault(published_type.uid, published_type)

    def serves_tracked_entities(self) -> bool:
        """True when the guide published a tracked entity type, which is what a DHIS2 search requires."""
        return bool(self.tracked_entity_types)

    def tracked_entity_type_uids(self) -> tuple[str, ...]:
        """Every tracked entity type the published forms register, in the order they register them."""
        return tuple(published.uid for published in self.tracked_entity_types)

    def tracked_entity_type(self, tracked_entity_type_uid: str) -> PublishedTrackedEntityType | None:
        """What the guide publishes about one tracked entity type, or None when it publishes none."""
        return self._types_by_uid.get(tracked_entity_type_uid)

    def attribute(self, attribute_uid: str) -> PublishedAttribute | None:
        """What the guide publishes about one tracked entity attribute, or None when it publishes none."""
        return self._attributes_by_uid.get(attribute_uid)

    def attribute_for_system(self, system: str) -> PublishedAttribute | None:
        """The attribute whose values are carried under one identifier system, or None."""
        return self._attributes_by_system.get(system)

    def attributes_asked_of(self, tracked_entity_type_uid: str) -> tuple[PublishedAttribute, ...]:
        """The attributes one tracked entity type's registration forms ask, in the order they ask them.

        An attribute a form asks and `D2TEA_CS` publishes nothing about is still one of them, carrying
        the identifier system its UID gives it and nothing else - the same reading `_attributes_named`
        takes of an attribute an operator names, and for the same reason: the instance holds the
        values whether or not this project's vocabulary happened to describe them.
        """
        return tuple(
            self._attributes_by_uid.get(attribute_uid)
            or PublishedAttribute(
                attribute_uid=attribute_uid,
                identifier_system=tracked_entity_attribute_identifier_system(
                    self.identifier_system_base, attribute_uid
                ),
            )
            for attribute_uid in self.tracked_entity_type_attribute_uids.get(tracked_entity_type_uid, ())
        )

    def search_key_attributes(self) -> tuple[PublishedAttribute, ...]:
        """Every attribute DHIS2 declares unique or searchable - the set a bare identifier value is searched across."""
        return tuple(attribute for attribute in self.attributes if attribute.is_search_key())

    def program_name(self, program_uid: str) -> str | None:
        """The name this guide publishes one program under, or None when the program is outside its selection."""
        return self.program_names.get(program_uid)

    def program_uids(self) -> tuple[str, ...]:
        """Every program this guide publishes, in the order it published them.

        A sync's enrollment poll needs these because `/api/tracker/enrollments` will be scoped by
        program and by nothing else - it answers `E1003 "Program is mandatory"` to a query naming a
        tracked entity type (BUGS.md 102). So the programs the guide published are the scope an
        enrollment poll walks, where every other register read walks the types the register serves.
        """
        return tuple(self.program_names)

    def organisation_unit_name(self, organisation_unit_uid: str) -> str | None:
        """The name this guide publishes one organisation unit under, or None when the registry omits it."""
        return self.organisation_unit_names.get(organisation_unit_uid)
Attributes
tracked_entity_system instance-attribute

{base}/id/tracked-entity - the system whose value is the tracked entity UID itself.

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

Which tracked entity attributes each registered type's own forms ask, in the order they ask them.

The join is one registration form's linkIds onto the type that form registers: a registration Questionnaire's questions ARE the type's attributes, one question per attribute, each keyed by the attribute's DHIS2 UID. Two forms registering one type - the program's and the type's own - contribute one union, because a person registered through either holds the same attributes.

attributes alone cannot answer this: D2TEA_CS publishes every attribute the project's forms ask anywhere, so a register of specimens would otherwise declare a person's date of birth filterable. What a type collects is the form's to say.

identity = Field(default_factory=IdentityNominations) class-attribute instance-attribute

The [ips.identity] nominations this project states - the one fact here read from fhir.toml.

Methods:
from_store(project, store) classmethod

Read every join a register lookup needs out of one loaded store.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
@classmethod
def from_store(cls, project: FhirProject, store: ResourceStore) -> TrackedEntityIndex:
    """Read every join a register lookup needs out of one loaded store."""
    base = project.config.generate.identifier_system_base
    naming = QuestionnaireNaming.from_naming(project.config.generate.naming)
    tracked_entity_type_system = f"{base}/id/{TRACKED_ENTITY_TYPE_IDENTIFIER_SEGMENT}"
    program_system = f"{base}/id/{PROGRAM_IDENTIFIER_SEGMENT}"
    organisation_unit_system = f"{base}/id/{ORGANISATION_UNIT_IDENTIFIER_SEGMENT}"
    registered_type_uids: list[str] = []
    program_names: dict[str, str] = {}
    organisation_unit_names: dict[str, str] = {}
    attributes: tuple[PublishedAttribute, ...] = ()
    mapping: dict[str, PublishedTrackedEntityType] = {}
    asked: dict[str, dict[str, None]] = {}
    bound_value_sets: dict[str, str] = {}
    for entry in store.entries:
        if entry.resource_type == QUESTIONNAIRE_RESOURCE_TYPE:
            registered = _identifier_values(entry, tracked_entity_type_system)
            if not registered:
                continue
            registered_type_uids.extend(registered)
            _record_asked_attributes(entry, registered, asked, bound_value_sets)
            _record_name(entry, program_system, program_names, "title")
        elif entry.resource_type == CODE_SYSTEM_RESOURCE_TYPE and (
            entry.resource_id == naming.tracked_entity_attribute_code_system_id
        ):
            attributes = _published_attributes(entry, base)
        elif entry.resource_type == CONCEPT_MAP_RESOURCE_TYPE and (
            entry.resource_id == naming.tracked_entity_type_resource_map_id
        ):
            mapping = _mapped_tracked_entity_types(entry)
        elif entry.resource_type in REGISTRY_RESOURCE_TYPES:
            _record_name(entry, organisation_unit_system, organisation_unit_names, "name")
    identity = project.config.ips.identity
    attributes = _bound(attributes, bound_value_sets)
    _refuse_unfillable_nominations(identity, attributes)
    return cls(
        tracked_entity_system=f"{base}/id/{TRACKED_ENTITY_IDENTIFIER_SEGMENT}",
        tracked_entity_type_system=tracked_entity_type_system,
        attribute_value_extension_url=tracked_entity_attribute_value_extension_url(
            project.config.generate, project.config.ig.canonical
        ),
        identifier_system_base=base,
        tracked_entity_types=_registered_types(registered_type_uids, mapping),
        attributes=attributes,
        tracked_entity_type_attribute_uids={
            type_uid: tuple(attribute_uids) for type_uid, attribute_uids in asked.items()
        },
        program_names=program_names,
        organisation_unit_names=organisation_unit_names,
        identity=identity,
    )
model_post_init(context)

Key the published facts by UID and by identifier system (private attributes stay settable).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def model_post_init(self, context: Any, /) -> None:
    """Key the published facts by UID and by identifier system (private attributes stay settable)."""
    for attribute in self.attributes:
        self._attributes_by_uid.setdefault(attribute.attribute_uid, attribute)
        self._attributes_by_system.setdefault(attribute.identifier_system, attribute)
    for published_type in self.tracked_entity_types:
        self._types_by_uid.setdefault(published_type.uid, published_type)
serves_tracked_entities()

True when the guide published a tracked entity type, which is what a DHIS2 search requires.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def serves_tracked_entities(self) -> bool:
    """True when the guide published a tracked entity type, which is what a DHIS2 search requires."""
    return bool(self.tracked_entity_types)
tracked_entity_type_uids()

Every tracked entity type the published forms register, in the order they register them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def tracked_entity_type_uids(self) -> tuple[str, ...]:
    """Every tracked entity type the published forms register, in the order they register them."""
    return tuple(published.uid for published in self.tracked_entity_types)
tracked_entity_type(tracked_entity_type_uid)

What the guide publishes about one tracked entity type, or None when it publishes none.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def tracked_entity_type(self, tracked_entity_type_uid: str) -> PublishedTrackedEntityType | None:
    """What the guide publishes about one tracked entity type, or None when it publishes none."""
    return self._types_by_uid.get(tracked_entity_type_uid)
attribute(attribute_uid)

What the guide publishes about one tracked entity attribute, or None when it publishes none.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def attribute(self, attribute_uid: str) -> PublishedAttribute | None:
    """What the guide publishes about one tracked entity attribute, or None when it publishes none."""
    return self._attributes_by_uid.get(attribute_uid)
attribute_for_system(system)

The attribute whose values are carried under one identifier system, or None.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def attribute_for_system(self, system: str) -> PublishedAttribute | None:
    """The attribute whose values are carried under one identifier system, or None."""
    return self._attributes_by_system.get(system)
attributes_asked_of(tracked_entity_type_uid)

The attributes one tracked entity type's registration forms ask, in the order they ask them.

An attribute a form asks and D2TEA_CS publishes nothing about is still one of them, carrying the identifier system its UID gives it and nothing else - the same reading _attributes_named takes of an attribute an operator names, and for the same reason: the instance holds the values whether or not this project's vocabulary happened to describe them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def attributes_asked_of(self, tracked_entity_type_uid: str) -> tuple[PublishedAttribute, ...]:
    """The attributes one tracked entity type's registration forms ask, in the order they ask them.

    An attribute a form asks and `D2TEA_CS` publishes nothing about is still one of them, carrying
    the identifier system its UID gives it and nothing else - the same reading `_attributes_named`
    takes of an attribute an operator names, and for the same reason: the instance holds the
    values whether or not this project's vocabulary happened to describe them.
    """
    return tuple(
        self._attributes_by_uid.get(attribute_uid)
        or PublishedAttribute(
            attribute_uid=attribute_uid,
            identifier_system=tracked_entity_attribute_identifier_system(
                self.identifier_system_base, attribute_uid
            ),
        )
        for attribute_uid in self.tracked_entity_type_attribute_uids.get(tracked_entity_type_uid, ())
    )
search_key_attributes()

Every attribute DHIS2 declares unique or searchable - the set a bare identifier value is searched across.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def search_key_attributes(self) -> tuple[PublishedAttribute, ...]:
    """Every attribute DHIS2 declares unique or searchable - the set a bare identifier value is searched across."""
    return tuple(attribute for attribute in self.attributes if attribute.is_search_key())
program_name(program_uid)

The name this guide publishes one program under, or None when the program is outside its selection.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def program_name(self, program_uid: str) -> str | None:
    """The name this guide publishes one program under, or None when the program is outside its selection."""
    return self.program_names.get(program_uid)
program_uids()

Every program this guide publishes, in the order it published them.

A sync's enrollment poll needs these because /api/tracker/enrollments will be scoped by program and by nothing else - it answers E1003 "Program is mandatory" to a query naming a tracked entity type (BUGS.md 102). So the programs the guide published are the scope an enrollment poll walks, where every other register read walks the types the register serves.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def program_uids(self) -> tuple[str, ...]:
    """Every program this guide publishes, in the order it published them.

    A sync's enrollment poll needs these because `/api/tracker/enrollments` will be scoped by
    program and by nothing else - it answers `E1003 "Program is mandatory"` to a query naming a
    tracked entity type (BUGS.md 102). So the programs the guide published are the scope an
    enrollment poll walks, where every other register read walks the types the register serves.
    """
    return tuple(self.program_names)
organisation_unit_name(organisation_unit_uid)

The name this guide publishes one organisation unit under, or None when the registry omits it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/index.py
def organisation_unit_name(self, organisation_unit_uid: str) -> str | None:
    """The name this guide publishes one organisation unit under, or None when the registry omits it."""
    return self.organisation_unit_names.get(organisation_unit_uid)

surface

What this run serves: what the guide published, narrowed by what [serve.tracked_entities] says.

Two sources decide the register, and they answer different questions. TrackedEntityIndex reads the published guide and answers "what does this project know about the instance's subjects" - which tracked entity types its forms register, which FHIR resource each of them is published as, which attributes DHIS2 declares unique or searchable, what each identifier system means. [serve.tracked_entities] is the operator's own statement and answers "what may this process say about them" - whether the register exists at all, whether it lists, and which of the published types and attributes are in scope.

The narrowing rules, each stated once here so no route re-derives them:

  • The types. Empty config means the types the published forms register. A stated list is used verbatim, for both search and listing - it is a restriction the operator wrote down, and intersecting it with the published set would silently serve nothing when a project's forms and its [serve.tracked_entities] disagree, which is a config error worth surfacing as an empty answer to a named type rather than as a mystery. A stated type the guide never published is still served, as the Patient every unmapped type is.
  • The search keys. Empty config means the attributes DHIS2 declares unique or searchable. Uniqueness makes a value name a subject; searchability is DHIS2's own statement that a clerk looks people up by it, and a clinic finding a woman by her first name is doing the ordinary thing rather than the exceptional one. A stated list is the key set outright, unique or searchable or neither: an instance that enforces nothing on the number a district actually files people under still has one, and the operator naming it has said so.

What the surface deliberately does not touch is the projection. A served resource still carries as identifier[] exactly the values of the attributes DHIS2 declares unique, whatever this table names or this server searches, because that element states what the instance enforces and not what this process looks under.

Resources come from the published map, never from config. register_resource_types is the set of FHIR resource types D2TET_CM takes the in-scope types onto, and each resource is served over its own types alone: a GET /Specimen searches the types published as Specimen and no others, so a sample never comes back as a person.

Classes

ServedRegister

Bases: BaseModel

One FHIR resource type this run serves, the tracked entity types that ride it, and what filters it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
class ServedRegister(BaseModel):
    """One FHIR resource type this run serves, the tracked entity types that ride it, and what filters it."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    tracked_entity_types: tuple[PublishedTrackedEntityType, ...] = ()

    filter_attributes: tuple[PublishedAttribute, ...] = ()
    """The attributes `d2-attribute` filters this register by - what its types' own forms ask.

    The union over the register's types, in the order the types ride it, each attribute once: two
    types published as one resource are one register, and an attribute both of them collect is one
    filter. It is read off the guide and narrowed by nothing - `register.filtering` argues why there
    is no config dial here where `search_attributes` has one.
    """

    filter_attribute_type_uids: dict[str, tuple[str, ...]] = Field(default_factory=dict)
    """Which of this register's tracked entity types declare each filter attribute, keyed by attribute UID.

    The union above says what the register filters on; this says whose. A register of people carrying
    two types - a person and a focus area - filters on what either of them collects, and a screen
    narrowed to the focus area must offer the focus area's own attributes rather than a person's
    first name. The values are type UIDs in the order the types ride the register, and every
    attribute in `filter_attributes` has an entry: the union is built from these very declarations.
    """
Attributes
filter_attributes = () class-attribute instance-attribute

The attributes d2-attribute filters this register by - what its types' own forms ask.

The union over the register's types, in the order the types ride it, each attribute once: two types published as one resource are one register, and an attribute both of them collect is one filter. It is read off the guide and narrowed by nothing - register.filtering argues why there is no config dial here where search_attributes has one.

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

Which of this register's tracked entity types declare each filter attribute, keyed by attribute UID.

The union above says what the register filters on; this says whose. A register of people carrying two types - a person and a focus area - filters on what either of them collects, and a screen narrowed to the focus area must offer the focus area's own attributes rather than a person's first name. The values are type UIDs in the order the types ride the register, and every attribute in filter_attributes has an entry: the union is built from these very declarations.

RegisterSurface

Bases: BaseModel

What this process answers for: the published index, narrowed by [serve.tracked_entities].

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
class RegisterSurface(BaseModel):
    """What this process answers for: the published index, narrowed by `[serve.tracked_entities]`."""

    model_config = ConfigDict(frozen=True)

    index: TrackedEntityIndex
    tracked_entities: TrackedEntitiesConfig
    served_types: tuple[PublishedTrackedEntityType, ...] = ()
    """The types a search and a listing run over - the published ones, or the ones the table names."""

    search_attributes: tuple[PublishedAttribute, ...] = ()
    """The attributes a bare identifier value is searched across, and whose systems name a key."""

    @classmethod
    def resolve(cls, index: TrackedEntityIndex, tracked_entities: TrackedEntitiesConfig) -> RegisterSurface:
        """Narrow one published index by one `[serve.tracked_entities]` table, once, at startup."""
        stated_types = tuple(tracked_entities.tracked_entity_types)
        stated_attributes = tuple(tracked_entities.search_attributes)
        keys = _attributes_named(index, stated_attributes) if stated_attributes else index.search_key_attributes()
        return cls(
            index=index,
            tracked_entities=tracked_entities,
            served_types=_types_named(index, stated_types) if stated_types else index.tracked_entity_types,
            search_attributes=tuple(keys),
        )

    def serves_tracked_entities(self) -> bool:
        """True when this process answers about the instance at all: the table allows it, and a type is in scope."""
        return self.tracked_entities.enabled and bool(self.served_types)

    def serves_listing(self) -> bool:
        """True when a search naming no identifier is answered with a page rather than a refusal."""
        return self.serves_tracked_entities() and self.tracked_entities.listing

    def serves_events(self) -> bool:
        """True when one entity's own record is answered here, as well as its identity."""
        return self.serves_tracked_entities() and self.tracked_entities.events

    def registers(self) -> tuple[ServedRegister, ...]:
        """One entry per FHIR resource type this register serves, in the order its types are registered."""
        grouped: dict[str, list[PublishedTrackedEntityType]] = {}
        for served in self.served_types:
            grouped.setdefault(served.resource_type, []).append(served)
        return tuple(
            ServedRegister(
                resource_type=resource_type,
                tracked_entity_types=tuple(types),
                filter_attributes=self._filter_attributes(types),
                filter_attribute_type_uids=self._filter_attribute_type_uids(types),
            )
            for resource_type, types in grouped.items()
        )

    def register_resource_types(self) -> tuple[str, ...]:
        """Every resource type a register route answers on, which is what a read of that type dispatches to.

        A register with no type in scope still claims the default resource. Nothing is served under
        it, but the refusal a client reads then names the register and the line that switched it off,
        rather than saying this server does not serve `Patient` at all - which would be a different
        and less useful fact about a facade whose whole purpose is to serve people.
        """
        resource_types = tuple(register.resource_type for register in self.registers())
        return resource_types or (DEFAULT_SUBJECT_RESOURCE_TYPE,)

    def tracked_entity_type_uids_for(self, resource_type: str) -> tuple[str, ...]:
        """The types one resource is served over, in the order a listing pages through them."""
        return tuple(served.uid for served in self.served_types if served.resource_type == resource_type)

    def attribute_for_system(self, system: str) -> PublishedAttribute | None:
        """The search key one system names, or None when this surface holds no key under it."""
        for attribute in self.search_attributes:
            if attribute.identifier_system == system:
                return attribute
        return None

    def filter_attributes_for(self, resource_type: str) -> tuple[PublishedAttribute, ...]:
        """The attributes one register is filtered by, empty where this run serves no such register."""
        for register in self.registers():
            if register.resource_type == resource_type:
                return register.filter_attributes
        return ()

    def _filter_attributes(self, types: list[PublishedTrackedEntityType]) -> tuple[PublishedAttribute, ...]:
        """The attributes one resource's types collect between them, each once, in the order they ride it."""
        found: dict[str, PublishedAttribute] = {}
        for served in types:
            for attribute in self.index.attributes_asked_of(served.uid):
                found.setdefault(attribute.attribute_uid, attribute)
        return tuple(found.values())

    def _filter_attribute_type_uids(self, types: list[PublishedTrackedEntityType]) -> dict[str, tuple[str, ...]]:
        """Which of one resource's types declare each attribute it filters on, in the order they ride it.

        The same walk `_filter_attributes` takes, keeping the type rather than dropping it: a type
        whose form asks an attribute is a type that attribute may be offered under, and one whose
        form does not is not.
        """
        declared: dict[str, list[str]] = {}
        for served in types:
            for attribute in self.index.attributes_asked_of(served.uid):
                declaring = declared.setdefault(attribute.attribute_uid, [])
                if served.uid not in declaring:
                    declaring.append(served.uid)
        return {attribute_uid: tuple(declaring) for attribute_uid, declaring in declared.items()}
Attributes
served_types = () class-attribute instance-attribute

The types a search and a listing run over - the published ones, or the ones the table names.

search_attributes = () class-attribute instance-attribute

The attributes a bare identifier value is searched across, and whose systems name a key.

Methods:
resolve(index, tracked_entities) classmethod

Narrow one published index by one [serve.tracked_entities] table, once, at startup.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
@classmethod
def resolve(cls, index: TrackedEntityIndex, tracked_entities: TrackedEntitiesConfig) -> RegisterSurface:
    """Narrow one published index by one `[serve.tracked_entities]` table, once, at startup."""
    stated_types = tuple(tracked_entities.tracked_entity_types)
    stated_attributes = tuple(tracked_entities.search_attributes)
    keys = _attributes_named(index, stated_attributes) if stated_attributes else index.search_key_attributes()
    return cls(
        index=index,
        tracked_entities=tracked_entities,
        served_types=_types_named(index, stated_types) if stated_types else index.tracked_entity_types,
        search_attributes=tuple(keys),
    )
serves_tracked_entities()

True when this process answers about the instance at all: the table allows it, and a type is in scope.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def serves_tracked_entities(self) -> bool:
    """True when this process answers about the instance at all: the table allows it, and a type is in scope."""
    return self.tracked_entities.enabled and bool(self.served_types)
serves_listing()

True when a search naming no identifier is answered with a page rather than a refusal.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def serves_listing(self) -> bool:
    """True when a search naming no identifier is answered with a page rather than a refusal."""
    return self.serves_tracked_entities() and self.tracked_entities.listing
serves_events()

True when one entity's own record is answered here, as well as its identity.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def serves_events(self) -> bool:
    """True when one entity's own record is answered here, as well as its identity."""
    return self.serves_tracked_entities() and self.tracked_entities.events
registers()

One entry per FHIR resource type this register serves, in the order its types are registered.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def registers(self) -> tuple[ServedRegister, ...]:
    """One entry per FHIR resource type this register serves, in the order its types are registered."""
    grouped: dict[str, list[PublishedTrackedEntityType]] = {}
    for served in self.served_types:
        grouped.setdefault(served.resource_type, []).append(served)
    return tuple(
        ServedRegister(
            resource_type=resource_type,
            tracked_entity_types=tuple(types),
            filter_attributes=self._filter_attributes(types),
            filter_attribute_type_uids=self._filter_attribute_type_uids(types),
        )
        for resource_type, types in grouped.items()
    )
register_resource_types()

Every resource type a register route answers on, which is what a read of that type dispatches to.

A register with no type in scope still claims the default resource. Nothing is served under it, but the refusal a client reads then names the register and the line that switched it off, rather than saying this server does not serve Patient at all - which would be a different and less useful fact about a facade whose whole purpose is to serve people.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def register_resource_types(self) -> tuple[str, ...]:
    """Every resource type a register route answers on, which is what a read of that type dispatches to.

    A register with no type in scope still claims the default resource. Nothing is served under
    it, but the refusal a client reads then names the register and the line that switched it off,
    rather than saying this server does not serve `Patient` at all - which would be a different
    and less useful fact about a facade whose whole purpose is to serve people.
    """
    resource_types = tuple(register.resource_type for register in self.registers())
    return resource_types or (DEFAULT_SUBJECT_RESOURCE_TYPE,)
tracked_entity_type_uids_for(resource_type)

The types one resource is served over, in the order a listing pages through them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def tracked_entity_type_uids_for(self, resource_type: str) -> tuple[str, ...]:
    """The types one resource is served over, in the order a listing pages through them."""
    return tuple(served.uid for served in self.served_types if served.resource_type == resource_type)
attribute_for_system(system)

The search key one system names, or None when this surface holds no key under it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def attribute_for_system(self, system: str) -> PublishedAttribute | None:
    """The search key one system names, or None when this surface holds no key under it."""
    for attribute in self.search_attributes:
        if attribute.identifier_system == system:
            return attribute
    return None
filter_attributes_for(resource_type)

The attributes one register is filtered by, empty where this run serves no such register.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/surface.py
def filter_attributes_for(self, resource_type: str) -> tuple[PublishedAttribute, ...]:
    """The attributes one register is filtered by, empty where this run serves no such register."""
    for register in self.registers():
        if register.resource_type == resource_type:
            return register.filter_attributes
    return ()

wire

The DHIS2 reads behind a register lookup, and the contract /api/tracker/trackedEntities actually holds to.

Five facts decide every request here, the first four established against a running instance and recorded in the repository's BUGS.md:

  1. A search names a tracked entity type or a program, or it is refused with E1003. The type comes from the published forms (dhis2w_fhir_serve.register.index); a program is never named, for the reason in point 3.
  2. A unique attribute gets no org-unit-scope exemption (BUGS.md 74). The legacy documentation describes a unique value as an instance-wide key; the tracker endpoint scopes it like any other filter, so a lookup scoped to the capture unit misses exactly the entities identifier search exists to find. Every search here therefore sends orgUnitMode=ACCESSIBLE: as wide as the requesting user may see, and no wider.
  3. An entity-scoped read with a program the entity is not enrolled in answers 404 E1005, claiming the tracked entity does not exist (BUGS.md 72). So nothing here ever probes by program. The enrollments are read off the entity itself, without program=, and inspected here.
  4. The default projection omits the enrollments entirely, and folds no program-level attribute value into the entity's own attributes. Both are field-selection defaults rather than statements about the person, so every read names its fields explicitly - including enrollments[...] and the attribute values those enrollments carry, without which a person found by a program attribute's unique value would come back not carrying it.
  5. A page states how many pages there are only when it is asked to. page and pageSize come back on every request; total and pageCount come back only under totalPages=true, on 2.42 and 2.43 alike. The listing asks for it, because a searchset that states a total is worth one count of a table DHIS2 has indexed; a lookup does not, because nobody asks an identifier search how many tracked entities the instance holds.

WHAT reader IS HERE IS THE POINT OF THE PARAMETER. Every read below takes a RegisterReader - one raw GET answering parsed JSON - and never a Dhis2Client, because whose credentials a register read runs under is settled per request rather than per process. Under [serve] auth = "dhis2" it is the caller's own header on the process's credential-free pool; under none and token it is the runtime's client, which Dhis2Client satisfies as it stands. dhis2w_fhir_serve.passthrough states which is which, and nothing in this module branches on the answer.

Classes

TrackedEntitiesPager

Bases: BaseModel

Where one tracker page sits in its result set, as totalPages=true states it.

total and pageCount are absent unless that parameter was passed, so both are optional here rather than defaulted to zero: "the instance did not say" and "the instance said none" are different answers, and a Bundle states a total only for the first.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
class TrackedEntitiesPager(BaseModel):
    """Where one tracker page sits in its result set, as `totalPages=true` states it.

    `total` and `pageCount` are absent unless that parameter was passed, so both are optional here
    rather than defaulted to zero: "the instance did not say" and "the instance said none" are
    different answers, and a Bundle states a total only for the first.
    """

    model_config = ConfigDict(extra="allow")

    page: int | None = None
    pageSize: int | None = None
    total: int | None = None
    pageCount: int | None = None

TrackedEntitiesPage

Bases: BaseModel

One page of /api/tracker/trackedEntities: the entities on it, and where it sits.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
class TrackedEntitiesPage(BaseModel):
    """One page of `/api/tracker/trackedEntities`: the entities on it, and where it sits."""

    model_config = ConfigDict(extra="allow")

    trackedEntities: list[TrackerTrackedEntity] = []
    pager: TrackedEntitiesPager | None = None
    tombstones_visible: bool = True
    """False when the instance refused the read with `includeDeleted=true` and the page was read
    without it (DHIS2 2.42.6, BUGS.md #116): a deleted entity is then absent from this page rather
    than present as a tombstone."""
Attributes
tombstones_visible = True class-attribute instance-attribute

False when the instance refused the read with includeDeleted=true and the page was read without it (DHIS2 2.42.6, BUGS.md #116): a deleted entity is then absent from this page rather than present as a tombstone.

TouchedRow

Bases: BaseModel

One row of an enrollment poll: whose projection it touches, when, and whether it is a tombstone.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
class TouchedRow(BaseModel):
    """One row of an enrollment poll: whose projection it touches, when, and whether it is a tombstone."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str | None = None
    updated_at: datetime | None = None
    deleted: bool = False

TouchedPage

Bases: BaseModel

One page of an enrollment poll, and how many pages the instance said there are.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
class TouchedPage(BaseModel):
    """One page of an enrollment poll, and how many pages the instance said there are."""

    model_config = ConfigDict(frozen=True)

    rows: tuple[TouchedRow, ...] = ()
    page_count: int | None = None

EnrollmentsPage

Bases: BaseModel

One page of /api/tracker/enrollments: the enrollments on it, and where it sits.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
class EnrollmentsPage(BaseModel):
    """One page of `/api/tracker/enrollments`: the enrollments on it, and where it sits."""

    model_config = ConfigDict(extra="allow")

    enrollments: list[TrackerEnrollment] = []
    pager: TrackedEntitiesPager | None = None

Functions:

upstream_refusal_text(error)

What DHIS2 said, for an operator-facing diagnostic - the body's message when the status line has none.

DHIS2 sends an empty HTTP reason phrase, so str(error) can end at a bare colon while the refusal's actual cause sits in the JSON body; a diagnostic that names no cause is worse than the refusal itself. A transport error keeps its own text.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
def upstream_refusal_text(error: Exception) -> str:
    """What DHIS2 said, for an operator-facing diagnostic - the body's message when the status line has none.

    DHIS2 sends an empty HTTP reason phrase, so `str(error)` can end at a bare colon while the
    refusal's actual cause sits in the JSON body; a diagnostic that names no cause is worse than
    the refusal itself. A transport error keeps its own text.
    """
    text = str(error)
    if not isinstance(error, Dhis2ApiError) or error.message:
        return text
    body = error.body if isinstance(error.body, dict) else {}
    body_message = str(body.get("message", "")).strip()
    return f"{text}{body_message}" if body_message else text

list_tracked_entities(reader, *, tracked_entity_type_uid, page, page_size, filters=()) async

Read one page of one tracked entity type, counted so the page can say how many.

filters is the value filter a request narrowed the register by, one filter= expression apiece, ANDed by the endpoint; a request naming none sends none, and this is the whole listing. It goes on the wire rather than thinning the page afterwards because the pager counts what the query selected: a page narrowed after DHIS2 counted it would be a short page beside a total describing everybody.

orgUnitMode=ACCESSIBLE for the same reason every search here sends it (BUGS.md 74) - the register a user may see is the register they are shown, and a listing scoped to the capture unit would answer a fraction of it without saying so. A type the instance does not hold answers an empty page, not a refusal.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def list_tracked_entities(
    reader: RegisterReader,
    *,
    tracked_entity_type_uid: str,
    page: int,
    page_size: int,
    filters: Sequence[str] = (),
) -> TrackedEntitiesPage:
    """Read one page of one tracked entity type, counted so the page can say how many.

    `filters` is the value filter a request narrowed the register by, one `filter=` expression apiece,
    ANDed by the endpoint; a request naming none sends none, and this is the whole listing. It goes on
    the wire rather than thinning the page afterwards because the pager counts what the query
    selected: a page narrowed after DHIS2 counted it would be a short page beside a total describing
    everybody.

    `orgUnitMode=ACCESSIBLE` for the same reason every search here sends it (BUGS.md 74) - the register a
    user may see is the register they are shown, and a listing scoped to the capture unit would
    answer a fraction of it without saying so. A type the instance does not hold answers an empty
    page, not a refusal.
    """
    try:
        raw = await reader.get_raw(
            TRACKED_ENTITIES_PATH,
            params={
                "trackedEntityType": tracked_entity_type_uid,
                "orgUnitMode": SEARCH_ORG_UNIT_MODE,
                "fields": TRACKED_ENTITY_FIELDS,
                "page": page,
                "pageSize": page_size,
                TOTAL_PAGES_PARAMETER: "true",
                **_filter_parameter(filters),
            },
        )
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return TrackedEntitiesPage()
        raise
    return TrackedEntitiesPage.model_validate(raw)

count_tracked_entity_pages(reader, *, tracked_entity_type_uid, page_size, filters=()) async

How many pages of one type there are at one page size, asked without carrying anybody back.

The listing needs this at one place only: a previous link crossing back over a type boundary lands on the last page of the type before it, and the last page is a number nothing on the current page states. The projection is the UID alone, because the answer read is the pager, and filters rides it because the last page of a filtered walk is the last page of the filter rather than of the type. A type the instance does not hold counts zero pages.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def count_tracked_entity_pages(
    reader: RegisterReader, *, tracked_entity_type_uid: str, page_size: int, filters: Sequence[str] = ()
) -> int:
    """How many pages of one type there are at one page size, asked without carrying anybody back.

    The listing needs this at one place only: a `previous` link crossing back over a type boundary
    lands on the last page of the type before it, and the last page is a number nothing on the
    current page states. The projection is the UID alone, because the answer read is the pager, and
    `filters` rides it because the last page of a filtered walk is the last page of the filter rather
    than of the type. A type the instance does not hold counts zero pages.
    """
    try:
        raw = await reader.get_raw(
            TRACKED_ENTITIES_PATH,
            params={
                "trackedEntityType": tracked_entity_type_uid,
                "orgUnitMode": SEARCH_ORG_UNIT_MODE,
                "fields": _COUNT_ONLY_FIELDS,
                "page": 1,
                "pageSize": page_size,
                TOTAL_PAGES_PARAMETER: "true",
                **_filter_parameter(filters),
            },
        )
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return 0
        raise
    pager = TrackedEntitiesPage.model_validate(raw).pager
    return 0 if pager is None or pager.pageCount is None else pager.pageCount

count_tracked_entities(reader, *, tracked_entity_type_uid, filters=()) async

How many entities one tracked entity type holds, asked without carrying any of them back.

The listing needs this when several types are in scope: DHIS2 counts one type at a time, so the searchset's total is the sum of one count per type, and a count is what this asks for - the UID projection at a page size of one, reading the pager and dropping the page. filters narrows the count exactly as it narrows the page, which is what makes _count=0 beside a value filter answer how many of the register hold that value. None means the instance stated no total, which is a different answer from a total of zero and stays different all the way to the Bundle. A type the instance does not hold holds nothing, so it counts zero.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def count_tracked_entities(
    reader: RegisterReader, *, tracked_entity_type_uid: str, filters: Sequence[str] = ()
) -> int | None:
    """How many entities one tracked entity type holds, asked without carrying any of them back.

    The listing needs this when several types are in scope: DHIS2 counts one type at a time, so the
    searchset's total is the sum of one count per type, and a count is what this asks for - the UID
    projection at a page size of one, reading the pager and dropping the page. `filters` narrows the
    count exactly as it narrows the page, which is what makes `_count=0` beside a value filter answer
    how many of the register hold that value. None means the instance stated no total, which is a
    different answer from a total of zero and stays different all the way to the Bundle. A type the
    instance does not hold holds nothing, so it counts zero.
    """
    try:
        raw = await reader.get_raw(
            TRACKED_ENTITIES_PATH,
            params={
                "trackedEntityType": tracked_entity_type_uid,
                "orgUnitMode": SEARCH_ORG_UNIT_MODE,
                "fields": _COUNT_ONLY_FIELDS,
                "page": 1,
                "pageSize": _COUNT_ONLY_PAGE_SIZE,
                TOTAL_PAGES_PARAMETER: "true",
                **_filter_parameter(filters),
            },
        )
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return 0
        raise
    pager = TrackedEntitiesPage.model_validate(raw).pager
    return None if pager is None else pager.total

search_tracked_entities(reader, *, tracked_entity_type_uid, attribute_uid, value) async

Find every tracked entity of one type whose attribute holds one exact value.

A type the instance does not hold matches nothing, not a refusal.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def search_tracked_entities(
    reader: RegisterReader,
    *,
    tracked_entity_type_uid: str,
    attribute_uid: str,
    value: str,
) -> list[TrackerTrackedEntity]:
    """Find every tracked entity of one type whose attribute holds one exact value.

    A type the instance does not hold matches nothing, not a refusal.
    """
    try:
        raw = await reader.get_raw(
            TRACKED_ENTITIES_PATH,
            params={
                "trackedEntityType": tracked_entity_type_uid,
                "filter": f"{attribute_uid}:eq:{value}",
                "orgUnitMode": SEARCH_ORG_UNIT_MODE,
                "fields": TRACKED_ENTITY_FIELDS,
                "pageSize": SEARCH_PAGE_SIZE,
            },
        )
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return []
        raise
    return TrackedEntitiesPage.model_validate(raw).trackedEntities

poll_tracked_entities(reader, *, tracked_entity_type_uid, updated_after, page, page_size=POLL_PAGE_SIZE) async

Read one page of one tracked entity type as a sync polls it - tombstones included, always.

includeDeleted=true rides every call and is not a parameter: without it a deleted entity is invisible to a cursor poll and nothing says so (section 3.4, finding 2). updated_after None is the initial full materialization, which reads the type whole.

A type the instance does not hold answers an empty page, not a refusal - the same fold every other read here applies, because a mistyped UID in [serve.tracked_entities] should show up as a surface that finds nothing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def poll_tracked_entities(
    reader: RegisterReader,
    *,
    tracked_entity_type_uid: str,
    updated_after: datetime | None,
    page: int,
    page_size: int = POLL_PAGE_SIZE,
) -> TrackedEntitiesPage:
    """Read one page of one tracked entity type as a sync polls it - tombstones included, always.

    `includeDeleted=true` rides every call and is not a parameter: without it a deleted entity is
    invisible to a cursor poll and nothing says so (section 3.4, finding 2). `updated_after` None is
    the initial full materialization, which reads the type whole.

    A type the instance does not hold answers an empty page, not a refusal - the same fold every
    other read here applies, because a mistyped UID in `[serve.tracked_entities]` should show up as a
    surface that finds nothing.
    """
    params: dict[str, Any] = {
        "trackedEntityType": tracked_entity_type_uid,
        "orgUnitMode": SEARCH_ORG_UNIT_MODE,
        "fields": POLLED_TRACKED_ENTITY_FIELDS,
        "order": POLL_ORDER,
        "page": page,
        "pageSize": page_size,
        INCLUDE_DELETED_PARAMETER: "true",
        TOTAL_PAGES_PARAMETER: "true",
    }
    if updated_after is not None:
        params[UPDATED_AFTER_PARAMETER] = dhis2_instant(updated_after)
    try:
        raw = await reader.get_raw(TRACKED_ENTITIES_PATH, params=params)
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return TrackedEntitiesPage()
        if not _is_tombstone_read_syntax_refusal(error):
            raise
        # BUGS.md #116: the instance cannot read a type with its tombstones. Read the page without
        # them and say so on the page, so the run reports that removals were not learned.
        without_tombstones = {key: value for key, value in params.items() if key != INCLUDE_DELETED_PARAMETER}
        raw = await reader.get_raw(TRACKED_ENTITIES_PATH, params=without_tombstones)
        return TrackedEntitiesPage.model_validate(raw).model_copy(update={"tombstones_visible": False})
    return TrackedEntitiesPage.model_validate(raw)

poll_enrollments(reader, *, program_uid, updated_after, page, page_size=POLL_PAGE_SIZE) async

Read one page of one program's enrollments as a sync polls it, for whose projection moved.

SCOPED BY PROGRAM BECAUSE THE ENDPOINT ADMITS NOTHING ELSE. /api/tracker/enrollments answers E1003 "Program is mandatory" to a query naming a tracked entity type, or naming nothing at all, so an enrollment poll walks the programs the guide publishes rather than the types the register serves. Recorded as BUGS.md 102 alongside its sibling's refusal, which is not even JSON.

Only three fields are asked for. This poll never maps anything: what it answers is a list of tracked entities whose projection is stale, and each of them is re-read through the tracked entity path that every other projected row comes from, so there is exactly one mapping surface.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def poll_enrollments(
    reader: RegisterReader,
    *,
    program_uid: str,
    updated_after: datetime | None,
    page: int,
    page_size: int = POLL_PAGE_SIZE,
) -> TouchedPage:
    """Read one page of one program's enrollments as a sync polls it, for whose projection moved.

    SCOPED BY PROGRAM BECAUSE THE ENDPOINT ADMITS NOTHING ELSE. `/api/tracker/enrollments` answers
    `E1003 "Program is mandatory"` to a query naming a tracked entity type, or naming nothing at all,
    so an enrollment poll walks the programs the guide publishes rather than the types the register
    serves. Recorded as BUGS.md 102 alongside its sibling's refusal, which is not even JSON.

    Only three fields are asked for. This poll never maps anything: what it answers is a list of
    tracked entities whose projection is stale, and each of them is re-read through the tracked
    entity path that every other projected row comes from, so there is exactly one mapping surface.
    """
    params: dict[str, Any] = {
        "program": program_uid,
        "orgUnitMode": SEARCH_ORG_UNIT_MODE,
        "fields": _TOUCHED_ENROLLMENT_FIELDS,
        "order": ENROLLMENT_POLL_ORDER,
        "page": page,
        "pageSize": page_size,
        INCLUDE_DELETED_PARAMETER: "true",
        TOTAL_PAGES_PARAMETER: "true",
    }
    if updated_after is not None:
        params[UPDATED_AFTER_PARAMETER] = dhis2_instant(updated_after)
    try:
        raw = await reader.get_raw(ENROLLMENTS_PATH, params=params)
    except Dhis2ApiError as error:
        if _is_unknown_type_refusal(error):
            return TouchedPage()
        raise
    page_read = EnrollmentsPage.model_validate(raw)
    return TouchedPage(
        rows=tuple(
            TouchedRow(
                tracked_entity_uid=enrollment.trackedEntity,
                updated_at=as_instant(enrollment.updatedAt),
                deleted=bool(enrollment.deleted),
            )
            for enrollment in page_read.enrollments
        ),
        page_count=None if page_read.pager is None else page_read.pager.pageCount,
    )

as_instant(value)

One DHIS2 timestamp as a datetime, or None where it is not one this sync can compare against.

The tracker's OpenAPI document types an instant as a date-time OR an epoch integer, and this repo does not guess which unit an integer is in (routes.enrollments._instant carries the same integer through as text for the same reason). A watermark is a comparison, so an instant nobody can place on a clock is one this sync declines to advance to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
def as_instant(value: datetime | int | None) -> datetime | None:
    """One DHIS2 timestamp as a datetime, or None where it is not one this sync can compare against.

    The tracker's OpenAPI document types an instant as a date-time OR an epoch integer, and this repo
    does not guess which unit an integer is in (`routes.enrollments._instant` carries the same
    integer through as text for the same reason). A watermark is a comparison, so an instant nobody
    can place on a clock is one this sync declines to advance to.
    """
    return value if isinstance(value, datetime) else None

dhis2_instant(value)

One instant spelled the way updatedAfter takes it - the instance's own zone-less reading.

DHIS2 2.43 answers updatedAt as a wall-clock reading with no offset (BUGS.md 62) and takes updatedAfter in the same spelling, so a cursor read out of one answer goes back on the wire exactly as it arrived. Any offset this host attached would be a claim about a clock nobody consulted, so the value is written to seconds and the zone, if somebody attached one, is dropped.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
def dhis2_instant(value: datetime) -> str:
    """One instant spelled the way `updatedAfter` takes it - the instance's own zone-less reading.

    DHIS2 2.43 answers `updatedAt` as a wall-clock reading with no offset (BUGS.md 62) and takes
    `updatedAfter` in the same spelling, so a cursor read out of one answer goes back on the wire
    exactly as it arrived. Any offset this host attached would be a claim about a clock nobody
    consulted, so the value is written to seconds and the zone, if somebody attached one, is dropped.
    """
    return value.replace(tzinfo=None).isoformat(timespec="seconds")

is_tracked_entity_uid(value)

Whether a value could be a DHIS2 UID at all - eleven alphanumerics starting with a letter.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
def is_tracked_entity_uid(value: str) -> bool:
    """Whether a value could be a DHIS2 UID at all - eleven alphanumerics starting with a letter."""
    return _DHIS2_UID_PATTERN.match(value) is not None

fetch_tracked_entity(reader, tracked_entity_uid) async

Read one tracked entity by its UID, or None when the instance holds none under it.

A value that is not UID-shaped is None without a request: DHIS2 answers 400 for one, and a bare identifier search tries this read for every value it is given, most of which are national IDs rather than UIDs.

No program= parameter, ever: passing one turns "not enrolled in that program" into a 404 asserting the tracked entity does not exist (BUGS.md 72), which would make an unenrolled person indistinguishable from a wrong UID.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/wire.py
async def fetch_tracked_entity(reader: RegisterReader, tracked_entity_uid: str) -> TrackerTrackedEntity | None:
    """Read one tracked entity by its UID, or None when the instance holds none under it.

    A value that is not UID-shaped is None without a request: DHIS2 answers 400 for one, and a
    bare identifier search tries this read for every value it is given, most of which are national
    IDs rather than UIDs.

    No `program=` parameter, ever: passing one turns "not enrolled in that program" into a 404
    asserting the tracked entity does not exist (BUGS.md 72), which would make an unenrolled person
    indistinguishable from a wrong UID.
    """
    if not is_tracked_entity_uid(tracked_entity_uid):
        return None
    try:
        raw = await reader.get_raw(
            f"{TRACKED_ENTITIES_PATH}/{tracked_entity_uid}", params={"fields": TRACKED_ENTITY_FIELDS}
        )
    except Dhis2ApiError as error:
        if error.status_code == 404:
            return None
        raise
    return TrackerTrackedEntity.model_validate(raw)

projection

One DHIS2 tracked entity as the FHIR resource its type is registered as - identity, and nothing invented.

WHY THE PROJECTION IS THE SAME WHATEVER THE RESOURCE IS. D2TET_CM says a type is served as a Patient, a Specimen, a Group - and that is a statement about which resource the entity belongs in, not a promise that DHIS2 holds what that resource defines. A Specimen here therefore carries no Specimen.type, no collection, no subject: DHIS2 has no specimen-type field, no collection event on a tracked entity, and no declared link from a sample to the person it came from. It has tracked entity attributes, and which of them mean those things is a decision each instance makes for itself, usually differently. The same holds for the people: Patient.name, Patient.gender, and Patient.birthDate are the elements every FHIR client reaches for first, and DHIS2 has no first-name attribute, no sex attribute, no date-of-birth attribute. A server that matched on attribute names would be inventing a semantic mapping and publishing it as fact - and a wrong gender on a person, or a wrong type on a sample, is a worse answer than none.

WHICH IS WHY THOSE THREE ELEMENTS COME FROM A NOMINATION AND FROM NOTHING ELSE. [ips.identity] names the attribute carrying a person's name, birth date, and sex, and maps that sex attribute's values onto R4's administrative-gender codes; dhis2w_fhir.ips reads a person's values through it (docs/fhir/design/ips.md section 4, and section 9's phase 1, which is this). A project that nominates nothing serves exactly what this register served before the table existed, byte for byte, because a registered resource then answers exactly one question, which is what this thing is in this instance. A nomination adds a reading of a value and removes nothing: the value keeps riding the attribute-value extension as the string DHIS2 sent, so a client sees both what the instance holds and what this project says it means. Nomination reaches only the resource types that define those elements - the people - and never a Specimen or a Location, which define none of them.

WHAT IT DOES CARRY, and where each fact comes from:

  • resourceType is what the published map takes the entity's tracked entity type onto.
  • id is the DHIS2 tracked entity UID, so {resourceType}/<uid> reads back the same entity.
  • identifier[] opens with that UID under {base}/id/tracked-entity - the very system a QuestionnaireResponse names its subject under, so a capture client and a lookup speak one language - and then carries one entry per value of an attribute DHIS2 declares unique, under {base}/tracked-entity-attribute/{uid}. Uniqueness is what makes a value name a subject rather than describe one; the guide already publishes the flag as a D2TEA_CS concept property. A searchable attribute that is not unique is a key this server looks under, not an identifier the instance enforces, so it rides the extensions with everything else.
  • meta.tag states the tracked entity type, under {base}/id/tracked-entity-type. A tag is R4's element for classifying a resource, and the type is a classification rather than a name, so it does not belong in identifier[] - and stating it as a tag needs no StructureDefinition to resolve, which matters because live mode serves none.
  • extension[] carries every remaining attribute value on the D2TrackedEntityAttributeValue extension: the attribute's UID, the DHIS2 code when the instance set one, and the value as the string DHIS2 sent. Untyped on purpose - it is DHIS2's own value, not a FHIR reading of it.

Values are read off the entity and off its enrollments alike, deduplicated by attribute and value. A DHIS2 attribute may be collected at the tracked entity type or at the program, and an entity found by a program attribute's unique value that then came back not carrying it would be an answer contradicting the question that found it.

Classes

Functions:

registered_entity_for(entity, index, resource_type)

Project one tracked entity onto the resource this server serves it as.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/projection.py
def registered_entity_for(
    entity: TrackerTrackedEntity, index: TrackedEntityIndex, resource_type: str
) -> RegisteredEntity:
    """Project one tracked entity onto the resource this server serves it as."""
    tracked_entity_uid = entity.trackedEntity or ""
    values = attribute_values(entity, index)
    identifiers = [Identifier(system=index.tracked_entity_system, value=tracked_entity_uid)]
    identifiers.extend(tracked_entity_attribute_identifiers(values, index.identifier_system_base))
    extensions = tracked_entity_attribute_value_extensions(values, index.attribute_value_extension_url)
    identity = _identity(values, index, resource_type)
    return RegisteredEntity(
        resourceType=resource_type,
        id=tracked_entity_uid,
        meta=_type_tag(entity, index),
        identifier=identifiers,
        extension=extensions or None,
        name=identity.name,
        gender=identity.gender,
        birthDate=identity.birth_date,
        birthDate_element=identity.birth_date_element,
    )

attribute_values(entity, index)

Every attribute value the entity holds, entity-level and enrollment-level, each carried once.

The join onto the guide is what decides how a value is carried: unique makes it an identifier, and the DHIS2 code and display come from what D2TEA_CS published. An attribute the guide never published is still carried - the instance holds the value, and dropping it would make the served resource depend on which forms this project happened to select - it simply carries no code and is never treated as an identifier, because nothing here states that DHIS2 enforces its uniqueness.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/projection.py
def attribute_values(entity: TrackerTrackedEntity, index: TrackedEntityIndex) -> list[TrackedEntityAttributeValueIn]:
    """Every attribute value the entity holds, entity-level and enrollment-level, each carried once.

    The join onto the guide is what decides how a value is carried: `unique` makes it an identifier,
    and the DHIS2 code and display come from what `D2TEA_CS` published. An attribute the guide never
    published is still carried - the instance holds the value, and dropping it would make the served
    resource depend on which forms this project happened to select - it simply carries no code and is
    never treated as an identifier, because nothing here states that DHIS2 enforces its uniqueness.
    """
    carried: dict[tuple[str, str], TrackedEntityAttributeValueIn] = {}
    for attribute in _attributes(entity):
        if attribute.attribute is None or attribute.value is None:
            continue
        published = index.attribute(attribute.attribute)
        key = (attribute.attribute, attribute.value)
        carried.setdefault(
            key,
            TrackedEntityAttributeValueIn(
                attribute_uid=attribute.attribute,
                value=attribute.value,
                display=published.display if published is not None else attribute.displayName,
                code=published.code if published is not None else attribute.code,
                unique=published.unique if published is not None else False,
            ),
        )
    return list(carried.values())

listing

Paging the register: how a search with no identifier walks the instance, one page at a time.

WHAT PAGES ARE NAMED WITH. FHIR fixes _count as the number of resources a client wants on a page and leaves the naming of the page itself to the server - "the parameters used to continue a search are implementation defined" (R4 3.1.1.4, Bundle.link). This server names it page, so a page of the listing is GET /{resourceType}?_count=20&page=<token>, and a client's whole job is to follow the next and previous links rather than to build either parameter.

WHAT THE TOKEN IS. DHIS2 pages one tracked entity type at a time - the tracker endpoint requires a type and offers no way to ask about two - so a listing over several types of one resource is several upstream pagings walked in the order [serve.tracked_entities] tracked_entity_types declares them (or, when it declares none, the order the published forms register them in). A page is therefore located by two numbers, the type's place in that order and the upstream page number within it, plus the searchset total once it has been counted, and the token is those folded into one opaque string: t{type index}p{upstream page} with n{total} appended when a total is carried. page=dDBwMg is type 0, page 2. It is opaque in the sense that matters - a client mints none of it, and its shape is this server's business - and decodable in one line by whoever is reading a log.

A stateless token is the point. Nothing about a listing is remembered between requests, so a link handed out an hour ago still resolves, and two clients walking the same listing share nothing. The total rides the token for that reason rather than a cache: it is a fact this server counted once, carried forward by the link rather than remembered against the client that followed it.

WHAT A PAGE HOLDS. A page never mixes tracked entity types: _count is a maximum, and the last page of each type carries whatever that type had left. Filling the remainder from the next type would cost a second upstream request per page and put two kinds of subject in one page for no gain a client asked for. A type in scope that holds nothing is skipped rather than served as an empty page, so a next link never lands somewhere with nothing on it while entities remain further on.

WHAT total MEANS. Bundle.total is the number of tracked entities in the whole searchset, and DHIS2 states one per type. With a single type in scope that is the same number, and every page of the listing already carries it. With several, the searchset's total is the sum of one count per type, and this server asks for those counts rather than declining to state a number it can get: one count-only request per type, bounded by how many types the project put in scope, spent on the first page of a walk and carried through the rest on the page token. A type whose pager states no total makes the sum unknowable, and then the Bundle states no total - which is the instance's silence, not this server's choice.

Classes

ListingCursor

Bases: BaseModel

Where one page of the listing sits, and the searchset total counted before it was reached.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
class ListingCursor(BaseModel):
    """Where one page of the listing sits, and the searchset total counted before it was reached."""

    model_config = ConfigDict(frozen=True)

    type_index: int = 0
    upstream_page: int = 1
    searchset_total: int | None = None
    """How many tracked entities the whole searchset holds, once a page of this walk has counted them.

    Absent on the cursor a client starts from, which is what makes the first page do the counting.
    A single type in scope never fills it: that walk reads the total off every page's own pager, so
    there is nothing to carry.
    """

    @classmethod
    def from_token(cls, token: str) -> ListingCursor:
        """Read one `page` token, refusing anything this server did not mint."""
        try:
            decoded = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode("ascii")
        except (binascii.Error, UnicodeDecodeError, ValueError) as error:
            raise BadSearchError(_UNREADABLE_CURSOR) from error
        match = _CURSOR_PATTERN.match(decoded)
        if match is None:
            raise BadSearchError(_UNREADABLE_CURSOR)
        upstream_page = int(match.group(2))
        if upstream_page < 1:
            raise BadSearchError(_UNREADABLE_CURSOR)
        total = match.group(3)
        return cls(
            type_index=int(match.group(1)),
            upstream_page=upstream_page,
            searchset_total=None if total is None else int(total),
        )

    def token(self) -> str:
        """This cursor as the `page` parameter carries it."""
        counted = "" if self.searchset_total is None else f"n{self.searchset_total}"
        decoded = f"t{self.type_index}p{self.upstream_page}{counted}"
        return base64.urlsafe_b64encode(decoded.encode("ascii")).decode().rstrip("=")
Attributes
searchset_total = None class-attribute instance-attribute

How many tracked entities the whole searchset holds, once a page of this walk has counted them.

Absent on the cursor a client starts from, which is what makes the first page do the counting. A single type in scope never fills it: that walk reads the total off every page's own pager, so there is nothing to carry.

Methods:
from_token(token) classmethod

Read one page token, refusing anything this server did not mint.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
@classmethod
def from_token(cls, token: str) -> ListingCursor:
    """Read one `page` token, refusing anything this server did not mint."""
    try:
        decoded = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4)).decode("ascii")
    except (binascii.Error, UnicodeDecodeError, ValueError) as error:
        raise BadSearchError(_UNREADABLE_CURSOR) from error
    match = _CURSOR_PATTERN.match(decoded)
    if match is None:
        raise BadSearchError(_UNREADABLE_CURSOR)
    upstream_page = int(match.group(2))
    if upstream_page < 1:
        raise BadSearchError(_UNREADABLE_CURSOR)
    total = match.group(3)
    return cls(
        type_index=int(match.group(1)),
        upstream_page=upstream_page,
        searchset_total=None if total is None else int(total),
    )
token()

This cursor as the page parameter carries it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
def token(self) -> str:
    """This cursor as the `page` parameter carries it."""
    counted = "" if self.searchset_total is None else f"n{self.searchset_total}"
    decoded = f"t{self.type_index}p{self.upstream_page}{counted}"
    return base64.urlsafe_b64encode(decoded.encode("ascii")).decode().rstrip("=")

RegisterListingPage

Bases: BaseModel

One page of the register: what is on it, where it is, and where the pages either side of it are.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
class RegisterListingPage(BaseModel):
    """One page of the register: what is on it, where it is, and where the pages either side of it are."""

    model_config = ConfigDict(frozen=True)

    entities: list[TrackerTrackedEntity] = []
    cursor: ListingCursor = ListingCursor()
    """Where the entities on this page actually came from, which is what the `self` link names."""

    total: int | None = None
    """How many tracked entities the whole searchset holds, when the instance stated it for all of it."""

    next_cursor: ListingCursor | None = None
    previous_cursor: ListingCursor | None = None
Attributes
cursor = ListingCursor() class-attribute instance-attribute

Where the entities on this page actually came from, which is what the self link names.

total = None class-attribute instance-attribute

How many tracked entities the whole searchset holds, when the instance stated it for all of it.

Functions:

read_listing_page(reader, *, tracked_entity_type_uids, cursor, count, filters=()) async

Read the page one cursor names, skipping forward over any type in scope that holds nothing.

filters narrows every request this walk makes - the page, the counts behind its total, and the page count a step backwards over a type boundary asks for - so a filtered register pages exactly as the whole one does and states a total of what the filter selected. A type holding nobody who matches is skipped the way a type holding nobody is.

Running off the end of the type list is an empty page rather than a refusal: a link minted before the register was emptied, or before a type left [serve.tracked_entities], has become a page with nothing on it, and that is what a searchset says when the query is unsatisfied rather than malformed.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
async def read_listing_page(
    reader: RegisterReader,
    *,
    tracked_entity_type_uids: tuple[str, ...],
    cursor: ListingCursor,
    count: int,
    filters: Sequence[str] = (),
) -> RegisterListingPage:
    """Read the page one cursor names, skipping forward over any type in scope that holds nothing.

    `filters` narrows every request this walk makes - the page, the counts behind its total, and the
    page count a step backwards over a type boundary asks for - so a filtered register pages exactly
    as the whole one does and states a total of what the filter selected. A type holding nobody who
    matches is skipped the way a type holding nobody is.

    Running off the end of the type list is an empty page rather than a refusal: a link minted before
    the register was emptied, or before a type left `[serve.tracked_entities]`, has become a page with
    nothing on it, and that is what a searchset says when the query is unsatisfied rather than malformed.
    """
    type_index = cursor.type_index
    upstream_page = cursor.upstream_page
    read: TrackedEntitiesPage | None = None
    while type_index < len(tracked_entity_type_uids):
        read = await list_tracked_entities(
            reader,
            tracked_entity_type_uid=tracked_entity_type_uids[type_index],
            page=upstream_page,
            page_size=count,
            filters=filters,
        )
        if read.trackedEntities:
            total = await _searchset_total(reader, read, tracked_entity_type_uids, cursor, filters)
            reached = ListingCursor(
                type_index=type_index, upstream_page=upstream_page, searchset_total=cursor.searchset_total
            )
            return RegisterListingPage(
                entities=read.trackedEntities,
                cursor=reached,
                total=total,
                next_cursor=_next_cursor(reached, read, count, len(tracked_entity_type_uids), total),
                previous_cursor=await _previous_cursor(
                    reader, reached, tracked_entity_type_uids, count, total, filters
                ),
            )
        type_index += 1
        upstream_page = 1
    total = None if read is None else await _searchset_total(reader, read, tracked_entity_type_uids, cursor, filters)
    return RegisterListingPage(
        cursor=cursor,
        total=total,
        previous_cursor=await _previous_cursor(reader, cursor, tracked_entity_type_uids, count, total, filters),
    )

count_listing_total(reader, *, tracked_entity_type_uids, filters=()) async

How many tracked entities the whole listing holds, counted without carrying any of them back.

DHIS2 counts one type at a time, so this is one count-only request per type in scope, summed, each narrowed by whatever the request filtered on. A type whose pager states no total makes the sum unknowable, and an unknowable sum is stated as no total rather than as a partial one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
async def count_listing_total(
    reader: RegisterReader, *, tracked_entity_type_uids: tuple[str, ...], filters: Sequence[str] = ()
) -> int | None:
    """How many tracked entities the whole listing holds, counted without carrying any of them back.

    DHIS2 counts one type at a time, so this is one count-only request per type in scope, summed, each
    narrowed by whatever the request filtered on. A type whose pager states no total makes the sum
    unknowable, and an unknowable sum is stated as no total rather than as a partial one.
    """
    counted = 0
    for tracked_entity_type_uid in tracked_entity_type_uids:
        total = await count_tracked_entities(reader, tracked_entity_type_uid=tracked_entity_type_uid, filters=filters)
        if total is None:
            return None
        counted += total
    return counted

filtering

Filtering the register by what a record holds: d2-attribute={attributeUid}|{value}.

identifier answers the keys and this answers everything else. A value of an attribute DHIS2 declares unique names one person, and identifier is FHIR's own parameter for that. A value of an attribute DHIS2 declares nothing about describes a lot of people - sex, district of residence, whether consent was given - and no FHIR parameter names it, because FHIR has no element for it: the value rides the D2TrackedEntityAttributeValue extension, which is exactly where a facade puts what the standard states no place for (register.projection). So the parameter is this server's own, it is spelled with the d2- prefix every DHIS2-specific thing here is spelled with, and it names the attribute in its own value rather than inventing one parameter per attribute an instance happens to hold.

IT ANSWERS EQUALITY AND NOTHING ELSE, AND EVERY DECLARATION OF IT SAYS SO. No prefix, no substring, no range, no :missing, no ordering. d2-attribute=cejWyOfXge6|Female finds whoever holds exactly that value under exactly that attribute. A filter that looks like search and matches only exact values is a trap unless it says which it is, so /metadata says it in the search parameter's documentation, /facade/uiconfig says it beside the attributes it declares, and the serving guide says it in prose. A caller wanting "starts with" wants _content, which is the substring search a projection-served register answers.

EQUALITY IGNORES CASE, BECAUSE DHIS2'S OWN eq DOES. filter=<uid>:eq:Female and filter=<uid>:eq:female answer the same 243 people on a 2.43 instance (BUGS.md 109), so an operator this server called equality would otherwise mean two different things depending on which backend answered it. The projection matches the folded value for that reason - the column is already indexed folded, because _content needed it first - and the two backends agree on every value in the register rather than on the ones that happen to be typed the way they were stored.

THE GRAMMAR IS identifier'S, READ ONE PLACE FURTHER LEFT. {system}|{value} is R4's token form and routes.read.identifier_token already parses it; here the system slot carries the DHIS2 tracked entity attribute UID rather than a URI, because the attribute is what the value belongs to and the UID is what the instance and this guide both name it by. A token naming no attribute is refused rather than searched across all of them: identifier may look everywhere because a key names somebody wherever it is held, and a bare Female looked for everywhere would match a district called Female as readily as a sex.

ONE OCCURRENCE IS ONE PAIR AND OCCURRENCES NARROW. d2-attribute=A|x&d2-attribute=B|y is whoever holds both, which is what R4 says two instances of one parameter mean. A comma is NOT an alternative here, and this is the one place this server departs from R4's token grammar deliberately: a DHIS2 attribute value is free text an instance chose, Smith, John is a value somebody actually holds, and splitting it would make the person holding it unfindable by the value they hold. So the value is taken whole, and the declarations say so.

WHICH ATTRIBUTES ARE FILTERABLE IS THE GUIDE'S ANSWER AND NOT A DIAL. The attributes a register filters on are the ones the published registration forms ask of the tracked entity types it is served over - the same set whose values the register already serves on every resource it hands back. There is no [serve.tracked_entities] filter_attributes key, and the reason is the reason search_attributes HAS one: that table restricts what values NAME a subject, which is a decision about identity an operator can only make themselves, while this filters on values the same response already carries in full. A dial narrowing it would let an operator hide a filter over data the server hands over anyway, which is a setting that reads like a control and is not one.

Classes

AttributeFilter

Bases: BaseModel

One {attributeUid}|{value} equality a register search is narrowed by.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
class AttributeFilter(BaseModel):
    """One `{attributeUid}|{value}` equality a register search is narrowed by."""

    model_config = ConfigDict(frozen=True)

    attribute_uid: str
    value: str

    def wire_expression(self) -> str:
        """This equality as `/api/tracker/trackedEntities` takes it on a `filter=` parameter."""
        return f"{self.attribute_uid}:{ATTRIBUTE_FILTER_OPERATOR}:{self.value}"

    def folded_value(self) -> str:
        """The value as an index holding folded text matches it - see this module's note on case."""
        return self.value.casefold()
Methods:
wire_expression()

This equality as /api/tracker/trackedEntities takes it on a filter= parameter.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
def wire_expression(self) -> str:
    """This equality as `/api/tracker/trackedEntities` takes it on a `filter=` parameter."""
    return f"{self.attribute_uid}:{ATTRIBUTE_FILTER_OPERATOR}:{self.value}"
folded_value()

The value as an index holding folded text matches it - see this module's note on case.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
def folded_value(self) -> str:
    """The value as an index holding folded text matches it - see this module's note on case."""
    return self.value.casefold()

Functions:

requested_attribute_filters(query_items, *, resource_type, declared)

Read every value filter one request names, refusing a malformed one and an undeclared attribute.

Both refusals are 400s and both name what would have worked, because there is no reading of either that a client should discover by getting back a page of everybody: a token with no attribute in it is a query this server cannot place, and an attribute this register does not filter on is a query nothing could ever satisfy.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
def requested_attribute_filters(
    query_items: Iterable[tuple[str, str]],
    *,
    resource_type: str,
    declared: Sequence[PublishedAttribute],
) -> tuple[AttributeFilter, ...]:
    """Read every value filter one request names, refusing a malformed one and an undeclared attribute.

    Both refusals are 400s and both name what would have worked, because there is no reading of
    either that a client should discover by getting back a page of everybody: a token with no
    attribute in it is a query this server cannot place, and an attribute this register does not
    filter on is a query nothing could ever satisfy.
    """
    filterable = {attribute.attribute_uid for attribute in declared}
    filters: list[AttributeFilter] = []
    for name, raw in query_items:
        if name != ATTRIBUTE_FILTER_PARAMETER:
            continue
        attribute_uid, separator, value = raw.partition(ATTRIBUTE_FILTER_SEPARATOR)
        if not separator or not attribute_uid or not value:
            raise BadSearchError(
                f"`{ATTRIBUTE_FILTER_PARAMETER}` was given `{raw}`, which names no attribute and value: "
                f"the filter is `{ATTRIBUTE_FILTER_PARAMETER}=<trackedEntityAttributeUid>"
                f"{ATTRIBUTE_FILTER_SEPARATOR}<value>`, and it matches that value exactly"
            )
        if attribute_uid not in filterable:
            raise UnknownFilterAttributeError(
                resource_type, ATTRIBUTE_FILTER_PARAMETER, attribute_uid, tuple(sorted(filterable))
            )
        filters.append(AttributeFilter(attribute_uid=attribute_uid, value=value))
    return tuple(filters)

holds_every_filter(values, filters)

Whether one entity's own attribute values satisfy every filter, folded as DHIS2's eq folds.

Read off the entity rather than asked of anything, and it is asked at the one place a search already has the record in hand: an identifier search resolves each match live before it hands it over, so the values are there, and the alternative is a second query per attribute per type to learn what this one already knows. The listing cannot work this way - a page has to be narrowed where it is counted, or paging over a filtered register would hand out short pages - so the listing narrows at the instance and at the projection instead.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
def holds_every_filter(values: Sequence[TrackedEntityAttributeValueIn], filters: Sequence[AttributeFilter]) -> bool:
    """Whether one entity's own attribute values satisfy every filter, folded as DHIS2's `eq` folds.

    Read off the entity rather than asked of anything, and it is asked at the one place a search
    already has the record in hand: an identifier search resolves each match live before it hands it
    over, so the values are there, and the alternative is a second query per attribute per type to
    learn what this one already knows. The listing cannot work this way - a page has to be narrowed
    where it is counted, or paging over a filtered register would hand out short pages - so the
    listing narrows at the instance and at the projection instead.
    """
    held: dict[str, set[str]] = {}
    for attribute_value in values:
        held.setdefault(attribute_value.attribute_uid, set()).add(attribute_value.value.casefold())
    return all(filter_.folded_value() in held.get(filter_.attribute_uid, set()) for filter_ in filters)

wire_filters(filters)

Every filter as the tracker endpoint takes them - one filter= parameter apiece, ANDed there.

Repeated rather than comma-joined: both are ANDed by 2.42 and 2.43 alike, and the repeated form is the one whose values may contain a comma without the endpoint reading it as a separator.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/filtering.py
def wire_filters(filters: Sequence[AttributeFilter]) -> list[str]:
    """Every filter as the tracker endpoint takes them - one `filter=` parameter apiece, ANDed there.

    Repeated rather than comma-joined: both are ANDed by 2.42 and 2.43 alike, and the repeated form
    is the one whose values may contain a comma without the endpoint reading it as a separator.
    """
    return [filter_.wire_expression() for filter_ in filters]

register

The register: GET /{resourceType} and GET /{resourceType}/{id} answered from the DHIS2 instance.

These are the resource types this server answers from DHIS2 rather than from what it loaded at startup, and the only ones whose answer can differ between two requests a second apart. Which types they are is the published D2TET_CM's to say: one row per tracked entity type the project's forms register, each naming the FHIR resource its registrations are served as. A project tracking people alone serves Patient and nothing else here; a project that also tracks samples serves Specimen beside it, over exactly the types the map puts there. dhis2w_fhir_serve.routes.read dispatches to this module for those types and answers from the store for every other, so there is one pair of catch-all routes rather than a pair per resource.

One implementation, parameterized by resource type. Nothing below branches on which resource is being answered. A Specimen is searched by the same identifier grammar, paged by the same cursor, and projected by the same rule as a Patient - see dhis2w_fhir_serve.register.projection for why that projection states no resource-specific element for any of them.

Whose data this is depends on who asked. Under [serve] auth = "dhis2" every read below carries the CALLER'S own Authorization header to the instance, so DHIS2's sharing, organisation-unit scopes, ownership, and access levels decide what comes back, per caller, and this module applies no rule of its own. Under none and token the reads run over the runtime's client and answer with that profile's rights. dhis2w_fhir_serve.passthrough is where that is decided and why.

Live mode only. The default mode serves a compiled guide and holds no DHIS2 client, so there is no instance to ask; it answers a not-supported OperationOutcome saying so, and /metadata declares none of these types, which is the same fact stated ahead of the request. A live process whose project publishes no registration form answers the same way for the same reason one step further in: with no published tracked entity type, /api/tracker/trackedEntities has nothing to be given and refuses the query (E1003).

[serve.tracked_entities] says how much of this surface exists. enabled = false refuses every route here and declares no register type at /metadata, which is the compiled-mode posture arrived at from the project rather than from the invocation - and it is checked first, because a project that serves no tracked entity serves none whichever way the process was started. listing = false refuses the no-identifier request alone and leaves identifier search exactly as it is. Both refusals name the key that produced them, so an operator reading the outcome knows which line to change.

A request naming no identifier is the listing: a paged searchset over the tracked entity types that resource is served over, which dhis2w_fhir_serve.register.listing walks and links. _count is honoured up to [serve.tracked_entities] page_size_limit and clamped rather than refused above it, and _count=0 asks how large the register is - answered by counting the instance rather than by building a page nobody wants.

ONE FHIR RESOURCE TYPE IS ONE REGISTER OVER THE UNION OF ITS TRACKED ENTITY TYPES. An instance may map fifty types onto nine resources, and two types mapped onto Device - a cold-chain fridge and a delivery vehicle - are one GET /Device answering about both. Nothing collides and nothing is last-writer-wins: the map is read into one row per type, the surface groups the rows by resource, and every read, search, listing, and count below is parameterized by the list of types it runs over. Each served resource still says which DHIS2 type it is, as the meta.tag register.projection carries - so a union is a union of stated things rather than a merge.

_tag is how a caller asks that union about one of its types, and it is R4's own token search over exactly the element the resource states the type in: _tag={base}/id/tracked-entity-type|{uid}, or _tag={uid} for the code alone. It narrows the listing's walk, the search's scope, and the _count=0 count alike, it rides every next and previous link so a walk stays inside the type it started in, and under [serve.search] backend = "projection" it narrows the store's own query rather than thinning its pages. A _tag naming a type this resource is not served over is an unsatisfied query, answered with an empty searchset. See _requested_type_uids.

d2-attribute IS HOW A CALLER ASKS THE REGISTER FOR WHAT A RECORD HOLDS RATHER THAN WHO IT IS. identifier answers the attributes DHIS2 declares unique, exactly and by design. The attributes it declares nothing about - sex, district of residence, whether consent was given - describe a lot of people rather than naming one, and d2-attribute={attributeUid}|{value} is the filter over them: one occurrence is one attribute and one value, occurrences narrow, and IT ANSWERS EQUALITY AND NOTHING ELSE. It narrows the listing, the identifier search, the _content search, and the _count=0 count alike, under either backend, and it rides the listing's next and previous links the way _tag does. Which attributes a register filters on is what its types' own published forms ask, declared per register at /metadata and at /facade/uiconfig; an attribute the request names and this register does not filter on is a 400 naming the ones it does. dhis2w_fhir_serve.register.filtering argues every part of that, including why there is no config dial narrowing the set.

A parameter this surface cannot apply is refused, and that is the whole reason search_register reads the query rather than filtering it. The store searches ignore what they do not recognise, because the worst an ignored parameter costs there is a larger result set than a client expected. Here it costs the register itself: family=Smith answered with the listing is every registered person handed back as though each were a Smith. So the query is checked before anything is read, and anything but identifier and _tag (plus _count, and page on the listing) is a 400 naming what is answered. See _require_answerable_parameters.

identifier is the whole search surface for naming one entity, in both of FHIR's token forms:

  • identifier={system}|{value} names which key the value is. {base}/id/tracked-entity is the tracked entity UID itself and is answered by reading that one entity, not by filtering - a UID is not an attribute and no filter= expression could ask for it. Every other system names one tracked entity attribute the guide publishes, and the search filters on it.
  • identifier={value} names no key, so every key is tried: the UID read plus one filtered search per key attribute, folded into one result set and deduplicated by tracked entity UID. An entity holding the same value in two of them appears once.

[serve.search] backend = "projection" MOVES THE FINDING HALF AND LEAVES THE DISCLOSING HALF WHERE IT IS. Under that backend the searchset's membership, its paging, and its _content answer all come out of the materialized projection d2w fhir sync fills - one indexed query over a local file rather than one tracker query per key per tracked entity type, and answerable in ways an exact-match filter is not. Three things stay exactly as they are, and they are the three that matter:

  • Every record is still read live, under the caller's own credentials. The projection says who is on the page; fetch_tracked_entity says whether this caller may have them, per person, per request. That is docs/fhir/design/projection.md R9 and its recommended posture (iii) in full, and it is why a projection can answer the finding half without this facade taking on one line of DHIS2's authorization model. GET /{resourceType}/{id} is a person-level read and therefore stays live whatever the backend says.
  • Every projection-served answer states the instant it is as of - an outcome entry in the searchset and a header beside it (dhis2w_fhir_serve.projection.serving). A live answer states none, because it is as of the moment the instance answered.
  • No projection-served answer states a total. The projection counted its rows under the build identity the sync ran as, and how many of them THIS caller may see is DHIS2's to say one read at a time - so a count taken before that is a number about somebody else, and a Bundle states no total rather than one nobody counted for the person reading it.

_content is the one search parameter that arrives with the backend, and it is R4's own parameter for a text search over a resource's whole content. It is the honest spelling: this server does not know which of a person's DHIS2 attribute values is their name - register.projection says at length why it refuses to guess - so it offers a search across all of them rather than a family it would be inventing. Under backend = "dhis2" the parameter is refused exactly as every other unanswerable one is, because an exact-match filter cannot answer it.

A SEARCH FINDS IDENTIFIERS AND A READ RESOLVES THEM. Every filtered search here goes through dhis2w_fhir_serve.projection.NameSearchIndex, which answers with tracked entity UIDs and scores and never with records; each match is then read back through fetch_tracked_entity under the credentials this request runs as. Splitting it that way is what lets a search index sit behind the seam without this facade ever deciding on DHIS2's behalf who may see whom - the index says an identifier matched, and the instance says whether this caller may have the person behind it (docs/fhir/design/projection.md R9). [serve.search] backend names which index; dhis2 is the instance itself, and it is the default.

Which attributes are keys is the surface's answer, not this module's: by default the ones DHIS2 declares unique or searchable, and the ones [serve.tracked_entities] search_attributes names when it names any. A searchable non-unique attribute matching several entities is answered with all of them - a searchset carries as many matches as there are, and a register listing is already the shape that renders them.

A system this guide publishes nothing for matches nothing, and answers an empty searchset rather than an error: FHIR's search semantics make an unmatched token an empty result, and a 404 would tell a client its query was malformed when it was merely unsatisfied. Empty is likewise what an identifier nothing holds answers - never a 404, which on a search path would mean the endpoint does not exist.

Searching across every tracked entity type one resource is served over is deliberate. DHIS2 requires exactly one type per query, so a resource carrying two of them costs two requests per key; that is the price of not making a client know which type its identifier belongs to.

Classes

RegisterLookup

Bases: BaseModel

What one register request runs against: the connection, what this run serves, and where it searches.

They are resolved together because a request that cannot have one has no use for the others: a compiled run holds no connection, a project serving no tracked entity type has nothing to search, and the index is built over the very connection - or the very store - the resolution reads through.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
class RegisterLookup(BaseModel):
    """What one register request runs against: the connection, what this run serves, and where it searches.

    They are resolved together because a request that cannot have one has no use for the others: a
    compiled run holds no connection, a project serving no tracked entity type has nothing to search,
    and the index is built over the very connection - or the very store - the resolution reads through.
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    reader: RegisterReader
    surface: RegisterSurface
    index: NameSearchIndex

    store: ProjectionStore | None = None
    """The materialized projection, where this run answers a search from one, and None otherwise.

    None is both postures a live run can be in: `[serve.search] backend = "dhis2"`, and a project that
    configured no projection at all. Either way the searches below read the instance, which is what
    they have always done.
    """

    tracked_entity_type_uids: tuple[str, ...] = ()
    """The tracked entity types THIS request runs over, in the order the listing pages through them.

    The resource's own types by default - one FHIR resource is served over every type the published
    map takes onto it - narrowed to the ones a `_tag` named when the request named any.
    """

    typed_by_request: bool = False
    """Whether `_tag` named the types, which is what makes an entity DHIS2 states no type for a miss.

    A read of a resource that carries no `trackedEntityType` is served under whatever resource was
    asked, because the instance itself declined to classify it. A request that NAMED a type is a
    different question - "which of these are samples" - and an entity nothing states the type of is
    not an answer to it.
    """

    def from_projection(self) -> bool:
        """Whether this request's searchset membership comes from the projection rather than the instance."""
        return self.store is not None

    def scoped_type_uids(self) -> tuple[str, ...]:
        """The types a projection query is narrowed by, empty where the request narrowed nothing.

        Empty means "every type this resource is served over" to a store query, which is what a
        request naming no `_tag` asks for. A `_tag` that narrowed the scope to nothing never reaches
        a store: `search_register` answers it empty before anything is asked.
        """
        return self.tracked_entity_type_uids if self.typed_by_request else ()
Attributes
store = None class-attribute instance-attribute

The materialized projection, where this run answers a search from one, and None otherwise.

None is both postures a live run can be in: [serve.search] backend = "dhis2", and a project that configured no projection at all. Either way the searches below read the instance, which is what they have always done.

tracked_entity_type_uids = () class-attribute instance-attribute

The tracked entity types THIS request runs over, in the order the listing pages through them.

The resource's own types by default - one FHIR resource is served over every type the published map takes onto it - narrowed to the ones a _tag named when the request named any.

typed_by_request = False class-attribute instance-attribute

Whether _tag named the types, which is what makes an entity DHIS2 states no type for a miss.

A read of a resource that carries no trackedEntityType is served under whatever resource was asked, because the instance itself declined to classify it. A request that NAMED a type is a different question - "which of these are samples" - and an entity nothing states the type of is not an answer to it.

Methods:
from_projection()

Whether this request's searchset membership comes from the projection rather than the instance.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
def from_projection(self) -> bool:
    """Whether this request's searchset membership comes from the projection rather than the instance."""
    return self.store is not None
scoped_type_uids()

The types a projection query is narrowed by, empty where the request narrowed nothing.

Empty means "every type this resource is served over" to a store query, which is what a request naming no _tag asks for. A _tag that narrowed the scope to nothing never reaches a store: search_register answers it empty before anything is asked.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
def scoped_type_uids(self) -> tuple[str, ...]:
    """The types a projection query is narrowed by, empty where the request narrowed nothing.

    Empty means "every type this resource is served over" to a store query, which is what a
    request naming no `_tag` asks for. A `_tag` that narrowed the scope to nothing never reaches
    a store: `search_register` answers it empty before anything is asked.
    """
    return self.tracked_entity_type_uids if self.typed_by_request else ()

Functions:

register_resource_types(request)

Every resource type this process answers from the instance, which is what a read dispatches on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
def register_resource_types(request: Request) -> tuple[str, ...]:
    """Every resource type this process answers from the instance, which is what a read dispatches on."""
    return serve_context(request).register_surface.register_resource_types()

search_register(request, resource_type) async

Answer the entities a search names, or - naming none - one page of the register.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
async def search_register(request: Request, resource_type: str) -> Response:
    """Answer the entities a search names, or - naming none - one page of the register."""
    lookup = await register_lookup(request, resource_type)
    honored: list[HonoredParameter] = []
    tokens: list[IdentifierToken] = []
    contents: list[tuple[str, ...]] = []
    for name, raw in request.query_params.multi_items():
        if name == IDENTIFIER_SEARCH_PARAMETER:
            tokens.extend(identifier_token(name, value) for value in alternatives(name, raw))
        elif name == CONTENT_SEARCH_PARAMETER:
            contents.append(tuple(alternatives(name, raw)))
        elif name not in (TAG_SEARCH_PARAMETER, ATTRIBUTE_FILTER_PARAMETER):
            continue
        # `_tag` is honored like the other two and produces no token of its own: it narrowed which
        # tracked entity types this lookup runs over, before any of them was asked anything.
        # `d2-attribute` is honored here and read below, so a malformed one is refused after this
        # resource is known to be served rather than before.
        honored.append(HonoredParameter(name=name, value=raw))
    filters = requested_attribute_filters(
        request.query_params.multi_items(),
        resource_type=resource_type,
        declared=lookup.surface.filter_attributes_for(resource_type),
    )
    _require_answerable_parameters(request, lookup, resource_type, searching=bool(tokens or contents))
    service_base = base_url(request)
    cap = requested_entry_cap(request.query_params.get(COUNT_PARAMETER))
    if lookup.typed_by_request and not lookup.tracked_entity_type_uids:
        # Every `_tag` named a type this resource is not served over, so the scope is empty and
        # nothing can be in it. Answered here rather than further in: an empty scope is an
        # unsatisfied query, and neither the instance nor the projection needs to be asked to
        # confirm that nobody is of no type.
        return bundle_response(service_base, resource_type, tuple(honored), [], cap)
    if not tokens and not contents:
        # A request naming values and no identifier is the register narrowed to who holds them, and
        # it is the listing rather than a search: it is paged, it states a total, and a client walks
        # it with the same links. A filter narrows a question about everybody; it does not name one.
        return await _listing_response(request, lookup, resource_type, service_base, tuple(honored), filters)
    if lookup.from_projection():
        return await _projection_search_response(
            lookup, resource_type, tokens, tuple(contents), service_base, tuple(honored), cap, filters
        )
    entities = await matching_entities(lookup, resource_type, tokens, filters)
    entries = _entries(entities, lookup.surface, resource_type, service_base)
    return bundle_response(service_base, resource_type, tuple(honored), entries, cap)

registered_tracked_entity(lookup, tracked_entity_uid) async

Read one entity of this register as the caller, or None where this register serves nobody under that UID.

The read and the type check together, because a caller that may not see somebody and a somebody of a type this resource is not served over are the same answer to whoever asked: no such person here. dhis2w_fhir_serve.routes.summary reads a subject through this, so a summary can never be assembled about a person the register itself would not serve.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
async def registered_tracked_entity(lookup: RegisterLookup, tracked_entity_uid: str) -> TrackerTrackedEntity | None:
    """Read one entity of this register as the caller, or None where this register serves nobody under that UID.

    The read and the type check together, because a caller that may not see somebody and a somebody
    of a type this resource is not served over are the same answer to whoever asked: no such person
    here. `dhis2w_fhir_serve.routes.summary` reads a subject through this, so a summary can never be
    assembled about a person the register itself would not serve.
    """
    entity = await _read(lookup.reader, tracked_entity_uid)
    return entity if entity is not None and _is_served_as(entity, lookup) else None

read_registered_entity(request, resource_type, tracked_entity_uid) async

Answer one entity by its DHIS2 tracked entity UID, which is what a search result links to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
async def read_registered_entity(request: Request, resource_type: str, tracked_entity_uid: str) -> Response:
    """Answer one entity by its DHIS2 tracked entity UID, which is what a search result links to."""
    lookup = await register_lookup(request, resource_type)
    entity = await registered_tracked_entity(lookup, tracked_entity_uid)
    if entity is None:
        raise NotFoundError(resource_type, tracked_entity_uid)
    registered = registered_entity_for(entity, lookup.surface.index, resource_type)
    return JSONResponse(
        content=registered.model_dump(mode="json", exclude_none=True, by_alias=True), media_type=FHIR_JSON_MEDIA_TYPE
    )

register_lookup(request, resource_type) async

What a lookup runs against, refusing every way this process serves none of it.

The config comes first: a project whose [serve.tracked_entities] serves nothing serves nothing however the process was started, so telling its operator to restart with --live would be advice that changes nothing. The resource comes last, because "this server does not serve Specimen" is only true of a process that serves the register at all.

register_reader is where the posture is answered, and under dhis2 it is also where a request carrying no credential is refused: what comes back is read as the caller, so a request with nobody to be is a 401 rather than a page read as the facade. It sits between the two because authenticating a caller comes before telling them what this guide publishes.

The index is built last, over the reader under [serve.search] backend = "dhis2" and over the projection under "projection". Under the first, the connection a search runs over is the connection a match is resolved back through and one request never uses two; under the second they are deliberately two things, because that split is the whole design - the projection says who matched and the instance says who may be seen.

THE READER IS REQUIRED UNDER EITHER BACKEND. A projection-served search still resolves every match live under the caller's own credentials, so a process with no instance behind it can no more answer a projection-served register than a live one - R9's posture (iii) makes the live read part of every answer rather than a fallback for when the projection is cold.

The types come last and they are per-request, because _tag is the one thing about a register lookup a client decides: the resource's own types, narrowed to the ones the request named. The unnarrowed set is what decides whether this server serves the resource at all - narrowing to nothing is an unsatisfied query, and serving no Specimen is a different fact stated earlier.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
async def register_lookup(request: Request, resource_type: str) -> RegisterLookup:
    """What a lookup runs against, refusing every way this process serves none of it.

    The config comes first: a project whose `[serve.tracked_entities]` serves nothing serves nothing
    however the process was started, so telling its operator to restart with `--live` would be advice
    that changes nothing. The resource comes last, because "this server does not serve `Specimen`" is
    only true of a process that serves the register at all.

    `register_reader` is where the posture is answered, and under `dhis2` it is also where a request
    carrying no credential is refused: what comes back is read as the caller, so a request with
    nobody to be is a 401 rather than a page read as the facade. It sits between the two because
    authenticating a caller comes before telling them what this guide publishes.

    The index is built last, over the reader under `[serve.search] backend = "dhis2"` and over the
    projection under `"projection"`. Under the first, the connection a search runs over is the
    connection a match is resolved back through and one request never uses two; under the second they
    are deliberately two things, because that split is the whole design - the projection says who
    matched and the instance says who may be seen.

    THE READER IS REQUIRED UNDER EITHER BACKEND. A projection-served search still resolves every match
    live under the caller's own credentials, so a process with no instance behind it can no more
    answer a projection-served register than a live one - R9's posture (iii) makes the live read part
    of every answer rather than a fallback for when the projection is cold.

    The types come last and they are per-request, because `_tag` is the one thing about a register
    lookup a client decides: the resource's own types, narrowed to the ones the request named. The
    unnarrowed set is what decides whether this server serves the resource at all - narrowing to
    nothing is an unsatisfied query, and serving no `Specimen` is a different fact stated earlier.
    """
    context = serve_context(request)
    surface = context.register_surface
    if not surface.tracked_entities.enabled:
        raise RegisterDisabledError(resource_type)
    reader = await register_reader(request)
    if reader is None:
        raise NotServedFromCompiledIgError(resource_type)
    if not surface.serves_tracked_entities():
        raise NoPublishedSubjectTypeError(resource_type)
    served = surface.tracked_entity_type_uids_for(resource_type)
    if not served:
        raise NotServedError(resource_type)
    tagged = _requested_type_uids(request, surface, served)
    backend = context.settings.search.backend
    store = projection_store(request) if backend is SearchBackend.PROJECTION else None
    return RegisterLookup(
        reader=reader,
        surface=surface,
        index=build_name_search_index(backend, reader=reader, store=store),
        store=store,
        tracked_entity_type_uids=served if tagged is None else tagged,
        typed_by_request=tagged is not None,
    )

matching_entities(lookup, resource_type, tokens, filters=()) async

Fold every token's matches into one result set, in the order they were found, once per entity.

A value filter narrows the fold rather than the searches inside it, and it is read off the record each search already carried back: an identifier search resolves every match live before it hands it over, so what the person holds is in hand and a second query per attribute would ask the instance what this already knows. Membership is therefore as of now here, exactly as the rest of a live answer is - where a projection-served search decides membership as of its cursor and says so in the searchset.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/register.py
async def matching_entities(
    lookup: RegisterLookup,
    resource_type: str,
    tokens: tuple[IdentifierToken, ...] | list[IdentifierToken],
    filters: tuple[AttributeFilter, ...] = (),
) -> list[TrackerTrackedEntity]:
    """Fold every token's matches into one result set, in the order they were found, once per entity.

    A value filter narrows the fold rather than the searches inside it, and it is read off the record
    each search already carried back: an identifier search resolves every match live before it hands
    it over, so what the person holds is in hand and a second query per attribute would ask the
    instance what this already knows. Membership is therefore as of now here, exactly as the rest of
    a live answer is - where a projection-served search decides membership as of its cursor and says
    so in the searchset.
    """
    found: dict[str, TrackerTrackedEntity] = {}
    for token in tokens:
        for entity in await _entities_for_token(lookup, resource_type, token):
            if entity.trackedEntity is None or not _is_served_as(entity, lookup):
                continue
            if not holds_every_filter(attribute_values(entity, lookup.surface.index), filters):
                continue
            found.setdefault(entity.trackedEntity, entity)
    return list(found.values())

The projection seams

Reach for these when you are putting something other than the DHIS2 instance behind a register search, or holding a copy of an instance as FHIR resources. NameSearchIndex is what every register search runs through: it answers with tracked entity identifiers and scores and never with records, so the record behind a match is read back live, under the caller's own credentials, and DHIS2 authorizes each disclosure exactly as it does today. ProjectionStore is the durable document backend beside it, written by a sync and by nothing else. The design is the materialized projection, sections 7 and 9.

Two backends of each ship. Dhis2NameSearchIndex is the instance itself, which is what [serve.search] backend = "dhis2" selects and what a server that states no [serve.search] runs; it improves nothing over the search a live run has always run, and proving the seam is its whole job. SqliteProjectionStore and SqliteNameSearchIndex are the other half - one file under the project, selected together by [serve.projection] store = "sqlite" and [serve.search] backend = "projection".

base

The two Protocols a projection is served over, and the models they speak in.

ProjectionStore is the durable document backend: the mapped scope of a DHIS2 instance held as FHIR resources, filled by a sync and by nothing else. NameSearchIndex is the lookup beside it: it finds candidates and it answers with identifiers. Both are async, because everything in the serve layer is.

WHY THEY ARE TWO PROTOCOLS AND NOT ONE. They are separately adoptable, separately backed, and - this is the sharp one - separately safe. A store holds records, so serving an answer out of one means this facade has decided the caller may see it. An index holds no records, so it can do its whole job while disclosing nothing but the fact that some identifier matched.

AUTHORIZATION BY CONSTRUCTION, WHICH IS WHY NameMatch CARRIES SO LITTLE. DHIS2 enforces sharing, organisation-unit scope, and tracker ownership in the request path, and a projection takes DHIS2 out of the request path. docs/fhir/design/projection.md R9 is the posture that answer forces: an index discloses a tracked entity UID and how well it matched, and resolving one of those UIDs into a record goes back through the live, caller-credentialed read - register.wire.fetch_tracked_entity, under the caller's own credentials, exactly as a register read runs today. So DHIS2 decides, per UID, per caller, what may be seen; a caller who may not see somebody gets nothing back from the read and learns from the search only that an identifier exists. Nothing in this module may widen that: a field added to NameMatch is a disclosure this facade makes without asking DHIS2, and there is no place in the design where that is the right thing to add.

WHERE THAT LINE ACTUALLY FALLS, NOW THAT A STORE EXISTS. The store holds whole records, because a document backend that held less could rebuild neither its own index nor the evaluations of steps 8 and 9 without re-reading the instance. What it never does is hand one over on its own authority: a ProjectionStore answer names candidates, and routes/register reads each one back live under the caller's credentials before it reaches a Bundle. So the boundary is at the response and not at the row - which is R9's posture (iii) exactly, and section 6's one rule regardless of posture: the projection stores what a configured BUILD identity could read, and the facade never lets a caller's own identity imply more than that.

THE STORE'S DOCTRINE, in one line each, from section 4 of the same paper. A projection is derived and its sync is the only thing that writes it; it is rebuildable from zero as a routine operation; every row is cursor-stamped and every answer served from it states the cursor it was read at; and DHIS2 stays the record for everything DHIS2 can hold.

Classes

ProjectionEndpoint

Bases: StrEnum

Which DHIS2 tracker collection a watermark belongs to.

One watermark per collection rather than one for the projection, because docs/fhir/design/projection.md section 3.4 measured them as independent polls with independent tombstone behaviour, and 5.2 rule 3 draws the conclusion: a single global cursor would be a guess about which endpoint's clock leads.

Two, and /api/tracker/events is deliberately not a third. A projected register resource carries identity and attribute values (register.projection), and an event carries neither - so an event that moved is not a change to anything this projection holds, and polling for one would be a request per interval spent to learn nothing. The event watermark arrives with the resources that need it, which is steps 8 and 9 of that paper. A member that parses and then has nothing to poll for is the shape SearchBackend refuses to reserve, and this refuses it for the same reason.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionEndpoint(StrEnum):
    """Which DHIS2 tracker collection a watermark belongs to.

    One watermark per collection rather than one for the projection, because
    `docs/fhir/design/projection.md` section 3.4 measured them as independent polls with independent
    tombstone behaviour, and 5.2 rule 3 draws the conclusion: a single global cursor would be a guess
    about which endpoint's clock leads.

    Two, and `/api/tracker/events` is deliberately not a third. A projected register resource carries
    identity and attribute values (`register.projection`), and an event carries neither - so an event
    that moved is not a change to anything this projection holds, and polling for one would be a
    request per interval spent to learn nothing. The event watermark arrives with the resources that
    need it, which is steps 8 and 9 of that paper. A member that parses and then has nothing to poll
    for is the shape `SearchBackend` refuses to reserve, and this refuses it for the same reason.
    """

    #: `/api/tracker/trackedEntities` - the collection the projected resources come from.
    TRACKED_ENTITIES = "trackedEntities"

    #: `/api/tracker/enrollments` - polled for whose enrollment moved, never projected on its own.
    #:
    #: A tracked entity's own `lastUpdated` does not move when an enrollment of theirs does, and an
    #: enrollment carries program-level attribute values that the projected resource does carry. So
    #: this poll says whose projection has gone stale, and the rows it finds are re-materialized from
    #: the tracked entity read rather than mapped here.
    ENROLLMENTS = "enrollments"

ProjectionCursor

Bases: BaseModel

How far a projection has been filled: the instant every change up to it has been read.

A sync advances it in the same write as the batch it describes, which is what makes "the watermark never runs ahead of the rows" a property of the store rather than a convention a caller has to honour. updated_at is None on a projection nothing has been written into yet.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionCursor(BaseModel):
    """How far a projection has been filled: the instant every change up to it has been read.

    A sync advances it in the same write as the batch it describes, which is what makes "the
    watermark never runs ahead of the rows" a property of the store rather than a convention a
    caller has to honour. `updated_at` is None on a projection nothing has been written into yet.
    """

    model_config = ConfigDict(frozen=True)

    updated_at: datetime | None = None

ProjectedResourceKey

Bases: BaseModel

Which resource one projection row is: the type it is served under, and the id it is served at.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectedResourceKey(BaseModel):
    """Which resource one projection row is: the type it is served under, and the id it is served at."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    resource_id: str

ProjectedResource

Bases: BaseModel

One FHIR resource a projection holds, and the cursor the sync wrote it at.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectedResource(BaseModel):
    """One FHIR resource a projection holds, and the cursor the sync wrote it at."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    resource_id: str
    cursor: ProjectionCursor = Field(default_factory=ProjectionCursor)

    tracked_entity_type_uid: str | None = None
    """Which DHIS2 tracked entity type this resource is, where the entity it came from stated one.

    One FHIR resource type is served over every tracked entity type the published map takes onto it,
    so `resource_type` alone cannot answer "which of these are fridges" - two types both published as
    `Device` are one register and two kinds of thing. The document already states it as a `meta.tag`
    (`register.projection`); this is the same fact as a column, so a page narrowed to one type is one
    indexed query rather than a page read and then thinned.
    """

    body: dict[str, Any] = Field(default_factory=dict)
    """The FHIR document verbatim.

    The same HTTP/JSON-boundary escape hatch `StoreEntry.body` documents, and for the same reason: a
    store parses just enough of a document to index it and passes the rest through untouched, so a
    resource this toolkit has no model for is still a resource the projection can hold.
    """
Attributes
tracked_entity_type_uid = None class-attribute instance-attribute

Which DHIS2 tracked entity type this resource is, where the entity it came from stated one.

One FHIR resource type is served over every tracked entity type the published map takes onto it, so resource_type alone cannot answer "which of these are fridges" - two types both published as Device are one register and two kinds of thing. The document already states it as a meta.tag (register.projection); this is the same fact as a column, so a page narrowed to one type is one indexed query rather than a page read and then thinned.

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

The FHIR document verbatim.

The same HTTP/JSON-boundary escape hatch StoreEntry.body documents, and for the same reason: a store parses just enough of a document to index it and passes the rest through untouched, so a resource this toolkit has no model for is still a resource the projection can hold.

ProjectionWatermarks

Bases: BaseModel

How far each polled tracker collection has been read, one instant apiece.

Both are None on a projection nothing has been synced into. cursor folds them into the one instant an answer states, and it folds them by taking the EARLIER: a projection is as current as its least current half, and stating the later one would be claiming to know about changes at a collection nobody has polled that far.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionWatermarks(BaseModel):
    """How far each polled tracker collection has been read, one instant apiece.

    Both are None on a projection nothing has been synced into. `cursor` folds them into the one
    instant an answer states, and it folds them by taking the EARLIER: a projection is as current as
    its least current half, and stating the later one would be claiming to know about changes at a
    collection nobody has polled that far.
    """

    model_config = ConfigDict(frozen=True)

    tracked_entities: datetime | None = None
    enrollments: datetime | None = None

    def at(self, endpoint: ProjectionEndpoint) -> datetime | None:
        """How far one collection has been read, or None where nothing has read it yet."""
        match endpoint:
            case ProjectionEndpoint.TRACKED_ENTITIES:
                return self.tracked_entities
            case ProjectionEndpoint.ENROLLMENTS:
                return self.enrollments

    def cursor(self) -> ProjectionCursor:
        """The one instant every answer served from this projection states - the earlier of the two."""
        marks = [mark for mark in (self.tracked_entities, self.enrollments) if mark is not None]
        return ProjectionCursor() if len(marks) < 2 else ProjectionCursor(updated_at=min(marks))
Methods:
at(endpoint)

How far one collection has been read, or None where nothing has read it yet.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
def at(self, endpoint: ProjectionEndpoint) -> datetime | None:
    """How far one collection has been read, or None where nothing has read it yet."""
    match endpoint:
        case ProjectionEndpoint.TRACKED_ENTITIES:
            return self.tracked_entities
        case ProjectionEndpoint.ENROLLMENTS:
            return self.enrollments
cursor()

The one instant every answer served from this projection states - the earlier of the two.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
def cursor(self) -> ProjectionCursor:
    """The one instant every answer served from this projection states - the earlier of the two."""
    marks = [mark for mark in (self.tracked_entities, self.enrollments) if mark is not None]
    return ProjectionCursor() if len(marks) < 2 else ProjectionCursor(updated_at=min(marks))

ProjectionQuery

Bases: BaseModel

What a caller is asking a projection for: which resources, which identifiers, and how many.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionQuery(BaseModel):
    """What a caller is asking a projection for: which resources, which identifiers, and how many."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    identifiers: tuple[str, ...] = ()
    identifier_systems: tuple[str, ...] = ()
    """The systems those identifiers are looked for under; empty looks under every system held.

    Two questions FHIR spells with one parameter. `identifier=<system>|<value>` says which key the
    value is, and `identifier=<value>` says the caller does not know - so a query naming systems is
    the first and a query naming none is the second, and neither is a filter the other can express.
    """

    tracked_entity_type_uids: tuple[str, ...] = ()
    """Which tracked entity types the answer is narrowed to; empty asks about every type held.

    This is what `_tag` on a register search becomes. Narrowing in the store rather than after it is
    what keeps a narrowed page a full page: thinning a page of the whole resource down to one type
    would hand back a short page with more of that type still behind it.
    """

    attribute_values: tuple[AttributeFilter, ...] = ()
    """The attribute-value equalities the answer is narrowed by; every one of them has to hold.

    This is what `d2-attribute` on a register search becomes, and the projection can answer it
    because the sync already indexed every attribute value every entity holds - the index `_content`
    reads is keyed by the attribute the value belongs to, so filtering by one attribute is that same
    index read with one predicate more. Narrowing in the store rather than after it, for the reason
    `tracked_entity_type_uids` gives: a page thinned afterwards is a short page with more behind it.
    """

    offset: int = 0
    """How many resources to skip before the page starts, which is how the listing walks the store."""

    count: int | None = None
    """How many resources the caller will read, or None for as many as the backend carries."""
Attributes
identifier_systems = () class-attribute instance-attribute

The systems those identifiers are looked for under; empty looks under every system held.

Two questions FHIR spells with one parameter. identifier=<system>|<value> says which key the value is, and identifier=<value> says the caller does not know - so a query naming systems is the first and a query naming none is the second, and neither is a filter the other can express.

tracked_entity_type_uids = () class-attribute instance-attribute

Which tracked entity types the answer is narrowed to; empty asks about every type held.

This is what _tag on a register search becomes. Narrowing in the store rather than after it is what keeps a narrowed page a full page: thinning a page of the whole resource down to one type would hand back a short page with more of that type still behind it.

attribute_values = () class-attribute instance-attribute

The attribute-value equalities the answer is narrowed by; every one of them has to hold.

This is what d2-attribute on a register search becomes, and the projection can answer it because the sync already indexed every attribute value every entity holds - the index _content reads is keyed by the attribute the value belongs to, so filtering by one attribute is that same index read with one predicate more. Narrowing in the store rather than after it, for the reason tracked_entity_type_uids gives: a page thinned afterwards is a short page with more behind it.

offset = 0 class-attribute instance-attribute

How many resources to skip before the page starts, which is how the listing walks the store.

count = None class-attribute instance-attribute

How many resources the caller will read, or None for as many as the backend carries.

ProjectionPage

Bases: BaseModel

One page of a projection search: the resources on it, how many there are, and when it was read.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionPage(BaseModel):
    """One page of a projection search: the resources on it, how many there are, and when it was read."""

    model_config = ConfigDict(frozen=True)

    resources: tuple[ProjectedResource, ...] = ()
    total: int | None = None
    """How many the whole result set holds, or None where the backend stated no total."""

    cursor: ProjectionCursor = Field(default_factory=ProjectionCursor)
    """The instant this page is as of, which every answer served from a projection states."""
Attributes
total = None class-attribute instance-attribute

How many the whole result set holds, or None where the backend stated no total.

cursor = Field(default_factory=ProjectionCursor) class-attribute instance-attribute

The instant this page is as of, which every answer served from a projection states.

IndexedName

Bases: BaseModel

One name an index holds for one tracked entity, in the script the instance stores it in.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class IndexedName(BaseModel):
    """One name an index holds for one tracked entity, in the script the instance stores it in."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str
    attribute_uid: str
    value: str
    tracked_entity_type_uid: str | None = None

ProjectionBatch

Bases: BaseModel

One sync's worth of writes: the rows it maps, the rows it removes, and the cursor it advances to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class ProjectionBatch(BaseModel):
    """One sync's worth of writes: the rows it maps, the rows it removes, and the cursor it advances to."""

    model_config = ConfigDict(frozen=True)

    resources: tuple[ProjectedResource, ...] = ()
    removed: tuple[ProjectedResourceKey, ...] = ()
    """What DHIS2 no longer holds. A tombstone means "remove the row", never "keep the last state"."""

    names: tuple[IndexedName, ...] = ()
    """The search keys the resources on this batch carry, written in the same transaction they are.

    An index that advanced separately from the documents it describes would be a second watermark
    with a second way of being wrong, and the whole reason the batch is one write is that there is
    exactly one instant at which the projection is true.
    """

    endpoint: ProjectionEndpoint | None = None
    """Which collection's watermark this batch advances, or None for a batch that advances none.

    Per-endpoint because the three collections are polled independently
    (`docs/fhir/design/projection.md` section 5.2, rule 3). None is the honest value for a batch
    written to re-materialize entities an enrollment poll found stale: it carries rows, it advances
    no watermark of its own, and the poll that found them advances theirs when it finishes.
    """

    cursor: ProjectionCursor = Field(default_factory=ProjectionCursor)
Attributes
removed = () class-attribute instance-attribute

What DHIS2 no longer holds. A tombstone means "remove the row", never "keep the last state".

names = () class-attribute instance-attribute

The search keys the resources on this batch carry, written in the same transaction they are.

An index that advanced separately from the documents it describes would be a second watermark with a second way of being wrong, and the whole reason the batch is one write is that there is exactly one instant at which the projection is true.

endpoint = None class-attribute instance-attribute

Which collection's watermark this batch advances, or None for a batch that advances none.

Per-endpoint because the three collections are polled independently (docs/fhir/design/projection.md section 5.2, rule 3). None is the honest value for a batch written to re-materialize entities an enrollment poll found stale: it carries rows, it advances no watermark of its own, and the poll that found them advances theirs when it finishes.

NameQuery

Bases: BaseModel

One lookup put to an index: the value typed, and how wide the index may look for it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class NameQuery(BaseModel):
    """One lookup put to an index: the value typed, and how wide the index may look for it."""

    model_config = ConfigDict(frozen=True)

    value: str
    attribute_uids: tuple[str, ...] = ()
    """The attributes the value is looked for under; empty asks the backend to use every key it holds."""

    tracked_entity_type_uids: tuple[str, ...] = ()
    """The tracked entity types in scope; empty asks the backend to look across every type it holds."""

    limit: int | None = None
    """How many candidates the caller will read, or None for every one the backend finds.

    The register asks for None: a searchset states what was found rather than a page of it, and a
    lookup that quietly dropped the fifty-first person holding a value would be an answer that
    contradicts the question.
    """
Attributes
attribute_uids = () class-attribute instance-attribute

The attributes the value is looked for under; empty asks the backend to use every key it holds.

tracked_entity_type_uids = () class-attribute instance-attribute

The tracked entity types in scope; empty asks the backend to look across every type it holds.

limit = None class-attribute instance-attribute

How many candidates the caller will read, or None for every one the backend finds.

The register asks for None: a searchset states what was found rather than a page of it, and a lookup that quietly dropped the fifty-first person holding a value would be an answer that contradicts the question.

NameMatch

Bases: BaseModel

One candidate an index found: which tracked entity, and how well it matched.

Nothing else, deliberately. No attribute value, no organisation unit, no enrollment - see this module's docstring on authorization by construction. display_name is the label a candidate list shows where the backend holds one; the dhis2 backend holds none, because the instance states no display name for a tracked entity and assembling one out of attribute values would be this index disclosing exactly what it exists not to disclose.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class NameMatch(BaseModel):
    """One candidate an index found: which tracked entity, and how well it matched.

    Nothing else, deliberately. No attribute value, no organisation unit, no enrollment - see this
    module's docstring on authorization by construction. `display_name` is the label a candidate list
    shows where the backend holds one; the `dhis2` backend holds none, because the instance states no
    display name for a tracked entity and assembling one out of attribute values would be this index
    disclosing exactly what it exists not to disclose.
    """

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str
    display_name: str | None = None
    score: float = 1.0

NameMatches

Bases: BaseModel

What an index answers a lookup with: the candidates, in the order it ranked them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
class NameMatches(BaseModel):
    """What an index answers a lookup with: the candidates, in the order it ranked them."""

    model_config = ConfigDict(frozen=True)

    matches: tuple[NameMatch, ...] = ()

    cursor: ProjectionCursor | None = None
    """When these matches are as of, and None where they were read live.

    A live answer states no cursor because there is no watermark to state: it is as of the moment
    the instance answered. An index backend fills this in, and the register's answer says so.
    """
Attributes
cursor = None class-attribute instance-attribute

When these matches are as of, and None where they were read live.

A live answer states no cursor because there is no watermark to state: it is as of the moment the instance answered. An index backend fills this in, and the register's answer says so.

ProjectionStore

Bases: Protocol

Holds the FHIR projection of a DHIS2 instance, written only by sync.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
@runtime_checkable
class ProjectionStore(Protocol):
    """Holds the FHIR projection of a DHIS2 instance, written only by sync."""

    async def read(self, resource_type: str, resource_id: str) -> ProjectedResource | None:
        """Read one projected resource, or None where the projection holds none under that id."""
        ...

    async def search(self, query: ProjectionQuery) -> ProjectionPage:
        """Answer one page of the projection, stating the cursor it was read at."""
        ...

    async def write(self, batch: ProjectionBatch) -> ProjectionCursor:
        """Write one sync's batch and answer the cursor the projection now stands at."""
        ...

    async def cursor(self) -> ProjectionCursor:
        """How far this projection has been filled, which every answer served from it states."""
        ...

    async def watermarks(self) -> ProjectionWatermarks:
        """How far each tracker collection has been read, which is what the next poll asks from."""
        ...

    async def rebuild(self) -> None:
        """Empty the projection so a full materialization can fill it from zero."""
        ...
Methods:
read(resource_type, resource_id) async

Read one projected resource, or None where the projection holds none under that id.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def read(self, resource_type: str, resource_id: str) -> ProjectedResource | None:
    """Read one projected resource, or None where the projection holds none under that id."""
    ...
search(query) async

Answer one page of the projection, stating the cursor it was read at.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def search(self, query: ProjectionQuery) -> ProjectionPage:
    """Answer one page of the projection, stating the cursor it was read at."""
    ...
write(batch) async

Write one sync's batch and answer the cursor the projection now stands at.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def write(self, batch: ProjectionBatch) -> ProjectionCursor:
    """Write one sync's batch and answer the cursor the projection now stands at."""
    ...
cursor() async

How far this projection has been filled, which every answer served from it states.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def cursor(self) -> ProjectionCursor:
    """How far this projection has been filled, which every answer served from it states."""
    ...
watermarks() async

How far each tracker collection has been read, which is what the next poll asks from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def watermarks(self) -> ProjectionWatermarks:
    """How far each tracker collection has been read, which is what the next poll asks from."""
    ...
rebuild() async

Empty the projection so a full materialization can fill it from zero.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def rebuild(self) -> None:
    """Empty the projection so a full materialization can fill it from zero."""
    ...

NameSearchIndex

Bases: Protocol

Finds candidate tracked entity identifiers by name, across scripts.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
@runtime_checkable
class NameSearchIndex(Protocol):
    """Finds candidate tracked entity identifiers by name, across scripts."""

    async def index(self, entries: Sequence[IndexedName]) -> None:
        """Hold these names, replacing whatever was held for the tracked entities they name."""
        ...

    async def find(self, query: NameQuery) -> NameMatches:
        """Answer the candidates one lookup matched - identifiers and scores, never records."""
        ...

    async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
        """Drop everything held for these tracked entities, which is what a tombstone means here."""
        ...
Methods:
index(entries) async

Hold these names, replacing whatever was held for the tracked entities they name.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def index(self, entries: Sequence[IndexedName]) -> None:
    """Hold these names, replacing whatever was held for the tracked entities they name."""
    ...
find(query) async

Answer the candidates one lookup matched - identifiers and scores, never records.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def find(self, query: NameQuery) -> NameMatches:
    """Answer the candidates one lookup matched - identifiers and scores, never records."""
    ...
forget(tracked_entity_uids) async

Drop everything held for these tracked entities, which is what a tombstone means here.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/base.py
async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
    """Drop everything held for these tracked entities, which is what a tombstone means here."""
    ...

dhis2_names

The dhis2 backend of NameSearchIndex: the instance itself, asked one filtered search at a time.

IT IMPROVES NOTHING, AND THAT IS THE POINT. find runs exactly the search a live register has always run - filter=<attribute>:eq:<value> against /api/tracker/trackedEntities, one query per key per tracked entity type, orgUnitMode=ACCESSIBLE on every one of them - so it is exactly as weak as an exact match is, and it carries exactly today's authorization properties. Its whole value is that once a lookup is the only path a register search takes, swapping in a real index is a config line rather than a refactor. docs/fhir/design/projection.md section 7.2 is the design, and R5 is the reason this backend is built first: a seam nothing has crossed is not a seam.

WHOSE CREDENTIALS IT READS UNDER. Whatever the RegisterReader it was handed reads under, which is settled per request rather than per process: the caller's own Authorization under [serve] auth = "dhis2", and the runtime's client otherwise. dhis2w_fhir_serve.passthrough is where that is decided, and nothing here branches on the answer.

WHAT IT DISCLOSES, AND WHAT IT DOES NOT. A match is a tracked entity UID and a score. The record behind one is not this index's to hand over: the register resolves each match through register.wire.fetch_tracked_entity, under the same credentials, so DHIS2 authorizes the disclosure per match per caller - docs/fhir/design/projection.md R9. This backend therefore reads more than it discloses, because the tracker endpoint answers a filtered search with whole entities and this index keeps their identifiers alone. Narrowing that projection is an optimisation the index backends make moot, and it is not what proves the seam.

Classes

Dhis2NameSearchIndex

Bases: BaseModel

Finds candidate tracked entities by asking the DHIS2 instance, one exact-match search per key.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/dhis2_names.py
class Dhis2NameSearchIndex(BaseModel):
    """Finds candidate tracked entities by asking the DHIS2 instance, one exact-match search per key."""

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    reader: RegisterReader
    """What this lookup reads DHIS2 through - the caller's own credentials, or the runtime's client."""

    async def index(self, entries: Sequence[IndexedName]) -> None:
        """Hold nothing: this backend is the instance, and DHIS2 is the only thing that writes it."""
        return None

    async def find(self, query: NameQuery) -> NameMatches:
        """Answer the tracked entities one value matches exactly, one query per key per type.

        Attribute-major, type-minor, first match first: the order a register search folds its result
        set in, and the order a client sees the entries in. A tracked entity matched under two keys
        is one candidate, because a person holding the same value twice is one person. No cursor is
        stated, because a live answer is as of the moment the instance answered it.

        A KEY DHIS2 REFUSES MATCHED NOBODY. The fan-out is one query per key, and the keys are what
        DHIS2 declares unique or searchable rather than a set this facade chose - so a value the
        instance will not compare against one of them (a name put to a NUMBER key, a value outside
        an attribute's own constraint) draws a 400 on that query alone. That is one key answering
        nothing, and the other keys still answer: a search is not a transaction, and refusing the
        whole lookup would put the instance's own key set between a person and their own record.
        `dhis2w_fhir_serve.register.index.PublishedAttribute.can_hold` screens the keys whose
        declared value type settles it before a request is spent; this is what catches the rest.
        """
        found: dict[str, NameMatch] = {}
        for attribute_uid in query.attribute_uids:
            for tracked_entity_type_uid in query.tracked_entity_type_uids:
                for entity in await self._matches_under_key(query, attribute_uid, tracked_entity_type_uid):
                    if entity.trackedEntity is not None:
                        found.setdefault(entity.trackedEntity, NameMatch(tracked_entity_uid=entity.trackedEntity))
        matches = tuple(found.values())
        return NameMatches(matches=matches if query.limit is None else matches[: query.limit])

    async def _matches_under_key(
        self, query: NameQuery, attribute_uid: str, tracked_entity_type_uid: str
    ) -> list[TrackerTrackedEntity]:
        """The entities of one type holding the value under one key, empty where DHIS2 refuses the key."""
        try:
            return await search_tracked_entities(
                self.reader,
                tracked_entity_type_uid=tracked_entity_type_uid,
                attribute_uid=attribute_uid,
                value=query.value,
            )
        except Dhis2ApiError as error:
            if error.status_code != 400:
                raise
            logger.info(
                "register search: DHIS2 refused the key %s for the value %r, which matches nobody under it: %s",
                attribute_uid,
                query.value,
                error,
            )
            return []

    async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
        """Drop nothing: an entity DHIS2 no longer holds is one this backend stops finding by itself."""
        return None
Attributes
reader instance-attribute

What this lookup reads DHIS2 through - the caller's own credentials, or the runtime's client.

Methods:
index(entries) async

Hold nothing: this backend is the instance, and DHIS2 is the only thing that writes it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/dhis2_names.py
async def index(self, entries: Sequence[IndexedName]) -> None:
    """Hold nothing: this backend is the instance, and DHIS2 is the only thing that writes it."""
    return None
find(query) async

Answer the tracked entities one value matches exactly, one query per key per type.

Attribute-major, type-minor, first match first: the order a register search folds its result set in, and the order a client sees the entries in. A tracked entity matched under two keys is one candidate, because a person holding the same value twice is one person. No cursor is stated, because a live answer is as of the moment the instance answered it.

A KEY DHIS2 REFUSES MATCHED NOBODY. The fan-out is one query per key, and the keys are what DHIS2 declares unique or searchable rather than a set this facade chose - so a value the instance will not compare against one of them (a name put to a NUMBER key, a value outside an attribute's own constraint) draws a 400 on that query alone. That is one key answering nothing, and the other keys still answer: a search is not a transaction, and refusing the whole lookup would put the instance's own key set between a person and their own record. dhis2w_fhir_serve.register.index.PublishedAttribute.can_hold screens the keys whose declared value type settles it before a request is spent; this is what catches the rest.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/dhis2_names.py
async def find(self, query: NameQuery) -> NameMatches:
    """Answer the tracked entities one value matches exactly, one query per key per type.

    Attribute-major, type-minor, first match first: the order a register search folds its result
    set in, and the order a client sees the entries in. A tracked entity matched under two keys
    is one candidate, because a person holding the same value twice is one person. No cursor is
    stated, because a live answer is as of the moment the instance answered it.

    A KEY DHIS2 REFUSES MATCHED NOBODY. The fan-out is one query per key, and the keys are what
    DHIS2 declares unique or searchable rather than a set this facade chose - so a value the
    instance will not compare against one of them (a name put to a NUMBER key, a value outside
    an attribute's own constraint) draws a 400 on that query alone. That is one key answering
    nothing, and the other keys still answer: a search is not a transaction, and refusing the
    whole lookup would put the instance's own key set between a person and their own record.
    `dhis2w_fhir_serve.register.index.PublishedAttribute.can_hold` screens the keys whose
    declared value type settles it before a request is spent; this is what catches the rest.
    """
    found: dict[str, NameMatch] = {}
    for attribute_uid in query.attribute_uids:
        for tracked_entity_type_uid in query.tracked_entity_type_uids:
            for entity in await self._matches_under_key(query, attribute_uid, tracked_entity_type_uid):
                if entity.trackedEntity is not None:
                    found.setdefault(entity.trackedEntity, NameMatch(tracked_entity_uid=entity.trackedEntity))
    matches = tuple(found.values())
    return NameMatches(matches=matches if query.limit is None else matches[: query.limit])
forget(tracked_entity_uids) async

Drop nothing: an entity DHIS2 no longer holds is one this backend stops finding by itself.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/dhis2_names.py
async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
    """Drop nothing: an entity DHIS2 no longer holds is one this backend stops finding by itself."""
    return None

Functions:

factory

Which backend a projection is held in and a register search runs through, chosen from fhir.toml.

Two match statements over two config enums, rather than a registry, for the reason docs/fhir/design/projection.md section 7 gives for following AuthProvider: a backend a deployment has not installed should be a refusal the operator reads at the config key that asked for it, not an import error from inside a request.

build_projection_store answers what [serve.projection] store names, and None where it names nothing - which is the zero-ops default and the posture R11 says must stay the product rather than become a degraded mode. build_name_search_index answers what [serve.search] backend names, over the connection a live search reads and the store a synced one reads.

Classes

Functions:

projection_path(config, *, project_root)

Where this project's projection lives - relative to the project root unless it is absolute.

The same rule [serve] spool_dir follows, and stated once here so the process that fills the projection and the process that serves it can never land on two different files.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/factory.py
def projection_path(config: ProjectionConfig, *, project_root: Path) -> Path:
    """Where this project's projection lives - relative to the project root unless it is absolute.

    The same rule `[serve] spool_dir` follows, and stated once here so the process that fills the
    projection and the process that serves it can never land on two different files.
    """
    stated = Path(config.path)
    return stated if stated.is_absolute() else project_root / stated

build_projection_store(config, *, project_root)

Open the store [serve.projection] store names, or None where this project holds no projection.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/factory.py
def build_projection_store(config: ProjectionConfig, *, project_root: Path) -> ProjectionStore | None:
    """Open the store `[serve.projection] store` names, or None where this project holds no projection."""
    match config.store:
        case ProjectionBackend.NONE:
            return None
        case ProjectionBackend.SQLITE:
            return SqliteProjectionStore(projection_path(config, project_root=project_root))

build_name_search_index(backend, *, reader, store=None)

Build the index one register search runs through, over the connection or the store it reads.

The projection backend is refused when this process holds no store, rather than answered as the instance: a server told to search a projection and quietly searching DHIS2 instead would answer every lookup with a different search than the one its fhir.toml states, and the caller reading the cursor would find none. fhir.toml refuses that pair before the socket opens; this refusal is what a runtime assembled by hand meets.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/factory.py
def build_name_search_index(
    backend: SearchBackend, *, reader: RegisterReader, store: ProjectionStore | None = None
) -> NameSearchIndex:
    """Build the index one register search runs through, over the connection or the store it reads.

    The `projection` backend is refused when this process holds no store, rather than answered as the
    instance: a server told to search a projection and quietly searching DHIS2 instead would answer
    every lookup with a different search than the one its `fhir.toml` states, and the caller reading
    the cursor would find none. `fhir.toml` refuses that pair before the socket opens; this refusal is
    what a runtime assembled by hand meets.
    """
    match backend:
        case SearchBackend.DHIS2:
            return Dhis2NameSearchIndex(reader=reader)
        case SearchBackend.PROJECTION:
            if not isinstance(store, SqliteProjectionStore):
                raise ProjectionNotConfiguredError
            return SqliteNameSearchIndex(store)

The materialized projection

Reach for these when you are filling a projection, reading one, or serving an answer out of one. SqliteProjectionStore is the reference implementation of ProjectionStore and the Embedded posture's whole backend: SQLAlchemy over aiosqlite, one file, no service. run_sync is what fills it - the initial materialization, the incremental updatedAfter poll with includeDeleted=true as a constant (read again without it where DHIS2 2.42.6 refuses the flag, which SyncReport.tombstones_visible reports; BUGS.md #116), and the full rebuild - and SyncReport is what it answers with. SqliteNameSearchIndex is the search over its keys, and projection.serving is how an answer served from it states the instant it is as of.

What none of them does is decide who may read what. A projection-served answer names candidates; the record behind each one is read from the instance under the caller's own credentials, so DHIS2 authorizes every disclosure per match per caller. That is R9's recommended posture (iii), and each module docstring says where its half of it sits.

schema

The four tables a materialized projection is held in, and the one connection they are reached over.

docs/fhir/design/projection.md step 3 asks for SQLAlchemy over aiosqlite with typed Mapped[...] columns, which is CLAUDE.md rule 10's own default for state - and a projection is state in the exact sense that rule draws: derived, mutable, queried by predicate. The spool stayed as files on the other side of that line, because a receipt is an immutable artifact; this is the side the line was drawn to put something on.

WHAT IS TYPED AND WHAT IS NOT. The document is held as the wire JSON it was projected into, in one text column, and every dimension a query runs over is a column beside it: which resource it is, which identifiers name it, which instant it was written at. That is the split StoreEntry.body already states - parse just enough to index, pass the rest through untouched - and it is what lets the projection hold a resource this toolkit gains a model for tomorrow without a schema change today.

THE INSTANTS ARE THE INSTANCE'S OWN CLOCK, AND THEY ARE NAIVE ON PURPOSE. DHIS2 2.43 answers updatedAt as a zone-less wall-clock reading in the instance's own zone (BUGS.md 62), and a sync's watermark is compared against, and sent back as, exactly that reading. Stamping it with this host's zone would make the cursor a claim about a clock nobody consulted, and the first updatedAfter built from it would silently skip or re-read hours of rows. So the columns are naive, the values come from DHIS2 and never from datetime.now, and nothing here converts.

THERE IS NO MIGRATION AND THAT IS THE DESIGN. D3 makes rebuilding routine rather than a recovery step, so the way a schema change reaches a projection is d2w fhir sync --rebuild: the tables are created if they are absent, and a projection whose shape has moved on is refilled from the instance that is the record for all of it. A migration would be machinery for carrying forward a copy that is cheaper to make again.

Classes

ProjectionBase

Bases: DeclarativeBase

The declarative base every projection table is defined on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
class ProjectionBase(DeclarativeBase):
    """The declarative base every projection table is defined on."""

ProjectedResourceRow

Bases: ProjectionBase

One projected FHIR resource: what it is, when it was written, and the document verbatim.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
class ProjectedResourceRow(ProjectionBase):
    """One projected FHIR resource: what it is, when it was written, and the document verbatim."""

    __tablename__ = "projected_resource"

    resource_type: Mapped[str] = mapped_column(String, primary_key=True)
    resource_id: Mapped[str] = mapped_column(String, primary_key=True)
    updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
    body: Mapped[str] = mapped_column(Text, nullable=False)

    tracked_entity_type_uid: Mapped[str | None] = mapped_column(String, nullable=True)
    """Which DHIS2 tracked entity type the row is, so one resource's register narrows to one type.

    Nullable because the instance may state none, and a row whose type nothing states is a row no
    query naming a type can honestly claim - which is the same reading the live path takes of an
    entity carrying no `trackedEntityType`.
    """

    __table_args__ = (Index("ix_projected_resource_type", "resource_type", "tracked_entity_type_uid"),)
Attributes
tracked_entity_type_uid = mapped_column(String, nullable=True) class-attribute instance-attribute

Which DHIS2 tracked entity type the row is, so one resource's register narrows to one type.

Nullable because the instance may state none, and a row whose type nothing states is a row no query naming a type can honestly claim - which is the same reading the live path takes of an entity carrying no trackedEntityType.

ProjectedIdentifierRow

Bases: ProjectionBase

One identifier entry of one projected resource, as the token search matches it.

Every value of Resource.identifier[] lands here, system and all, so a system|value token and a bare value are the same index read with one predicate more or less. The row is a copy of what the document already says - the document stays the answer, and this is only how it is found.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
class ProjectedIdentifierRow(ProjectionBase):
    """One `identifier` entry of one projected resource, as the token search matches it.

    Every value of `Resource.identifier[]` lands here, system and all, so a `system|value` token and
    a bare value are the same index read with one predicate more or less. The row is a copy of what
    the document already says - the document stays the answer, and this is only how it is found.
    """

    __tablename__ = "projected_identifier"

    resource_type: Mapped[str] = mapped_column(String, primary_key=True)
    resource_id: Mapped[str] = mapped_column(String, primary_key=True)
    system: Mapped[str] = mapped_column(String, primary_key=True, default=NO_IDENTIFIER_SYSTEM)
    value: Mapped[str] = mapped_column(String, primary_key=True)

    __table_args__ = (
        Index("ix_projected_identifier_value", "resource_type", "value"),
        Index("ix_projected_identifier_system_value", "resource_type", "system", "value"),
    )

ProjectedNameRow

Bases: ProjectionBase

One attribute value a tracked entity is searchable by, and the folded key a name search matches.

folded is value.casefold() and it is what makes a name search behave the way the :like: filter of docs/fhir/design/projection.md section 3.3 measured: a case-insensitive substring match, which is the finding that page tells any replacement index not to regress. Lao and Khmer are written without spaces, so a word-tokenising analyzer would find LESS than a substring does - the interior substring and the bare Khmer consonant both hit, and both keep hitting here.

What this column deliberately does not hold is a transliteration. Finding ສົມສັກ from Somsack is R8, it is an index-time ICU chain, and it arrives with the OpenSearch backend of step 6. A fold is honest about being a fold.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
class ProjectedNameRow(ProjectionBase):
    """One attribute value a tracked entity is searchable by, and the folded key a name search matches.

    `folded` is `value.casefold()` and it is what makes a name search behave the way the `:like:`
    filter of `docs/fhir/design/projection.md` section 3.3 measured: a case-insensitive substring
    match, which is the finding that page tells any replacement index not to regress. Lao and Khmer
    are written without spaces, so a word-tokenising analyzer would find LESS than a substring does -
    the interior substring and the bare Khmer consonant both hit, and both keep hitting here.

    What this column deliberately does not hold is a transliteration. Finding `ສົມສັກ` from `Somsack`
    is R8, it is an index-time ICU chain, and it arrives with the OpenSearch backend of step 6. A
    fold is honest about being a fold.
    """

    __tablename__ = "projected_name"

    tracked_entity_uid: Mapped[str] = mapped_column(String, primary_key=True)
    attribute_uid: Mapped[str] = mapped_column(String, primary_key=True)
    value: Mapped[str] = mapped_column(String, primary_key=True)
    folded: Mapped[str] = mapped_column(String, nullable=False)
    tracked_entity_type_uid: Mapped[str | None] = mapped_column(String, nullable=True)

    __table_args__ = (
        Index("ix_projected_name_folded", "folded"),
        Index("ix_projected_name_attribute_folded", "attribute_uid", "folded"),
    )

ProjectionWatermarkRow

Bases: ProjectionBase

How far one tracker collection has been read, which is what the next poll asks from.

One row per ProjectionEndpoint, never one row for the projection, because the polled collections move independently and a single global cursor would be a guess about which of their clocks leads (docs/fhir/design/projection.md section 5.2, rule 3).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
class ProjectionWatermarkRow(ProjectionBase):
    """How far one tracker collection has been read, which is what the next poll asks from.

    One row per `ProjectionEndpoint`, never one row for the projection, because the polled collections
    move independently and a single global cursor would be a guess about which of their clocks leads
    (`docs/fhir/design/projection.md` section 5.2, rule 3).
    """

    __tablename__ = "projection_watermark"

    endpoint: Mapped[str] = mapped_column(String, primary_key=True)
    updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)

Functions:

open_projection_engine(database_path)

Open the aiosqlite engine one projection file is reached over, without touching the file yet.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
def open_projection_engine(database_path: Path) -> AsyncEngine:
    """Open the aiosqlite engine one projection file is reached over, without touching the file yet."""
    return create_async_engine(f"sqlite+aiosqlite:///{database_path}", echo=False, future=True)

projection_sessions(engine)

The session factory every read and every write of a projection runs in, one transaction apiece.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
def projection_sessions(engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
    """The session factory every read and every write of a projection runs in, one transaction apiece."""
    return async_sessionmaker(engine, expire_on_commit=False)

create_projection_tables(engine) async

Create whatever of the four tables is absent, which is what opening an empty projection does.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
async def create_projection_tables(engine: AsyncEngine) -> None:
    """Create whatever of the four tables is absent, which is what opening an empty projection does."""
    async with engine.begin() as connection:
        await connection.run_sync(ProjectionBase.metadata.create_all)

recreate_projection_tables(engine) async

Drop the four tables and create them at the current schema - what a rebuild stands on.

create_all adds absent tables and never alters a present one, so a projection file written by an older schema keeps its old columns for ever. A rebuild refills from zero anyway, which is exactly when dropping the tables costs nothing - so the rebuild is also the schema migration, as the store's doctrine states.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/schema.py
async def recreate_projection_tables(engine: AsyncEngine) -> None:
    """Drop the four tables and create them at the current schema - what a rebuild stands on.

    `create_all` adds absent tables and never alters a present one, so a projection file written by
    an older schema keeps its old columns for ever. A rebuild refills from zero anyway, which is
    exactly when dropping the tables costs nothing - so the rebuild is also the schema migration,
    as the store's doctrine states.
    """
    async with engine.begin() as connection:
        await connection.run_sync(ProjectionBase.metadata.drop_all)
        await connection.run_sync(ProjectionBase.metadata.create_all)

sqlite_store

The SQLite backend of ProjectionStore: one file under the project, written only by a sync.

This is step 3 of docs/fhir/design/projection.md and it is the Embedded posture's whole backend - no service, no port, no operator, one file make install already gave you the driver for. It is also the reference implementation of the Protocol, which is the part that outlives it: every later backend is measured against what this one does, and every test of the sync and of the serving path runs against it.

THE THREE CORRECTNESS RULES, EACH IN ONE PLACE HERE.

  1. The watermark advances only when the rows it describes are durable. write puts the resources, their identifiers, their search keys, the removals, and the new watermark in ONE session and ONE commit. A watermark ahead of its data is silent, permanent, undetectable data loss - the rows in the gap are never polled again - and this is the design that deletes the window rather than managing it (section 5.2, rule 1, and the sharpest argument in section 8.2).
  2. A write is idempotent by resource id. A sync re-polls from watermark - overlap and therefore re-reads rows it already holds; every one of them is an upsert, so re-reading a batch changes nothing but the instant it was written at. That is what makes the overlap window safe rather than duplicating (section 5.2, rule 2).
  3. A tombstone removes the row. removed deletes the resource, its identifiers, and its search keys. It never archives a last known state - the collection route enumerates a tombstone but the single-resource route answers 404 for it (docs/fhir/design/projection.md section 3.4, finding 3), so there is no final state to archive and pretending otherwise would be inventing one.

WHAT THIS CLASS IS, AND WHY IT IS NOT A PYDANTIC MODEL. It holds an open database engine and a file it creates on first use, which is a connection rather than a value - the same reading that keeps Dhis2Client off ServeContext and puts open_pass_through_client's pool on the runtime instead. dhis2w_core.token_store.SqliteTokenStore is the shape this follows; everything it hands back and takes in is a BaseModel.

WHAT IT DOES NOT DO. It does not write itself: D2 says the sync is the only writer, and nothing here is reachable from a route that changes anything. It does not reconcile: a row that disagrees with DHIS2 is a sync defect whose honest fix is rebuild. And it never decides who may read what - every answer it gives is a set of resources, and which caller may see which of them is settled by the instance, on the live read that resolves each one (docs/fhir/design/projection.md R9).

Classes

SqliteProjectionStore

Holds the FHIR projection of a DHIS2 instance in one SQLite file, written only by sync.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
class SqliteProjectionStore:
    """Holds the FHIR projection of a DHIS2 instance in one SQLite file, written only by sync."""

    def __init__(self, database_path: Path) -> None:
        """Bind an engine to one projection file, which is not created until something reads it."""
        self._database_path = database_path
        self._engine = open_projection_engine(database_path)
        self._sessions = projection_sessions(self._engine)
        self._created = False

    @property
    def database_path(self) -> Path:
        """Where this projection lives, which is what a sync report names and an operator deletes."""
        return self._database_path

    @asynccontextmanager
    async def session(self) -> AsyncGenerator[AsyncSession]:
        """Open one session over this projection's database, creating the file and its tables if new.

        Public because the name index reads the same database through it. The projection has one
        connection, because it has one writer, and a second engine over the same file would be
        something SQLite has to arbitrate for no gain - see `sqlite_names`.
        """
        await self._ensure_tables()
        async with self._sessions() as opened:
            yield opened

    async def read(self, resource_type: str, resource_id: str) -> ProjectedResource | None:
        """Read one projected resource, or None where the projection holds none under that id."""
        async with self.session() as session:
            row = await session.get(ProjectedResourceRow, (resource_type, resource_id))
            if row is None:
                return None
            return _projected(row, await self._cursor_in(session))

    async def search(self, query: ProjectionQuery) -> ProjectionPage:
        """Answer one page of the projection, stating the cursor it was read at.

        `total` is the whole match set and never the page, which is what R4 defines `Bundle.total`
        as - so a client paging a register of eight hundred people is told eight hundred on the first
        page rather than twenty. The order is by resource id: a walk needs an order that does not
        move between two pages of it, and the id is the one thing every projected resource has.
        """
        async with self.session() as session:
            matched = _matching_ids(query)
            total = await session.scalar(select(func.count()).select_from(matched.subquery()))
            page = select(ProjectedResourceRow).where(
                ProjectedResourceRow.resource_type == query.resource_type,
                ProjectedResourceRow.resource_id.in_(matched),
            )
            page = page.order_by(ProjectedResourceRow.resource_id).offset(query.offset)
            if query.count is not None:
                page = page.limit(query.count)
            cursor = await self._cursor_in(session)
            rows = (await session.execute(page)).scalars().all()
            return ProjectionPage(
                resources=tuple(_projected(row, cursor) for row in rows),
                total=int(total or 0),
                cursor=cursor,
            )

    async def write(self, batch: ProjectionBatch) -> ProjectionCursor:
        """Write one sync's batch and answer the cursor the projection now stands at.

        One session, one commit, everything in it: the rows, the index entries beside them, the
        removals, and the watermark the batch advances. Nothing here is written that the commit does
        not cover, which is the whole of correctness rule 1.
        """
        async with self.session() as session:
            for key in batch.removed:
                await _forget(session, key.resource_type, key.resource_id)
            for resource in batch.resources:
                await _forget(session, resource.resource_type, resource.resource_id)
                session.add(
                    ProjectedResourceRow(
                        resource_type=resource.resource_type,
                        resource_id=resource.resource_id,
                        updated_at=resource.cursor.updated_at,
                        tracked_entity_type_uid=resource.tracked_entity_type_uid,
                        body=json.dumps(resource.body, ensure_ascii=False, sort_keys=True),
                    )
                )
                for system, value in _identifiers_of(resource.body):
                    session.add(
                        ProjectedIdentifierRow(
                            resource_type=resource.resource_type,
                            resource_id=resource.resource_id,
                            system=system,
                            value=value,
                        )
                    )
            for name in _distinct_names(batch):
                session.add(
                    ProjectedNameRow(
                        tracked_entity_uid=name.tracked_entity_uid,
                        attribute_uid=name.attribute_uid,
                        value=name.value,
                        folded=name.value.casefold(),
                        tracked_entity_type_uid=name.tracked_entity_type_uid,
                    )
                )
            if batch.endpoint is not None and batch.cursor.updated_at is not None:
                await session.execute(
                    sqlite_insert(ProjectionWatermarkRow)
                    .values(endpoint=batch.endpoint.value, updated_at=batch.cursor.updated_at)
                    .on_conflict_do_update(
                        index_elements=[ProjectionWatermarkRow.endpoint],
                        set_={"updated_at": batch.cursor.updated_at},
                    )
                )
            await session.commit()
            return (await self._watermarks_in(session)).cursor()

    async def cursor(self) -> ProjectionCursor:
        """How far this projection has been filled, which every answer served from it states."""
        async with self.session() as session:
            return await self._cursor_in(session)

    async def watermarks(self) -> ProjectionWatermarks:
        """How far each tracker collection has been read, which is what the next poll asks from."""
        async with self.session() as session:
            return await self._watermarks_in(session)

    async def rebuild(self) -> None:
        """Drop and recreate the projection's tables so a full materialization fills a current schema.

        The watermarks go with the rows. A projection emptied of documents but still claiming to have
        read up to yesterday would never poll for any of them again, which is correctness rule 1
        stated backwards - and it is exactly the state a rebuild exists to be unable to leave behind.

        Dropping rather than deleting is what makes the rebuild the schema migration: a file written
        by an older schema keeps its old columns through any DELETE, and the refill would then write
        columns the table does not have. A rebuild starts from zero anyway, so the tables are remade
        at the schema this process carries.
        """
        await self._ensure_tables()
        await recreate_projection_tables(self._engine)

    async def close(self) -> None:
        """Dispose of the connection, which is what the serve lifespan does when the process unwinds."""
        await self._engine.dispose()

    async def _ensure_tables(self) -> None:
        """Create the file and its tables on the first read or write, and never again in this process."""
        if self._created:
            return
        self._database_path.parent.mkdir(parents=True, exist_ok=True)
        await create_projection_tables(self._engine)
        if self._database_path.exists():
            self._database_path.chmod(PROJECTION_FILE_MODE)
        self._created = True

    async def _cursor_in(self, session: AsyncSession) -> ProjectionCursor:
        """The one instant this projection is as of, read inside a session a caller already opened."""
        return (await self._watermarks_in(session)).cursor()

    async def _watermarks_in(self, session: AsyncSession) -> ProjectionWatermarks:
        """The three watermarks as one value, read inside a session a caller already opened."""
        rows = (await session.execute(select(ProjectionWatermarkRow))).scalars().all()
        marks = {row.endpoint: row.updated_at for row in rows}
        return ProjectionWatermarks(
            tracked_entities=marks.get(ProjectionEndpoint.TRACKED_ENTITIES.value),
            enrollments=marks.get(ProjectionEndpoint.ENROLLMENTS.value),
        )
Attributes
database_path property

Where this projection lives, which is what a sync report names and an operator deletes.

Methods:
__init__(database_path)

Bind an engine to one projection file, which is not created until something reads it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
def __init__(self, database_path: Path) -> None:
    """Bind an engine to one projection file, which is not created until something reads it."""
    self._database_path = database_path
    self._engine = open_projection_engine(database_path)
    self._sessions = projection_sessions(self._engine)
    self._created = False
session() async

Open one session over this projection's database, creating the file and its tables if new.

Public because the name index reads the same database through it. The projection has one connection, because it has one writer, and a second engine over the same file would be something SQLite has to arbitrate for no gain - see sqlite_names.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
@asynccontextmanager
async def session(self) -> AsyncGenerator[AsyncSession]:
    """Open one session over this projection's database, creating the file and its tables if new.

    Public because the name index reads the same database through it. The projection has one
    connection, because it has one writer, and a second engine over the same file would be
    something SQLite has to arbitrate for no gain - see `sqlite_names`.
    """
    await self._ensure_tables()
    async with self._sessions() as opened:
        yield opened
read(resource_type, resource_id) async

Read one projected resource, or None where the projection holds none under that id.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def read(self, resource_type: str, resource_id: str) -> ProjectedResource | None:
    """Read one projected resource, or None where the projection holds none under that id."""
    async with self.session() as session:
        row = await session.get(ProjectedResourceRow, (resource_type, resource_id))
        if row is None:
            return None
        return _projected(row, await self._cursor_in(session))
search(query) async

Answer one page of the projection, stating the cursor it was read at.

total is the whole match set and never the page, which is what R4 defines Bundle.total as - so a client paging a register of eight hundred people is told eight hundred on the first page rather than twenty. The order is by resource id: a walk needs an order that does not move between two pages of it, and the id is the one thing every projected resource has.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def search(self, query: ProjectionQuery) -> ProjectionPage:
    """Answer one page of the projection, stating the cursor it was read at.

    `total` is the whole match set and never the page, which is what R4 defines `Bundle.total`
    as - so a client paging a register of eight hundred people is told eight hundred on the first
    page rather than twenty. The order is by resource id: a walk needs an order that does not
    move between two pages of it, and the id is the one thing every projected resource has.
    """
    async with self.session() as session:
        matched = _matching_ids(query)
        total = await session.scalar(select(func.count()).select_from(matched.subquery()))
        page = select(ProjectedResourceRow).where(
            ProjectedResourceRow.resource_type == query.resource_type,
            ProjectedResourceRow.resource_id.in_(matched),
        )
        page = page.order_by(ProjectedResourceRow.resource_id).offset(query.offset)
        if query.count is not None:
            page = page.limit(query.count)
        cursor = await self._cursor_in(session)
        rows = (await session.execute(page)).scalars().all()
        return ProjectionPage(
            resources=tuple(_projected(row, cursor) for row in rows),
            total=int(total or 0),
            cursor=cursor,
        )
write(batch) async

Write one sync's batch and answer the cursor the projection now stands at.

One session, one commit, everything in it: the rows, the index entries beside them, the removals, and the watermark the batch advances. Nothing here is written that the commit does not cover, which is the whole of correctness rule 1.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def write(self, batch: ProjectionBatch) -> ProjectionCursor:
    """Write one sync's batch and answer the cursor the projection now stands at.

    One session, one commit, everything in it: the rows, the index entries beside them, the
    removals, and the watermark the batch advances. Nothing here is written that the commit does
    not cover, which is the whole of correctness rule 1.
    """
    async with self.session() as session:
        for key in batch.removed:
            await _forget(session, key.resource_type, key.resource_id)
        for resource in batch.resources:
            await _forget(session, resource.resource_type, resource.resource_id)
            session.add(
                ProjectedResourceRow(
                    resource_type=resource.resource_type,
                    resource_id=resource.resource_id,
                    updated_at=resource.cursor.updated_at,
                    tracked_entity_type_uid=resource.tracked_entity_type_uid,
                    body=json.dumps(resource.body, ensure_ascii=False, sort_keys=True),
                )
            )
            for system, value in _identifiers_of(resource.body):
                session.add(
                    ProjectedIdentifierRow(
                        resource_type=resource.resource_type,
                        resource_id=resource.resource_id,
                        system=system,
                        value=value,
                    )
                )
        for name in _distinct_names(batch):
            session.add(
                ProjectedNameRow(
                    tracked_entity_uid=name.tracked_entity_uid,
                    attribute_uid=name.attribute_uid,
                    value=name.value,
                    folded=name.value.casefold(),
                    tracked_entity_type_uid=name.tracked_entity_type_uid,
                )
            )
        if batch.endpoint is not None and batch.cursor.updated_at is not None:
            await session.execute(
                sqlite_insert(ProjectionWatermarkRow)
                .values(endpoint=batch.endpoint.value, updated_at=batch.cursor.updated_at)
                .on_conflict_do_update(
                    index_elements=[ProjectionWatermarkRow.endpoint],
                    set_={"updated_at": batch.cursor.updated_at},
                )
            )
        await session.commit()
        return (await self._watermarks_in(session)).cursor()
cursor() async

How far this projection has been filled, which every answer served from it states.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def cursor(self) -> ProjectionCursor:
    """How far this projection has been filled, which every answer served from it states."""
    async with self.session() as session:
        return await self._cursor_in(session)
watermarks() async

How far each tracker collection has been read, which is what the next poll asks from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def watermarks(self) -> ProjectionWatermarks:
    """How far each tracker collection has been read, which is what the next poll asks from."""
    async with self.session() as session:
        return await self._watermarks_in(session)
rebuild() async

Drop and recreate the projection's tables so a full materialization fills a current schema.

The watermarks go with the rows. A projection emptied of documents but still claiming to have read up to yesterday would never poll for any of them again, which is correctness rule 1 stated backwards - and it is exactly the state a rebuild exists to be unable to leave behind.

Dropping rather than deleting is what makes the rebuild the schema migration: a file written by an older schema keeps its old columns through any DELETE, and the refill would then write columns the table does not have. A rebuild starts from zero anyway, so the tables are remade at the schema this process carries.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def rebuild(self) -> None:
    """Drop and recreate the projection's tables so a full materialization fills a current schema.

    The watermarks go with the rows. A projection emptied of documents but still claiming to have
    read up to yesterday would never poll for any of them again, which is correctness rule 1
    stated backwards - and it is exactly the state a rebuild exists to be unable to leave behind.

    Dropping rather than deleting is what makes the rebuild the schema migration: a file written
    by an older schema keeps its old columns through any DELETE, and the refill would then write
    columns the table does not have. A rebuild starts from zero anyway, so the tables are remade
    at the schema this process carries.
    """
    await self._ensure_tables()
    await recreate_projection_tables(self._engine)
close() async

Dispose of the connection, which is what the serve lifespan does when the process unwinds.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_store.py
async def close(self) -> None:
    """Dispose of the connection, which is what the serve lifespan does when the process unwinds."""
    await self._engine.dispose()

Functions:

sqlite_names

The projection backend of NameSearchIndex: the search keys the sync wrote, asked once.

WHAT IT IMPROVES OVER THE dhis2 BACKEND, STATED HONESTLY. Two things, and not a third.

  • It answers a name. The dhis2 backend puts filter=<attribute>:eq:<value> on the wire, so it finds a person only from the whole of a value spelled exactly. This matches a case-insensitive substring of one, which is what docs/fhir/design/projection.md section 3.3 measured :like: doing and what that section tells any replacement not to regress: the interior substring hits, the bare Khmer consonant hits, and neither would survive a word-tokenising analyzer over scripts that are written without spaces.
  • It costs one query. The dhis2 backend spends one round trip per search key per tracked entity type, sequentially, while the caller waits. This is one indexed read of one local file however many keys and types are in scope, and it is as of the cursor rather than as of now.

What it does NOT do is transliterate. Finding ສົມສັກ from Somsack is R8 and it is an index-time ICU chain that arrives with the OpenSearch backend of step 6; the four rows section 3.3's table marks fails still fail here, and the tests say so rather than assuming otherwise.

WHAT IT DISCLOSES. A tracked entity UID and a score. Not a name, not a value, not an organisation unit - the shape of NameMatch is what makes that a property rather than a promise. The register resolves each match through register.wire.fetch_tracked_entity under the caller's own credentials, so DHIS2 authorizes every record this facade hands over, per match, per caller, exactly as it does when the instance itself did the finding. That is R9's recommended posture (iii) in full, and it is the reason a projection can answer the finding half without this facade taking on one line of DHIS2's authorization model.

WHY IT READS THROUGH THE STORE RATHER THAN OPENING THE FILE ITSELF. The projection has exactly one connection, because it has exactly one writer, and a second engine over the same file would be a second thing SQLite has to arbitrate for no gain at all. The index is a reader of the store's database, and the store is what owns the file.

Classes

SqliteNameSearchIndex

Finds candidate tracked entities in the materialized projection, one indexed query per lookup.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_names.py
class SqliteNameSearchIndex:
    """Finds candidate tracked entities in the materialized projection, one indexed query per lookup."""

    def __init__(self, store: SqliteProjectionStore) -> None:
        """Read through the projection store that owns the file - see this module's docstring on why."""
        self._store = store

    async def index(self, entries: Sequence[IndexedName]) -> None:
        """Hold nothing on its own: a search key reaches this index on the batch its resource rides.

        The keys and the documents they find have to become true at the same instant, so they travel
        in one `ProjectionBatch` and land in one transaction. An `index` that wrote separately would
        be a second watermark with a second way of being wrong, which is the failure
        `docs/fhir/design/projection.md` section 5.2 rule 1 exists to make impossible.
        """
        return None

    async def find(self, query: NameQuery) -> NameMatches:
        """Answer the tracked entities one value matches, exact first, as of the projection's cursor.

        The value is folded and matched as a substring, which is the measured behaviour of the filter
        this replaces. An empty value matches nobody rather than everybody: a lookup that was given
        nothing to look for is not a request to hand over the register.
        """
        needle = query.value.strip().casefold()
        if not needle:
            return NameMatches(cursor=await self._store.cursor())
        async with self._store.session() as session:
            statement = select(ProjectedNameRow).where(
                ProjectedNameRow.folded.like(f"%{_escaped(needle)}%", escape=_LIKE_ESCAPE)
            )
            if query.attribute_uids:
                statement = statement.where(ProjectedNameRow.attribute_uid.in_(query.attribute_uids))
            if query.tracked_entity_type_uids:
                statement = statement.where(
                    ProjectedNameRow.tracked_entity_type_uid.in_(query.tracked_entity_type_uids)
                )
            rows = (await session.execute(statement)).scalars().all()
            cursor = await self._store.cursor()
        found: dict[str, NameMatch] = {}
        for row in sorted(rows, key=lambda row: (row.folded != needle, row.tracked_entity_uid)):
            found.setdefault(
                row.tracked_entity_uid,
                NameMatch(
                    tracked_entity_uid=row.tracked_entity_uid,
                    score=EXACT_MATCH_SCORE if row.folded == needle else SUBSTRING_MATCH_SCORE,
                ),
            )
        matches = tuple(found.values())
        return NameMatches(matches=matches if query.limit is None else matches[: query.limit], cursor=cursor)

    async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
        """Drop nothing on its own: a tombstone reaches this index on the batch that removes the row.

        Same reason `index` holds nothing. `SqliteProjectionStore.write` deletes a removed resource's
        search keys in the transaction that deletes the resource, so the two can never disagree about
        whether somebody is still in the register.
        """
        return None
Methods:
__init__(store)

Read through the projection store that owns the file - see this module's docstring on why.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_names.py
def __init__(self, store: SqliteProjectionStore) -> None:
    """Read through the projection store that owns the file - see this module's docstring on why."""
    self._store = store
index(entries) async

Hold nothing on its own: a search key reaches this index on the batch its resource rides.

The keys and the documents they find have to become true at the same instant, so they travel in one ProjectionBatch and land in one transaction. An index that wrote separately would be a second watermark with a second way of being wrong, which is the failure docs/fhir/design/projection.md section 5.2 rule 1 exists to make impossible.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_names.py
async def index(self, entries: Sequence[IndexedName]) -> None:
    """Hold nothing on its own: a search key reaches this index on the batch its resource rides.

    The keys and the documents they find have to become true at the same instant, so they travel
    in one `ProjectionBatch` and land in one transaction. An `index` that wrote separately would
    be a second watermark with a second way of being wrong, which is the failure
    `docs/fhir/design/projection.md` section 5.2 rule 1 exists to make impossible.
    """
    return None
find(query) async

Answer the tracked entities one value matches, exact first, as of the projection's cursor.

The value is folded and matched as a substring, which is the measured behaviour of the filter this replaces. An empty value matches nobody rather than everybody: a lookup that was given nothing to look for is not a request to hand over the register.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_names.py
async def find(self, query: NameQuery) -> NameMatches:
    """Answer the tracked entities one value matches, exact first, as of the projection's cursor.

    The value is folded and matched as a substring, which is the measured behaviour of the filter
    this replaces. An empty value matches nobody rather than everybody: a lookup that was given
    nothing to look for is not a request to hand over the register.
    """
    needle = query.value.strip().casefold()
    if not needle:
        return NameMatches(cursor=await self._store.cursor())
    async with self._store.session() as session:
        statement = select(ProjectedNameRow).where(
            ProjectedNameRow.folded.like(f"%{_escaped(needle)}%", escape=_LIKE_ESCAPE)
        )
        if query.attribute_uids:
            statement = statement.where(ProjectedNameRow.attribute_uid.in_(query.attribute_uids))
        if query.tracked_entity_type_uids:
            statement = statement.where(
                ProjectedNameRow.tracked_entity_type_uid.in_(query.tracked_entity_type_uids)
            )
        rows = (await session.execute(statement)).scalars().all()
        cursor = await self._store.cursor()
    found: dict[str, NameMatch] = {}
    for row in sorted(rows, key=lambda row: (row.folded != needle, row.tracked_entity_uid)):
        found.setdefault(
            row.tracked_entity_uid,
            NameMatch(
                tracked_entity_uid=row.tracked_entity_uid,
                score=EXACT_MATCH_SCORE if row.folded == needle else SUBSTRING_MATCH_SCORE,
            ),
        )
    matches = tuple(found.values())
    return NameMatches(matches=matches if query.limit is None else matches[: query.limit], cursor=cursor)
forget(tracked_entity_uids) async

Drop nothing on its own: a tombstone reaches this index on the batch that removes the row.

Same reason index holds nothing. SqliteProjectionStore.write deletes a removed resource's search keys in the transaction that deletes the resource, so the two can never disagree about whether somebody is still in the register.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sqlite_names.py
async def forget(self, tracked_entity_uids: Sequence[str]) -> None:
    """Drop nothing on its own: a tombstone reaches this index on the batch that removes the row.

    Same reason `index` holds nothing. `SqliteProjectionStore.write` deletes a removed resource's
    search keys in the transaction that deletes the resource, so the two can never disagree about
    whether somebody is still in the register.
    """
    return None

sync

Filling the projection: the initial materialization, the incremental poll, and the report of both.

This is step 4 of docs/fhir/design/projection.md, and d2w fhir sync is the one thing that calls it. Section 5.1 states the shape in three sentences and this module is those three sentences:

  • Initial materialization. Walk the mapped scope - the tracked entity types [serve.tracked_entities] puts in scope - bulk-paged the way section 3.2 measured, project each page, write it.
  • Incremental runs. Poll the same collection with updatedAfter and includeDeleted=true, apply creates, updates, and tombstones, and advance the watermark. Measured idle cost: one request, 56 bytes.
  • Full rebuild. --rebuild drops to empty and refills. Per D3 that is routine rather than a recovery step, and it is how a fhir.toml mapping change reaches the projection.

NOTHING HERE MAPS ANYTHING. The projection of one tracked entity onto the FHIR resource its type is registered as is register.projection.registered_entity_for, which is the same function the live register answers a read with, and the search keys come off register.projection.attribute_values, which is the same function that decides what a served resource carries. A second mapping surface would be a second answer to "what is this person in FHIR", and the two would drift on the first mapping change. So a synced answer and a live answer are the same bytes, produced by the same code, and the only difference between them is the instant they are true as of.

WHY THE ENROLLMENTS ARE POLLED AND THE EVENTS ARE NOT. A tracked entity's own lastUpdated does not move when one of its enrollments does, and an enrollment carries program-level attribute values that the projected resource does carry - so an enrollment poll is how the projection learns whose copy has gone stale, and each entity it names is re-read through the one tracked entity path. An event carries data values, the projected resource carries none, so an event that moved is not a change to anything this projection holds; polling for one would be a request per interval spent to learn nothing. The event walk arrives with the resources that need it, at steps 8 and 9.

THE WATERMARK ADVANCES AFTER ITS WALK AND NEVER DURING IT. Pages are ordered by createdAt so the walk is stable, which means a later page can carry an EARLIER updatedAt than an earlier one - so no page knows the watermark, and only the finished walk does. Each page is written as its own batch, and the walk is closed by a batch carrying the new watermark once every row it describes is durable. That is section 5.2 rule 1 in the direction that matters: a watermark behind its data costs one re-read, and a watermark ahead of its data is silent permanent loss. A walk that fails halfway advances nothing and the next run re-reads it, which the idempotent write makes free.

WHOSE CREDENTIALS IT READS UNDER, AND WHAT THAT MEANS FOR WHAT IT HOLDS. Whatever the caller handed in - d2w fhir sync hands in the client for the profile the project resolves, which is the facade's own build identity. Section 6's one rule regardless of posture: the projection stores what a configured build identity could read, and the facade must never let a caller's own identity imply more than that. Which is why the serving path resolves every match live under the caller's credentials rather than handing over the row this sync wrote (R9, posture (iii)).

Classes

SyncNarrator

Bases: Protocol

What a run announces a finished step to, so a long materialization is not a silent one.

Narrow on purpose, and structurally satisfied by dhis2w_core.progress.ProgressReporter - the same reason RegisterReader is one method wide. A sync is a batch job that a district's whole register goes through, and the caller that started it is entitled to see it moving; what it is NOT entitled to is a dependency edge from this package to the CLI's console.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
@runtime_checkable
class SyncNarrator(Protocol):
    """What a run announces a finished step to, so a long materialization is not a silent one.

    Narrow on purpose, and structurally satisfied by `dhis2w_core.progress.ProgressReporter` - the
    same reason `RegisterReader` is one method wide. A sync is a batch job that a district's whole
    register goes through, and the caller that started it is entitled to see it moving; what it is
    NOT entitled to is a dependency edge from this package to the CLI's console.
    """

    def complete(self, index: int, total: int, label: str, summary: str, *, style: str | None = None) -> None:
        """Announce that step `index` finished, with a one-line summary."""
        ...
Methods:
complete(index, total, label, summary, *, style=None)

Announce that step index finished, with a one-line summary.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
def complete(self, index: int, total: int, label: str, summary: str, *, style: str | None = None) -> None:
    """Announce that step `index` finished, with a one-line summary."""
    ...

SyncMode

Bases: StrEnum

What one run of the sync was: how much it read, and why it read that much.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
class SyncMode(StrEnum):
    """What one run of the sync was: how much it read, and why it read that much."""

    #: The projection held no watermark, so the whole mapped scope was read. The one place the
    #: population-scale read cost of section 3.2 is paid.
    INITIAL = "initial"

    #: The projection held a watermark, so only what moved since it was read.
    INCREMENTAL = "incremental"

    #: `--rebuild`: the projection was dropped to empty first, then filled as an initial run.
    REBUILD = "rebuild"

SyncResourceCounts

Bases: BaseModel

What one run did to one FHIR resource type of the projection.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
class SyncResourceCounts(BaseModel):
    """What one run did to one FHIR resource type of the projection."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    created: int = 0
    updated: int = 0
    removed: int = 0
    """Rows a tombstone took out. A deletion means "remove the row", never "keep the last state"."""

    def changed(self) -> int:
        """How many rows of this type the run touched at all."""
        return self.created + self.updated + self.removed
Attributes
removed = 0 class-attribute instance-attribute

Rows a tombstone took out. A deletion means "remove the row", never "keep the last state".

Methods:
changed()

How many rows of this type the run touched at all.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
def changed(self) -> int:
    """How many rows of this type the run touched at all."""
    return self.created + self.updated + self.removed

SyncCursorMove

Bases: BaseModel

Where one collection's watermark stood before the run, and where it stands after it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
class SyncCursorMove(BaseModel):
    """Where one collection's watermark stood before the run, and where it stands after it."""

    model_config = ConfigDict(frozen=True)

    endpoint: ProjectionEndpoint
    moved_from: datetime | None = None
    moved_to: datetime | None = None

    def moved(self) -> bool:
        """Whether this run learned anything new about how far the collection has been read."""
        return self.moved_to is not None and self.moved_to != self.moved_from
Methods:
moved()

Whether this run learned anything new about how far the collection has been read.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
def moved(self) -> bool:
    """Whether this run learned anything new about how far the collection has been read."""
    return self.moved_to is not None and self.moved_to != self.moved_from

SyncReport

Bases: BaseModel

What one d2w fhir sync did: what it read, what it changed, and where the cursor now stands.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
class SyncReport(BaseModel):
    """What one `d2w fhir sync` did: what it read, what it changed, and where the cursor now stands."""

    model_config = ConfigDict(frozen=True)

    mode: SyncMode
    dry_run: bool = False
    """True when the run read the instance, counted what it would change, and wrote nothing."""

    project_root: Path
    store_path: Path
    tracked_entity_types: tuple[str, ...] = ()
    """The types the mapped scope put in this run, which is what `[serve.tracked_entities]` narrowed."""

    programs: tuple[str, ...] = ()
    """The programs whose enrollments were polled - the ones the guide publishes, since the endpoint
    admits no other scope (BUGS.md 102)."""

    pages_read: int = 0
    tombstones_visible: bool = True
    """False when the instance refused the tracked entity poll with `includeDeleted=true` and the
    pages were read without it (DHIS2 2.42.6, BUGS.md #116): this run did not learn of an entity
    removed since the last one unless an enrollment of it moved."""

    counts: tuple[SyncResourceCounts, ...] = ()
    cursors: tuple[SyncCursorMove, ...] = ()
    cursor: ProjectionCursor = Field(default_factory=ProjectionCursor)
    """Where the projection as a whole now stands, which is what every answer served from it states."""

    def changed(self) -> int:
        """How many projection rows this run touched, across every resource type."""
        return sum(counts.changed() for counts in self.counts)

    def counts_line(self) -> str:
        """The one line a finished run closes with - the same shape a forward report closes with."""
        created = sum(counts.created for counts in self.counts)
        updated = sum(counts.updated for counts in self.counts)
        removed = sum(counts.removed for counts in self.counts)
        posture = "would create" if self.dry_run else "created"
        return f"{posture} {created}, updated {updated}, removed {removed} over {self.pages_read} page(s)"
Attributes
dry_run = False class-attribute instance-attribute

True when the run read the instance, counted what it would change, and wrote nothing.

tracked_entity_types = () class-attribute instance-attribute

The types the mapped scope put in this run, which is what [serve.tracked_entities] narrowed.

programs = () class-attribute instance-attribute

The programs whose enrollments were polled - the ones the guide publishes, since the endpoint admits no other scope (BUGS.md 102).

tombstones_visible = True class-attribute instance-attribute

False when the instance refused the tracked entity poll with includeDeleted=true and the pages were read without it (DHIS2 2.42.6, BUGS.md #116): this run did not learn of an entity removed since the last one unless an enrollment of it moved.

cursor = Field(default_factory=ProjectionCursor) class-attribute instance-attribute

Where the projection as a whole now stands, which is what every answer served from it states.

Methods:
changed()

How many projection rows this run touched, across every resource type.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
def changed(self) -> int:
    """How many projection rows this run touched, across every resource type."""
    return sum(counts.changed() for counts in self.counts)
counts_line()

The one line a finished run closes with - the same shape a forward report closes with.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
def counts_line(self) -> str:
    """The one line a finished run closes with - the same shape a forward report closes with."""
    created = sum(counts.created for counts in self.counts)
    updated = sum(counts.updated for counts in self.counts)
    removed = sum(counts.removed for counts in self.counts)
    posture = "would create" if self.dry_run else "created"
    return f"{posture} {created}, updated {updated}, removed {removed} over {self.pages_read} page(s)"

Functions:

run_sync(reader, *, surface, store, project_root, store_path, overlap, rebuild=False, dry_run=False, narrator=None) async

Fill or refresh the projection from one DHIS2 instance, and report exactly what changed.

The mode is read off the projection rather than asked for: a store holding no watermark has never been filled, so the first run is a full materialization whether or not anybody said so, and every run after it reads what moved. rebuild is the one that is asked for, because dropping a filled projection is a decision rather than an inference.

dry_run reads the instance exactly as a committing run does and writes nothing at all - not the rows, not the watermark. It is the posture [forward] import = false establishes for the other half of the loop: the real endpoint answers, the real work is counted, and the file on disk is the file that was there before.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/sync.py
async def run_sync(
    reader: RegisterReader,
    *,
    surface: RegisterSurface,
    store: ProjectionStore,
    project_root: Path,
    store_path: Path,
    overlap: timedelta,
    rebuild: bool = False,
    dry_run: bool = False,
    narrator: SyncNarrator | None = None,
) -> SyncReport:
    """Fill or refresh the projection from one DHIS2 instance, and report exactly what changed.

    The mode is read off the projection rather than asked for: a store holding no watermark has never
    been filled, so the first run is a full materialization whether or not anybody said so, and every
    run after it reads what moved. `rebuild` is the one that is asked for, because dropping a filled
    projection is a decision rather than an inference.

    `dry_run` reads the instance exactly as a committing run does and writes nothing at all - not the
    rows, not the watermark. It is the posture `[forward] import = false` establishes for the other
    half of the loop: the real endpoint answers, the real work is counted, and the file on disk is
    the file that was there before.
    """
    before = await store.watermarks()
    mode = (
        SyncMode.REBUILD if rebuild else (SyncMode.INITIAL if before.tracked_entities is None else SyncMode.INCREMENTAL)
    )
    if rebuild:
        if not dry_run:
            await store.rebuild()
        before = ProjectionWatermarks()
    served = tuple((served.uid, served.resource_type) for served in surface.served_types)
    programs = surface.index.program_uids()
    _narrate(
        narrator,
        1,
        "register",
        f"{len(served)} tracked entity type(s), {len(programs)} program(s), mode {mode.value}",
    )

    run = _Run(reader=reader, surface=surface, store=store, dry_run=dry_run)
    entities_mark = await run.materialize(served, since=_since(before.tracked_entities, overlap))
    tombstones_note = "" if run.tombstones_visible else "; tombstones not visible on this instance (BUGS.md #116)"
    _narrate(narrator, 2, "tracked entities", run.counts_line() + tombstones_note)
    enrollments_mark = await run.refresh_from_enrollments(
        served,
        programs,
        since=_since(before.enrollments, overlap),
        whole_scope_read=mode is not SyncMode.INCREMENTAL,
    )
    _narrate(narrator, 3, "enrollments", f"{run.touched} entity(s) re-read from an enrollment that moved")

    after = ProjectionWatermarks(
        tracked_entities=entities_mark or before.tracked_entities,
        enrollments=enrollments_mark or before.enrollments,
    )
    return SyncReport(
        mode=mode,
        dry_run=dry_run,
        project_root=project_root,
        store_path=store_path,
        tracked_entity_types=tuple(uid for uid, _ in served),
        programs=programs,
        pages_read=run.pages,
        tombstones_visible=run.tombstones_visible,
        counts=run.counts(),
        cursors=(
            SyncCursorMove(
                endpoint=ProjectionEndpoint.TRACKED_ENTITIES,
                moved_from=before.tracked_entities,
                moved_to=after.tracked_entities,
            ),
            SyncCursorMove(
                endpoint=ProjectionEndpoint.ENROLLMENTS,
                moved_from=before.enrollments,
                moved_to=after.enrollments,
            ),
        ),
        cursor=after.cursor() if dry_run else await store.cursor(),
    )

serving

How a projection-served answer states the instant it is as of, which every one of them does.

D4 and R3 of docs/fhir/design/projection.md are one requirement written twice: a projection answer is always "as of <instant>", never "now", and the instant is carried in the response rather than inferred from a header nobody reads. Section 11 reserves HOW, listing five candidates. This is the answer, and it is two things rather than one because the two audiences are different.

In the searchset, an outcome entry. R4 3.1.1.4 gives a server exactly one way to say something about a search inside the search's own answer: a Bundle entry whose search.mode is outcome, carrying an OperationOutcome. That is a FHIR data-model element, so a client that reads Bundles reads this without being told the extension exists, and R4 already says such an entry is not a match and does not count toward total. Bundle.meta.lastUpdated was the other candidate and it is not the one: it would say when the Bundle was last changed, and a searchset assembled this second out of rows read yesterday has those as two different instants - stating the older one under the newer one's name would be a fidelity defect of exactly the kind register.projection refuses elsewhere.

Beside it, a response header. X-DHIS2W-Projection-As-Of carries the same instant for the half of the world that reads responses rather than resources - a proxy log, a curl -I, a cache. It is the second statement of one fact and never the only statement of it, which is what R3's "not inferred from a header nobody reads" rules out.

An instant with no zone, because that is what the instance said. DHIS2 2.43 answers updatedAt as a zone-less wall-clock reading in its own zone (BUGS.md 62), and the cursor is that reading carried through unchanged. Attaching this host's offset would make the statement a claim about a clock nobody consulted, so the text says the reading and says whose reading it is.

Classes

Functions:

as_of(cursor)

The instant a projection answer is as of, in the instance's own zone-less spelling.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/serving.py
def as_of(cursor: ProjectionCursor) -> str:
    """The instant a projection answer is as of, in the instance's own zone-less spelling."""
    return NOTHING_SYNCED if cursor.updated_at is None else cursor.updated_at.isoformat()

as_of_headers(cursor)

The one header every projection-served response carries, whatever shape its body is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/serving.py
def as_of_headers(cursor: ProjectionCursor) -> dict[str, str]:
    """The one header every projection-served response carries, whatever shape its body is."""
    return {PROJECTION_AS_OF_HEADER: as_of(cursor)}

as_of_entry(cursor)

The outcome entry every projection-served searchset carries, saying what it is as of.

information severity and the informational issue code, because nothing went wrong: this is a server telling a client a true thing about the answer it is holding. It is not a match, it does not count toward total, and a client that ignores it has lost nothing but the knowledge of when the answer was true.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/projection/serving.py
def as_of_entry(cursor: ProjectionCursor) -> BundleEntry:
    """The `outcome` entry every projection-served searchset carries, saying what it is as of.

    `information` severity and the `informational` issue code, because nothing went wrong: this is a
    server telling a client a true thing about the answer it is holding. It is not a match, it does
    not count toward `total`, and a client that ignores it has lost nothing but the knowledge of when
    the answer was true.
    """
    return BundleEntry(
        resource=json_resource(
            OperationOutcome(
                issue=[
                    OperationOutcomeIssue(
                        severity="information",
                        code="informational",
                        diagnostics=(
                            "served from this project's materialized projection of the DHIS2 instance, as of "
                            f"{as_of(cursor)} on the instance's own clock. "
                            "A record this answer names is read from the instance under the credentials of "
                            "whoever asked, so what the projection decides is who is on the page and what "
                            "DHIS2 decides is who may see them."
                        )
                        if cursor.updated_at is not None
                        else (
                            "this project's materialized projection of the DHIS2 instance holds nothing yet: "
                            "no sync has read the instance, so this answer states what has been read rather "
                            "than what the instance holds. `d2w fhir sync` fills it."
                        ),
                    )
                ]
            )
        ),
        search=BundleEntrySearch(mode="outcome"),
    )

Enrollment listing

GET /facade/tracked-entities/{uid}/enrollments - which programs one tracked entity is enrolled in, as the picker's typed JSON feed rather than as a FHIR resource, because whether a DHIS2 enrollment is an EpisodeOfCare or a CarePlan is a decision this project has deliberately not taken yet.

enrollments

GET /facade/tracked-entities/{uid}/enrollments - which programs one tracked entity is enrolled in.

WHY THIS IS NOT A FHIR RESOURCE. A DHIS2 enrollment is one tracked entity's participation in a program, and FHIR has two candidate resources for that - EpisodeOfCare and CarePlan - which mean different things and would commit this project to one reading of what a DHIS2 program is. That choice is deliberately still open: it is roadmap decision 5.2, and picking it here, inside a picker's data feed, would settle by accident a question that deserves settling on purpose. So the listing is typed JSON on a path of its own, and the day the decision lands it becomes a resource without having had to un-publish one first.

SO IT IS /spool's AND /uiconfig's SHAPE, FOR THEIR REASONS, AT THEIR ADDRESS. Plain application/json, Pydantic models rather than a Bundle, served under the facade API's own mount rather than at the FHIR base. dhis2w_fhir_serve.routes.spool argues that choice in full.

WHAT IT IS FOR. A capture client that has found a subject still has to answer a stage form against one of that subject's enrollments, and the enrollment UID is what the response carries. This is the list it picks from.

WHY THE PATH NAMES A TRACKED ENTITY. DHIS2 enrols tracked entities, and a tracked entity is whatever the project tracks - so a path spelt /patients/ would be a lie the moment a project registers a type the published map takes onto something other than Patient. The listing is keyed by the DHIS2 tracked entity UID and serves every one of them, whichever FHIR resource its type is registered as.

A COMPLETED ENROLLMENT IS LISTED, AND SAID TO BE COMPLETED. DHIS2 accepts an event into a completed enrollment with no error and no warning (BUGS.md 70), so nothing downstream will tell a user that what they just captured went into a closed episode. This server does not enforce the rule - the instance is the authority on what it accepts, and a facade that hid a completed enrollment would be hiding data the instance is perfectly willing to be given - it states the status, and active is the one field a picker needs to grade it on.

The read is entity-scoped and never names a program, because naming one the entity is not enrolled in answers 404 asserting the entity does not exist (BUGS.md 72). The enrollments come off the entity.

This listing is part of the register, so [serve.tracked_entities] enabled = false refuses it along with the FHIR routes: a project that serves no tracked entity serves nothing about its enrollments either. It is part of the register in the other sense too: under [serve] auth = "dhis2" the entity is read with the CALLER'S own Authorization header, so a caller who may not see that person is told by DHIS2 that there is nobody there - see dhis2w_fhir_serve.passthrough.

Classes

TrackedEntityEnrollment

Bases: BaseModel

One enrollment of one tracked entity: what it is, where it sits, and whether it is still open.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/enrollments.py
class TrackedEntityEnrollment(BaseModel):
    """One enrollment of one tracked entity: what it is, where it sits, and whether it is still open."""

    model_config = ConfigDict(frozen=True)

    enrollment_uid: str
    program_uid: str
    program_name: str | None = None
    """The name this project's guide publishes the program under, or None when it publishes none."""

    status: str
    """The DHIS2 enrollment status verbatim - `ACTIVE`, `COMPLETED`, or `CANCELLED`."""

    active: bool
    """False for a completed or cancelled enrollment. DHIS2 still accepts events into one (BUGS.md 70)."""

    enrolled_at: str | None = None
    """When the enrollment began, as DHIS2 dated it, in ISO 8601."""

    organisation_unit_uid: str | None = None
    organisation_unit_name: str | None = None
    """The name the published registry gives that organisation unit, or None when it publishes none."""
Attributes
program_name = None class-attribute instance-attribute

The name this project's guide publishes the program under, or None when it publishes none.

status instance-attribute

The DHIS2 enrollment status verbatim - ACTIVE, COMPLETED, or CANCELLED.

active instance-attribute

False for a completed or cancelled enrollment. DHIS2 still accepts events into one (BUGS.md 70).

enrolled_at = None class-attribute instance-attribute

When the enrollment began, as DHIS2 dated it, in ISO 8601.

organisation_unit_name = None class-attribute instance-attribute

The name the published registry gives that organisation unit, or None when it publishes none.

TrackedEntityEnrollments

Bases: BaseModel

Every enrollment one tracked entity holds, in the order DHIS2 returned them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/enrollments.py
class TrackedEntityEnrollments(BaseModel):
    """Every enrollment one tracked entity holds, in the order DHIS2 returned them."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str
    enrollments: list[TrackedEntityEnrollment] = Field(default_factory=list)

Functions:

read_tracked_entity_enrollments(request, tracked_entity_uid) async

List the programs one tracked entity is enrolled in, read off the entity itself.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/enrollments.py
@router.get(
    TRACKED_ENTITY_ENROLLMENTS_PATH,
    tags=[REGISTER_TAG],
    summary="List one tracked entity's enrollments",
    description=(
        "The programs one tracked entity is enrolled in, read off the entity itself in the order "
        "DHIS2 returned them, with the names this project's guide publishes for the program and the "
        "organisation unit joined on. This is what a capture client picks an enrollment from before "
        "answering a program stage's form.\n\n"
        "A completed or cancelled enrollment is listed and said to be completed or cancelled: DHIS2 "
        "accepts an event into one with no error and no warning, so hiding it would hide data the "
        "instance is perfectly willing to be given. `active` is the field to grade a picker on.\n\n"
        "Live runs only. A run serving a compiled guide has no instance to read, and "
        "`[serve.tracked_entities] enabled = false` takes this away with the rest of the register. "
        'Under `[serve] auth = "dhis2"` the entity is read with the caller\'s own `Authorization`, '
        "so a caller who may not see that person is told by DHIS2 that there is nobody there."
    ),
    response_description="Every enrollment the entity holds, or an OperationOutcome naming why there is none to read.",
)
async def read_tracked_entity_enrollments(request: Request, tracked_entity_uid: str) -> TrackedEntityEnrollments:
    """List the programs one tracked entity is enrolled in, read off the entity itself."""
    surface = serve_context(request).register_surface
    if not surface.tracked_entities.enabled:
        raise RegisterDisabledError(ENROLLMENTS_SURFACE_NAME)
    reader = await register_reader(request)
    if reader is None:
        raise NotServedFromCompiledIgError(ENROLLMENTS_SURFACE_NAME)
    if not surface.serves_tracked_entities():
        raise NoPublishedSubjectTypeError(ENROLLMENTS_SURFACE_NAME)
    index = surface.index
    try:
        entity = await fetch_tracked_entity(reader, tracked_entity_uid)
    except Dhis2ClientError as error:
        raise UpstreamError(
            f"the DHIS2 instance did not answer the tracked entity read: {upstream_refusal_text(error)}"
        ) from error
    if entity is None:
        raise NotFoundError(ENROLLMENTS_SURFACE_NAME, tracked_entity_uid)
    return TrackedEntityEnrollments(
        tracked_entity_uid=tracked_entity_uid,
        enrollments=[_listed(enrollment, index) for enrollment in entity.enrollments or [] if enrollment.enrollment],
    )

The record

GET /facade/tracked-entities/{uid}/events - one tracked entity's own events, each served as the QuestionnaireResponse its program stage's published form describes, and GET /facade/tracked-entities/{uid}/events/{eventUid} for one of them. Where the register answers who somebody is, this answers what has happened to them: one entity-scoped read of the instance per request, under the credentials of whoever asked. The wire module holds the read and the order it puts the record in, and the projection turns one recorded event into the document the capture contract already states for it - so the shape a client reads back is the shape a client would post.

wire

The DHIS2 read behind one entity's record: the events, off the entity, in one request.

FOUR FACTS DECIDE THE SHAPE OF THIS READ, and three of them are recorded in the repository's BUGS.md:

  1. The events come off the tracked entity, never off a program. /api/tracker/events demands a program unconditionally on 2.43 and answers a Tomcat HTML page when it is missing (BUGS.md 91), and its singular enrollment= filter is accepted and silently dropped on every major. Naming a program here would also mean naming one the entity might not be enrolled in, which answers 404 claiming the entity does not exist (BUGS.md 72). The tracked entity read takes neither risk: the enrollments are a projection of the entity, and the events are a projection of those.
  2. No events listing is scoped by organisation unit. ?orgUnit= on the events collection filters by the enrollment owner's unit rather than the event's own (BUGS.md 69), so an event recorded away from the owning facility is invisible under the unit its payload names. An entity-scoped read serves every event whatever unit each was recorded at, which is the whole record and the only honest answer to "what happened to this person".
  3. The default projection carries none of this. The tracked entity endpoint omits the enrollments entirely unless fields names them, so the projection is spelled out in full here - down to the data values, without which the record would be a list of dates.
  4. The order DHIS2 answers in is not an order (BUGS.md 108). The nested events arrive sorted by neither their own date nor their creation - a seeded fridge answers 07:00, 06:00, 08:00 - and the order= the same request accepts does not reach them. So the record is ordered here, newest first, by the instant the event occurred and then by its UID. The tie-break is what makes two reads of an unchanged record answer the same bytes.

A DELETED EVENT IS NOT PART OF THE RECORD. includeDeleted is not passed, so a soft-deleted event is absent, which is what a read of what the instance holds now means. The sync's polls pass it for the opposite reason - a tombstone is exactly what a cursor walk is looking for.

WHAT reader IS HERE IS THE POINT OF THE PARAMETER, as it is for the register: every read takes a RegisterReader and never a Dhis2Client, so whose credentials the record is read under is settled per request. Under [serve] auth = "dhis2" it is the caller's own header, and DHIS2 decides per caller which events come back. See dhis2w_fhir_serve.passthrough.

Classes

RecordedValue

Bases: BaseModel

One value one event holds: the DHIS2 data element it answers, and the string the instance stored.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
class RecordedValue(BaseModel):
    """One value one event holds: the DHIS2 data element it answers, and the string the instance stored."""

    model_config = ConfigDict(frozen=True)

    data_element_uid: str
    value: str

RecordedEvent

Bases: BaseModel

One event of one enrollment, as the instance holds it right now.

Every field but the event's own UID is optional because the instance states each of them separately, and an event missing one is served with that fact missing rather than dropped: what a partial event costs is one element of the document it projects onto, and hiding the event would cost the whole of it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
class RecordedEvent(BaseModel):
    """One event of one enrollment, as the instance holds it right now.

    Every field but the event's own UID is optional because the instance states each of them
    separately, and an event missing one is served with that fact missing rather than dropped: what
    a partial event costs is one element of the document it projects onto, and hiding the event
    would cost the whole of it.
    """

    model_config = ConfigDict(frozen=True)

    event_uid: str
    program_uid: str | None = None
    program_stage_uid: str | None = None
    enrollment_uid: str | None = None
    status: str | None = None
    """The DHIS2 event status verbatim - `ACTIVE`, `COMPLETED`, `SCHEDULE`, `SKIPPED`, or `VISITED`."""

    occurred_at: str | None = None
    """When the event occurred, as DHIS2 dated it, in the instance's own zone-less spelling (BUGS.md 62)."""

    organisation_unit_uid: str | None = None
    values: tuple[RecordedValue, ...] = ()

    def order_key(self) -> tuple[str, str]:
        """Where one event sits in the record: the instant it occurred, then its UID as the tie-break.

        An event the instance dates nothing sorts to the end of a newest-first walk, which is where an
        undated event belongs: it is not newer than anything that carries a date.
        """
        return (self.occurred_at or "", self.event_uid)
Attributes
status = None class-attribute instance-attribute

The DHIS2 event status verbatim - ACTIVE, COMPLETED, SCHEDULE, SKIPPED, or VISITED.

occurred_at = None class-attribute instance-attribute

When the event occurred, as DHIS2 dated it, in the instance's own zone-less spelling (BUGS.md 62).

Methods:
order_key()

Where one event sits in the record: the instant it occurred, then its UID as the tie-break.

An event the instance dates nothing sorts to the end of a newest-first walk, which is where an undated event belongs: it is not newer than anything that carries a date.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
def order_key(self) -> tuple[str, str]:
    """Where one event sits in the record: the instant it occurred, then its UID as the tie-break.

    An event the instance dates nothing sorts to the end of a newest-first walk, which is where an
    undated event belongs: it is not newer than anything that carries a date.
    """
    return (self.occurred_at or "", self.event_uid)

TrackedEntityRecord

Bases: BaseModel

Everything one tracked entity holds over time: who they are to DHIS2, and every event, newest first.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
class TrackedEntityRecord(BaseModel):
    """Everything one tracked entity holds over time: who they are to DHIS2, and every event, newest first."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str
    tracked_entity_type_uid: str | None = None
    events: tuple[RecordedEvent, ...] = ()

Functions:

fetch_tracked_entity_record(reader, tracked_entity_uid) async

Read one entity's whole record in one entity-scoped request, or None when the instance holds none.

A value that is not UID-shaped is None without a request, exactly as the register's read is: DHIS2 answers 400 for one, and this facade knows the shape of the identifier it mints its own links from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
async def fetch_tracked_entity_record(reader: RegisterReader, tracked_entity_uid: str) -> TrackedEntityRecord | None:
    """Read one entity's whole record in one entity-scoped request, or None when the instance holds none.

    A value that is not UID-shaped is None without a request, exactly as the register's read is: DHIS2
    answers 400 for one, and this facade knows the shape of the identifier it mints its own links from.
    """
    if not is_tracked_entity_uid(tracked_entity_uid):
        return None
    try:
        raw = await reader.get_raw(
            f"{TRACKED_ENTITIES_PATH}/{tracked_entity_uid}", params={"fields": TRACKED_ENTITY_RECORD_FIELDS}
        )
    except Dhis2ApiError as error:
        if error.status_code == 404:
            return None
        raise
    return recorded_entity(TrackerTrackedEntity.model_validate(raw), tracked_entity_uid)

recorded_entity(entity, tracked_entity_uid)

Read one answered tracked entity into the record this facade serves, ordered newest first.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
def recorded_entity(entity: TrackerTrackedEntity, tracked_entity_uid: str) -> TrackedEntityRecord:
    """Read one answered tracked entity into the record this facade serves, ordered newest first."""
    events = [
        _recorded_event(event, enrollment_uid=enrollment.enrollment, program_uid=enrollment.program)
        for enrollment in entity.enrollments or []
        for event in enrollment.events or []
        if event.event
    ]
    return TrackedEntityRecord(
        tracked_entity_uid=entity.trackedEntity or tracked_entity_uid,
        tracked_entity_type_uid=entity.trackedEntityType,
        events=tuple(sorted(events, key=RecordedEvent.order_key, reverse=True)),
    )

instant_text(value)

One DHIS2 instant as text - the tracker's own spelling, not Python's str() of a datetime.

The generated model types the element as an instant or an epoch integer, because the OpenAPI document allows both; an integer is carried through as it arrived rather than guessed a unit for, which is the reading dhis2w_fhir_serve.routes.enrollments gives the same element.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/wire.py
def instant_text(value: datetime | int | None) -> str | None:
    """One DHIS2 instant as text - the tracker's own spelling, not Python's `str()` of a datetime.

    The generated model types the element as an instant or an epoch integer, because the OpenAPI
    document allows both; an integer is carried through as it arrived rather than guessed a unit for,
    which is the reading `dhis2w_fhir_serve.routes.enrollments` gives the same element.
    """
    if value is None:
        return None
    return value.isoformat() if isinstance(value, datetime) else str(value)

projection

One recorded event as the document the guide already publishes for it - a QuestionnaireResponse.

THE SHAPE IS THE CAPTURE CONTRACT'S, READ BACKWARDS. A tracker event captured through this facade arrives as a D2TrackerEventResponse: the stage's Questionnaire as questionnaire, the tracked entity as subject, the enrollment and the reporting unit as extensions, the event's own instant as authored, and one item per data value under the linkId its data element is asked as. This module builds exactly that document out of what the instance now holds, so the record a client reads back is the record a client could have written - one shape for both legs rather than two readings of one event.

NOTHING HERE IS INVENTED, AND THE PROJECTION IS THE SERVED FORM'S. Every question's item type, terminology binding, and DHIS2 value type is read off the very CaptureIndex a received response is validated against, so a value can never be typed one way on the way in and another on the way out. The same rule the capture path and $generate hold to holds here: the item type decides which value[x] element carries the answer, and the DHIS2 value the instance stored decides what that element carries.

WHAT IT REFUSES TO SAY. A stage this project publishes no form for is not projected at all - there is no questionnaire to name, and a document naming none is not one this guide describes. Those events are counted and their stages named, so a reader of the answer learns the record is wider than the guide, rather than reading a short answer as a short record. An event missing a fact its profile requires - no tracked entity, no enrollment, no reporting unit, no instant - is served without claiming the profile, which is the same rule the example corpus follows for the same reason.

HOW A STORED VALUE BECOMES AN ANSWER IS NOT THIS MODULE'S. dhis2w_fhir_serve.history.answers casts one DHIS2 string onto the value[x] element its question asks it on, resolves a coded one against the served terminology, and hangs the results in the form's own tree - and the data set read-back beside this reads the same functions, so one form can never type a value one way for a tracker event and another for an aggregate cell.

Classes

ProjectedEvent

Bases: BaseModel

One event of the record as this facade answers it: the document, or the reason there is none.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
class ProjectedEvent(BaseModel):
    """One event of the record as this facade answers it: the document, or the reason there is none."""

    model_config = ConfigDict(frozen=True)

    event_uid: str
    response: QuestionnaireResponse | None = None
    unpublished_stage_uid: str | None = None
    """The program stage this project publishes no form for, on an event that could not be projected."""
Attributes
unpublished_stage_uid = None class-attribute instance-attribute

The program stage this project publishes no form for, on an event that could not be projected.

ProjectedRecord

Bases: BaseModel

One page of a record, projected: the documents it carries and what it could not carry.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
class ProjectedRecord(BaseModel):
    """One page of a record, projected: the documents it carries and what it could not carry."""

    model_config = ConfigDict(frozen=True)

    responses: tuple[QuestionnaireResponse, ...] = ()
    unpublished_stage_uids: tuple[str, ...] = ()
    """Every stage on this page the guide publishes no form for, once each, in the order they occurred."""

    unprojected_events: int = 0
    """How many events on this page carry no document, which is one per event of an unpublished stage."""
Attributes
unpublished_stage_uids = () class-attribute instance-attribute

Every stage on this page the guide publishes no form for, once each, in the order they occurred.

unprojected_events = 0 class-attribute instance-attribute

How many events on this page carry no document, which is one per event of an unpublished stage.

RecordProjection

Bases: BaseModel

What one record read is projected through: the project's names, what it serves, and its zone.

Built per request and dropped with it, over state the process already holds: the store is loaded once at startup and the index cache is the very cache a capture validates against, so the forms a record is projected through are the forms this facade checks submissions against.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
class RecordProjection(BaseModel):
    """What one record read is projected through: the project's names, what it serves, and its zone.

    Built per request and dropped with it, over state the process already holds: the store is loaded
    once at startup and the index cache is the very cache a capture validates against, so the forms a
    record is projected through are the forms this facade checks submissions against.
    """

    model_config = ConfigDict(frozen=True)

    naming: CaptureNaming
    store: ResourceStore
    indexes: CaptureIndexCache
    timezone: str | None = None
    """The IANA zone the instance's zone-less timestamps are wall-clock readings in (BUGS.md 62)."""

    _resolvers: CodingResolverSet = PrivateAttr()
    _indexes_by_stage: dict[str, CaptureIndex | None] = PrivateAttr(default_factory=dict)

    def model_post_init(self, context: Any, /) -> None:
        """Open the terminology resolvers over the served store (private attributes stay settable)."""
        self._resolvers = CodingResolverSet(store=self.store)

    def project(self, record: TrackedEntityRecord, events: tuple[RecordedEvent, ...]) -> ProjectedRecord:
        """Project the events of one page, keeping the record's own order and naming what it could not carry."""
        projected = [self.project_event(record, event) for event in events]
        stages: dict[str, None] = {}
        for entry in projected:
            if entry.unpublished_stage_uid is not None:
                stages.setdefault(entry.unpublished_stage_uid, None)
        return ProjectedRecord(
            responses=tuple(entry.response for entry in projected if entry.response is not None),
            unpublished_stage_uids=tuple(stages),
            unprojected_events=sum(1 for entry in projected if entry.response is None),
        )

    def project_event(self, record: TrackedEntityRecord, event: RecordedEvent) -> ProjectedEvent:
        """Build the document one event is served as, or state the stage this project publishes no form for."""
        index = self.form_for(event.program_stage_uid)
        if index is None:
            return ProjectedEvent(event_uid=event.event_uid, unpublished_stage_uid=event.program_stage_uid or "")
        authored = self._authored(event)
        complete = authored is not None and event.enrollment_uid is not None and event.organisation_unit_uid is not None
        return ProjectedEvent(
            event_uid=event.event_uid,
            response=QuestionnaireResponse(
                id=event.event_uid,
                meta=Meta(profile=[self.naming.response_profile_url(TRACKER_EVENT_FORM_KIND)]) if complete else None,
                extension=self._extensions(event),
                questionnaire=index.canonical,
                status=response_status(response_status_code(event.status)),
                subject=Reference(
                    type=index.subject_type,
                    identifier=Identifier(system=self.naming.tracked_entity_system, value=record.tracked_entity_uid),
                ),
                authored=authored,
                item=self._items(index, event) or None,
            ),
        )

    def form_for(self, program_stage_uid: str | None) -> CaptureIndex | None:
        """The served form one program stage published, or None when this project publishes none for it.

        The lookup is by the DHIS2 identifier the generated Questionnaire carries, which is the same
        `{base}/id/program-stage` system the guide publishes the stage under - never by a name and
        never by a canonical this server composed, because what a form is called is the naming
        source's decision and what it is about is the identifier's.
        """
        if program_stage_uid is None:
            return None
        if program_stage_uid in self._indexes_by_stage:
            return self._indexes_by_stage[program_stage_uid]
        self._indexes_by_stage[program_stage_uid] = self._read_form(program_stage_uid)
        return self._indexes_by_stage[program_stage_uid]

    def _read_form(self, program_stage_uid: str) -> CaptureIndex | None:
        """Read the one served Questionnaire a stage UID names, or None when nothing served names it."""
        token = IdentifierToken(system=self.naming.program_stage_identifier_system, value=program_stage_uid)
        for entry in self.store.search(QUESTIONNAIRE_RESOURCE_TYPE, SearchQuery(identifiers=(token,))):
            if entry.canonical_url is None:
                continue
            try:
                return self.indexes.resolve(entry.canonical_url, self.naming, self.store)
            except UnreadableQuestionnaireError:
                continue
        return None

    def _authored(self, event: RecordedEvent) -> str | None:
        """When the event occurred, as an R4 `dateTime`, or nothing when the instance stated no readable instant.

        DHIS2 answers `occurredAt` as a zone-less wall-clock reading (BUGS.md 62), so it is given the
        offset the project's own zone stood at on that reading - the same normalisation the example
        corpus applies to the same field.
        """
        if event.occurred_at is None:
            return None
        normalized = zoned_date_time(event.occurred_at.strip(), self.timezone)
        return normalized if is_fhir_date_time(normalized) else None

    def _extensions(self, event: RecordedEvent) -> list[Extension]:
        """The extensions a stage response carries, in the order its own profile slices them.

        The reporting unit first, then the enrollment the event belongs to, then the form kind - and
        each is left off when the instance stated no value for it, because an extension pointing at
        nothing is worse than an absent one.
        """
        extensions: list[Extension] = []
        if event.organisation_unit_uid is not None:
            extensions.append(
                Extension(
                    url=self.naming.organisation_unit_url,
                    valueReference=Reference(reference=f"{LOCATION_RESOURCE_TYPE}/{event.organisation_unit_uid}"),
                )
            )
        if event.enrollment_uid is not None:
            extensions.append(
                Extension(
                    url=self.naming.tracker_enrollment_url,
                    valueIdentifier=Identifier(
                        system=self.naming.tracker_enrollment_system, value=event.enrollment_uid
                    ),
                )
            )
        extensions.append(Extension(url=self.naming.form_type_url, valueCode=TRACKER_EVENT_FORM_KIND))
        return extensions

    def _items(self, index: CaptureIndex, event: RecordedEvent) -> list[QuestionnaireResponseItem]:
        """Mirror the form's item tree in document order, keeping the branches a stored value reaches.

        A value whose data element the form does not ask is not carried: the response would answer a
        question this project's guide never published, and a client validating it against the form
        would be told so. The form's own tree is what the answers hang in, so a value stays inside the
        section its question was asked in.
        """
        answers = {
            value.data_element_uid: question_answers(
                index.questions[value.data_element_uid],
                value.value,
                resolvers=self._resolvers,
                timezone=self.timezone,
            )
            for value in event.values
            if value.data_element_uid in index.questions
        }
        return answered_items(item_children(index), answers, None)
Attributes
timezone = None class-attribute instance-attribute

The IANA zone the instance's zone-less timestamps are wall-clock readings in (BUGS.md 62).

Methods:
model_post_init(context)

Open the terminology resolvers over the served store (private attributes stay settable).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
def model_post_init(self, context: Any, /) -> None:
    """Open the terminology resolvers over the served store (private attributes stay settable)."""
    self._resolvers = CodingResolverSet(store=self.store)
project(record, events)

Project the events of one page, keeping the record's own order and naming what it could not carry.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
def project(self, record: TrackedEntityRecord, events: tuple[RecordedEvent, ...]) -> ProjectedRecord:
    """Project the events of one page, keeping the record's own order and naming what it could not carry."""
    projected = [self.project_event(record, event) for event in events]
    stages: dict[str, None] = {}
    for entry in projected:
        if entry.unpublished_stage_uid is not None:
            stages.setdefault(entry.unpublished_stage_uid, None)
    return ProjectedRecord(
        responses=tuple(entry.response for entry in projected if entry.response is not None),
        unpublished_stage_uids=tuple(stages),
        unprojected_events=sum(1 for entry in projected if entry.response is None),
    )
project_event(record, event)

Build the document one event is served as, or state the stage this project publishes no form for.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
def project_event(self, record: TrackedEntityRecord, event: RecordedEvent) -> ProjectedEvent:
    """Build the document one event is served as, or state the stage this project publishes no form for."""
    index = self.form_for(event.program_stage_uid)
    if index is None:
        return ProjectedEvent(event_uid=event.event_uid, unpublished_stage_uid=event.program_stage_uid or "")
    authored = self._authored(event)
    complete = authored is not None and event.enrollment_uid is not None and event.organisation_unit_uid is not None
    return ProjectedEvent(
        event_uid=event.event_uid,
        response=QuestionnaireResponse(
            id=event.event_uid,
            meta=Meta(profile=[self.naming.response_profile_url(TRACKER_EVENT_FORM_KIND)]) if complete else None,
            extension=self._extensions(event),
            questionnaire=index.canonical,
            status=response_status(response_status_code(event.status)),
            subject=Reference(
                type=index.subject_type,
                identifier=Identifier(system=self.naming.tracked_entity_system, value=record.tracked_entity_uid),
            ),
            authored=authored,
            item=self._items(index, event) or None,
        ),
    )
form_for(program_stage_uid)

The served form one program stage published, or None when this project publishes none for it.

The lookup is by the DHIS2 identifier the generated Questionnaire carries, which is the same {base}/id/program-stage system the guide publishes the stage under - never by a name and never by a canonical this server composed, because what a form is called is the naming source's decision and what it is about is the identifier's.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/history/projection.py
def form_for(self, program_stage_uid: str | None) -> CaptureIndex | None:
    """The served form one program stage published, or None when this project publishes none for it.

    The lookup is by the DHIS2 identifier the generated Questionnaire carries, which is the same
    `{base}/id/program-stage` system the guide publishes the stage under - never by a name and
    never by a canonical this server composed, because what a form is called is the naming
    source's decision and what it is about is the identifier's.
    """
    if program_stage_uid is None:
        return None
    if program_stage_uid in self._indexes_by_stage:
        return self._indexes_by_stage[program_stage_uid]
    self._indexes_by_stage[program_stage_uid] = self._read_form(program_stage_uid)
    return self._indexes_by_stage[program_stage_uid]

Functions:

history

GET /facade/tracked-entities/{uid}/events - one tracked entity's record, read from the instance per request.

THE REGISTER ANSWERS WHO SOMEBODY IS AND THIS ANSWERS WHAT HAPPENED TO THEM. GET /{RegisterType}/{uid} serves the identity of one tracked entity at the FHIR base; this serves the events of every enrollment that entity holds, each as the QuestionnaireResponse the guide already publishes for its program stage. Both are read from DHIS2 at request time, under the credentials of whoever asked, and neither is cached.

IT IS THE FACADE'S ADDRESS AND A FHIR DOCUMENT, WHICH IS NOT A CONTRADICTION. FHIR defines no interaction at /{type}/{uid}/events - the CapabilityStatement names this address in prose and declares nothing at it, exactly as it names /facade/spool - so the address belongs to this facade and sits under its mount beside the enrollment listing it is the other half of. What comes back is still a FHIR searchset Bundle under application/fhir+json, so this one router carries the Accept negotiation the rest of the facade API does not: a client that takes no JSON is refused before it runs. dhis2w_fhir_serve.routes.ServeRouters.negotiated is that requirement stated as data.

WHY A QuestionnaireResponse AND NOT A CLINICAL RESOURCE. The capture contract states one shape for a tracker event, the forwarder writes DHIS2 from that shape, and the instance-sourced example corpus is built by projecting real events into it. Serving the record in the same shape means the guide describes one event once - what a client may post is what a client reads back. Whether a DHIS2 event is also an Encounter, or its values also Observations, is the SDC $extract question the enrollment resource paper leaves open on purpose, and $extract runs over a QuestionnaireResponse: the form-faithful document is the substrate that line needs, not a rival to it. No clinical claim is made here, because DHIS2 states no mapping onto one.

THE SUBJECT NEED NOT BE A PERSON. The record is keyed by tracked entity UID and the subject of each document is whatever resource type the published D2TET_CM takes that entity's type onto - so a cold chain fridge's temperature readings are served exactly as a person's visits are, with Device in the subject's type where the fridge is registered as one. Nothing below branches on person-hood.

Live mode only, and gated twice. A compiled guide has no instance behind it and says so. [serve.tracked_entities] enabled = false takes the register away and this with it; [serve.tracked_entities] events = false takes this away alone, which is the posture for a deployment that publishes who its subjects are and not what was recorded about them. Each refusal names the key that produced it.

A page is a slice of the record, and the record is read whole. DHIS2 serves the events nested under the enrollments of one entity in a single request and offers no paging inside that projection, so the whole record arrives and this server pages the ordered result. Bundle.total is therefore every event this caller may see, counted under their own credentials - unlike a projection-served searchset, which states none. _count is honoured up to [serve.tracked_entities] page_size_limit and clamped rather than refused above it; _count=0 asks how long the record is and is answered with the number and no entries.

Every entry names the URL its document is really served at. One event is read at GET /facade/tracked-entities/{uid}/events/{eventUid}, and that is what the entry's fullUrl carries. QuestionnaireResponse/{id} on this server is deliberately NOT that URL: that address answers the spool, where a resource of that id is a receipt of what a client submitted rather than what DHIS2 now holds, and pointing a Bundle entry at it would merge the two.

A stage this project publishes no form for is stated rather than skipped quietly. Those events are counted in total - they are events the entity holds - carry no document, and the searchset closes with an outcome entry naming the stages, which is R4's own way for a server to say something about a search inside the search's answer. The alternative is a short answer a client reads as a short record.

The parameters are _count and page, and anything else is refused. A parameter this surface cannot apply, ignored, would answer a narrower question with the whole record - the same reason dhis2w_fhir_serve.routes.register refuses one.

Classes

FhirJsonResponse

Bases: Response

A response whose media type is FHIR's own, named so the facade API's document can say so.

The two record routes build their bodies themselves - a Bundle serialised with exclude_none, a projected resource dumped by alias - so they answer a Response rather than a model. Declaring the class is what tells FastAPI which media type to describe the answer under: without it the contract would say these reads answer application/json, which is the one thing about them that is not true.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/history.py
class FhirJsonResponse(Response):
    """A response whose media type is FHIR's own, named so the facade API's document can say so.

    The two record routes build their bodies themselves - a Bundle serialised with `exclude_none`, a
    projected resource dumped by alias - so they answer a `Response` rather than a model. Declaring
    the class is what tells FastAPI which media type to describe the answer under: without it the
    contract would say these reads answer `application/json`, which is the one thing about them that
    is not true.
    """

    media_type = FHIR_JSON_MEDIA_TYPE

RecordPage

Bases: BaseModel

One page of a record: the events on it, and where the pages either side of it are.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/history.py
class RecordPage(BaseModel):
    """One page of a record: the events on it, and where the pages either side of it are."""

    model_config = ConfigDict(frozen=True)

    events: tuple[RecordedEvent, ...] = ()
    total: int = 0
    cursor: SpoolCursor = SpoolCursor()
    next_cursor: SpoolCursor | None = None
    previous_cursor: SpoolCursor | None = None

Functions:

read_tracked_entity_events(request, tracked_entity_uid) async

Answer one page of one tracked entity's record, newest event first.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/history.py
@router.get(
    TRACKED_ENTITY_EVENTS_PATH,
    tags=[RECORD_TAG],
    summary="Read one tracked entity's record",
    description=(
        "Every event of every enrollment one tracked entity holds, newest first, each as the "
        "QuestionnaireResponse the program stage's own published form describes - the same shape a "
        "client posts a capture in, so a form is described once and read back as it was written.\n\n"
        "The answer is a FHIR searchset `Bundle` under `application/fhir+json`. `total` is every "
        "event this caller may see, counted under their own credentials; `_count` and `page` walk "
        "the pages, `_count=0` asks how long the record is, and any other parameter is refused "
        "rather than ignored. Events of a program stage this guide publishes no form for are counted "
        "in the total and named in a closing `outcome` entry, because a short answer would read as a "
        "short record.\n\n"
        "Live runs only, and gated twice: `[serve.tracked_entities] enabled = false` takes the "
        "register and this with it, and `events = false` takes this away alone."
    ),
    response_class=FhirJsonResponse,
    responses={200: {"model": Bundle, "description": "One page of the record as a FHIR searchset Bundle."}},
)
async def read_tracked_entity_events(request: Request, tracked_entity_uid: str) -> Response:
    """Answer one page of one tracked entity's record, newest event first."""
    reader = await _reader(request)
    _require_answerable_parameters(request)
    record = await _record(reader, tracked_entity_uid)
    service_base = record_base_url(request)
    if requested_entry_cap(request.query_params.get(COUNT_PARAMETER)) == 0:
        return _record_length(service_base, tracked_entity_uid, len(record.events))
    count = _requested_count(request)
    page = _page_of(record.events, requested_cursor(request.query_params.get(PAGE_PARAMETER)), count)
    projected = _projection(request).project(record, page.events)
    bundle = Bundle(
        type="searchset",
        total=page.total,
        link=_links(service_base, tracked_entity_uid, page, count),
        entry=[
            *(_entry(service_base, tracked_entity_uid, response) for response in projected.responses),
            *_unpublished_stage_entries(projected),
        ]
        or None,
    )
    return Response(content=bundle.model_dump_json(exclude_none=True, by_alias=True), media_type=FHIR_JSON_MEDIA_TYPE)

read_tracked_entity_event(request, tracked_entity_uid, event_uid) async

Answer one event of one tracked entity's record, which is what a page's entry links to.

The event is looked for inside the record rather than read by its own UID, which is what keeps the read entity-scoped: an event of somebody else is not this entity's record, and answering one here would let a caller walk from a person they may see to an event about a person they may not.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/history.py
@router.get(
    TRACKED_ENTITY_EVENT_PATH,
    tags=[RECORD_TAG],
    summary="Read one event of a record",
    description=(
        "One event of one tracked entity's record, as the QuestionnaireResponse its program stage's "
        "published form describes - which is the document the record page's own entry links to.\n\n"
        "The event is looked for inside that entity's record rather than read by its own UID, which "
        "is what keeps the read entity-scoped: an event of somebody else is not this entity's "
        "record, and answering one here would let a caller walk from a person they may see to an "
        "event about a person they may not."
    ),
    response_class=FhirJsonResponse,
    responses={200: {"model": QuestionnaireResponse, "description": "The event as the form's own response document."}},
)
async def read_tracked_entity_event(request: Request, tracked_entity_uid: str, event_uid: str) -> Response:
    """Answer one event of one tracked entity's record, which is what a page's entry links to.

    The event is looked for inside the record rather than read by its own UID, which is what keeps the
    read entity-scoped: an event of somebody else is not this entity's record, and answering one here
    would let a caller walk from a person they may see to an event about a person they may not.
    """
    record = await _record(await _reader(request), tracked_entity_uid)
    found = next((event for event in record.events if event.event_uid == event_uid), None)
    if found is None:
        raise NotFoundError(EVENT_NAME, event_uid)
    projected = _projection(request).project_event(record, found)
    if projected.response is None:
        raise NotFoundError(EVENT_NAME, event_uid)
    return JSONResponse(
        content=projected.response.model_dump(mode="json", exclude_none=True, by_alias=True),
        media_type=FHIR_JSON_MEDIA_TYPE,
    )

record_base_url(request)

Where this record is reached from, which is the base URL plus the mount it is served under.

request.base_url is the process's own base - FHIR's - and these routes answer under the facade API's mount beside it, so a link built from the base alone would name an address the FHIR read catch-all answers instead. Starlette states the mount as the request's root_path, and it is empty for an application that mounted these routers at its own base URL, which is exactly what such an application's links should then carry.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/history.py
def record_base_url(request: Request) -> str:
    """Where this record is reached from, which is the base URL plus the mount it is served under.

    `request.base_url` is the process's own base - FHIR's - and these routes answer under the facade
    API's mount beside it, so a link built from the base alone would name an address the FHIR read
    catch-all answers instead. Starlette states the mount as the request's `root_path`, and it is
    empty for an application that mounted these routers at its own base URL, which is exactly what
    such an application's links should then carry.
    """
    root_path = str(request.scope.get("root_path", ""))
    return f"{base_url(request)}{root_path}"

The patient summary

GET /{type}/{id}/$summary and GET /{type}/$summary?identifier= - the IPS's own two addresses, answered on the register resources R4 gives a person and refused by name on the rest. A summary reads nothing new: the subject is the resource the register already serves, and the doses come off the very record projection above, read through the immunisation mapping [ips.sections.immunizations] states. dhis2w_fhir.summary assembles the document; these two modules are what finds the doses and what answers the request, with the document's caveat riding the response as well as Composition.text.

summary

Reading one person's doses out of the record this facade already serves.

THE DOSES COME OUT OF THE RECORD PROJECTION AND NOT OUT OF A SECOND READ. GET /tracked-entities/{uid}/events answers one entity's events as the QuestionnaireResponse each stage's published form describes, and a summary is a projection of that record rather than a rival reading of the instance (docs/fhir/design/ips.md section 2, and R3: every read behind a summary is scoped to one tracked entity). So this module takes the very RecordProjection the record surface runs on, projects the events of the mapped stages through it, and reads the doses off the documents that come back. A value can therefore never be typed one way at /facade/tracked-entities/{uid}/events and another inside a summary: there is one projection and this is a reader of it.

WHAT A DOSE IS HERE. [ips.sections.immunizations] names the stages and, inside them, the data elements that each record a dose of one vaccine - the shape a DHIS2 immunisation form has, one element per vaccine with the value saying that the dose was given or which dose of the series it was. So a value under a nominated element on an event of a nominated stage is a dose, and nothing else is.

  • A boolean. true is a dose with no dose number; false is not a dose at all and produces nothing. DHIS2 says the vaccine was not given and states no reason, and R4 requires a statusReason on a not-done immunization - a reason this project will not invent.
  • A coded value. The dose number is the option's own display as the guide publishes it - Dose 2, IPT 1 - because that is what a person reading a summary needs and what the option actually means. The concept code behind it is a DHIS2 identifier under the default naming source and says nothing to a clinician.
  • Anything else. The value as the instance stored it, carried as the dose number.

A MAPPED STAGE THIS GUIDE PUBLISHES NO FORM FOR CONTRIBUTES NO DOSE, and is named rather than passed over. It is the same rule the record surface holds - a stage with no published form is counted and named rather than served as something else - and the summary states it in the Immunizations section's own narrative, so a reader never mistakes a guide that is narrower than the mapping for a person who was never vaccinated.

Classes

SummaryDoses

Bases: BaseModel

What one record's mapped stages hold: the doses, and the stages the guide could not read.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/summary.py
class SummaryDoses(BaseModel):
    """What one record's mapped stages hold: the doses, and the stages the guide could not read."""

    model_config = ConfigDict(frozen=True)

    doses: tuple[RecordedDose, ...] = ()
    unpublished_stage_uids: tuple[str, ...] = ()
    """Every mapped stage this guide publishes no form for, once each, in the order they occurred."""
Attributes
unpublished_stage_uids = () class-attribute instance-attribute

Every mapped stage this guide publishes no form for, once each, in the order they occurred.

Functions:

recorded_doses(record, projection, mapping)

Read every dose one person's record holds under one immunisation mapping, newest event first.

The record arrives ordered - newest first, by the instant the event occurred and then by its UID (dhis2w_fhir_serve.history.wire) - and that order is kept, which is what makes two assemblies of an unchanged record produce the same document (R4).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/summary.py
def recorded_doses(
    record: TrackedEntityRecord, projection: RecordProjection, mapping: ImmunizationsMapping
) -> SummaryDoses:
    """Read every dose one person's record holds under one immunisation mapping, newest event first.

    The record arrives ordered - newest first, by the instant the event occurred and then by its UID
    (`dhis2w_fhir_serve.history.wire`) - and that order is kept, which is what makes two assemblies
    of an unchanged record produce the same document (R4).
    """
    doses: list[RecordedDose] = []
    unpublished: dict[str, None] = {}
    for event in record.events:
        if event.program_stage_uid not in mapping.program_stages:
            continue
        projected = projection.project_event(record, event)
        if projected.response is None:
            unpublished.setdefault(projected.unpublished_stage_uid or event.program_stage_uid or "", None)
            continue
        index = projection.form_for(event.program_stage_uid)
        for item in _answered_items(projected.response.item or []):
            if item.linkId is None or not mapping.records_dose(event.program_stage_uid, item.linkId):
                continue
            question = None if index is None else index.questions.get(item.linkId)
            for answer in item.answer or []:
                dose_number = _dose_number(answer)
                if dose_number is _NOT_A_DOSE:
                    continue
                doses.append(
                    RecordedDose(
                        event_uid=event.event_uid,
                        data_element_uid=item.linkId,
                        display=None if question is None else question.display,
                        occurred_at=projected.response.authored,
                        dose_number=dose_number,
                    )
                )
    return SummaryDoses(doses=tuple(doses), unpublished_stage_uids=tuple(unpublished))

summary

$summary - one person's International Patient Summary, assembled from the register and the record.

THE OPERATION ALREADY HAD A NAME BEFORE THIS PROJECT EXISTED. The IPS publishes OperationDefinition/summary on Patient, at instance level ([base]/Patient/[id]/$summary) and at type level ([base]/Patient/$summary, where "the requestor SHALL provide an identifier"), and the IPS Server CapabilityStatement declares exactly that one operation on exactly that one resource. So a client that speaks IPS reaches this without learning a route this project invented, which is R6 in docs/fhir/design/ips.md section 8.

IT IS SCOPED TO THE PEOPLE. The register serves nine FHIR resource types over whatever tracked entity types a project maps onto them, and $summary on a Specimen or a Location is not a narrower patient summary - it is a document nobody has defined. So the operation is answered on the register resources R4 gives a person (register.projection.PERSON_RESOURCE_TYPES) and refused by name on the rest, with the refusal saying which resource this register serves.

NOTHING NEW IS READ. The subject is the very resource GET /{RegisterType}/{uid} answers with, and the doses come out of the record projection GET /facade/tracked-entities/{uid}/events runs on. A summary is a second reading of reads that already exist and not a third way into DHIS2.

That is two reads of the tracked entity's own address rather than one, and deliberately: each surface states the DHIS2 projection it needs in its own module - the register asks for the attributes, the record asks for the enrollments and their data values - and merging them here would make one surface's read depend on the other's field list. Both are entity-scoped and both carry the credentials of whoever asked, which is the whole of what R3 requires.

THE GATES, IN THE ORDER THEY COST SOMETHING. [ips] enabled first, because a project that publishes no summary publishes none however the process was started and a refusal costs no round trip. Then the register's own gates, which decide whether there is a person to be about. Then [serve.tracked_entities] events, and only where a clinical section is mapped: a summary whose sections are all empty reads no record, so refusing it for the record's sake would refuse a request that was never going to make one.

THE TYPE-LEVEL FORM RESOLVES THROUGH THE REGISTER'S OWN IDENTIFIER SEARCH. ?identifier= is the same token grammar GET /Patient?identifier= answers - a system-qualified token naming one key, or a bare value tried against every key - so a client that can find somebody can summarise them without learning a second search. An identifier several people hold is refused rather than answered: a summary is about one person, and handing back the first match would be this server picking which one.

THE CAVEAT RIDES TWICE. The document says what it is and is not in Composition.text, and the response says the same sentence in a header beside it - the two-place idiom dhis2w_fhir_serve.projection.serving argues for the projection's as-of instant, for the same reason. dhis2w_fhir.summary is where the sentence is written and why.

Classes

Functions:

read_patient_summary(request, resource_type, tracked_entity_uid) async

Assemble the summary of one person, named by their DHIS2 tracked entity UID.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/summary.py
@router.get(INSTANCE_SUMMARY_PATH)
async def read_patient_summary(request: Request, resource_type: str, tracked_entity_uid: str) -> Response:
    """Assemble the summary of one person, named by their DHIS2 tracked entity UID."""
    lookup = await _summary_lookup(request, resource_type)
    entity = await registered_tracked_entity(lookup, tracked_entity_uid)
    if entity is None:
        raise NotFoundError(resource_type, tracked_entity_uid)
    return await _summary_response(request, lookup, resource_type, entity)

search_patient_summary(request, resource_type) async

Assemble the summary of the one person an identifier names, refusing a value that names several.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/summary.py
@router.get(TYPE_SUMMARY_PATH)
async def search_patient_summary(request: Request, resource_type: str) -> Response:
    """Assemble the summary of the one person an identifier names, refusing a value that names several."""
    lookup = await _summary_lookup(request, resource_type)
    tokens = [
        identifier_token(IDENTIFIER_PARAMETER, value)
        for name, raw in request.query_params.multi_items()
        if name == IDENTIFIER_PARAMETER
        for value in alternatives(name, raw)
    ]
    _require_answerable_parameters(request, resource_type)
    if not tokens:
        raise BadOperationError(
            f"`$summary` on `{resource_type}` names no person: state `identifier={{system}}|{{value}}`, or "
            f"`identifier={{value}}` to look under every key this register searches, or ask for one person "
            f"at `{resource_type}/{{uid}}/$summary`"
        )
    try:
        found = await matching_entities(lookup, resource_type, tokens)
    except Dhis2ClientError as error:
        raise UpstreamError(
            f"the DHIS2 instance did not answer the tracked entity search: {upstream_refusal_text(error)}"
        ) from error
    if not found:
        raise NotFoundError(resource_type, ", ".join(token.value for token in tokens))
    if len(found) > 1:
        named = ", ".join(entity.trackedEntity or "" for entity in found)
        raise BadOperationError(
            f"that identifier names {len(found)} people on this instance ({named}), and a summary is about "
            f"one: ask for the one you mean at `{resource_type}/{{uid}}/$summary`"
        )
    return await _summary_response(request, lookup, resource_type, found[0])

Read and search routes

GET /{type}/{id} and GET /{type}, answered from the store for every definitional resource and from the spool for QuestionnaireResponse.

read

Read and search over what the facade serves: GET /{type}/{id} and GET /{type}.

These two routes match any path of their shape, so they mount last - every fixed path the facade serves is registered ahead of them. A type outside the served set is refused here rather than falling through to a bare 404, so a client learns the difference between "this server does not serve Specimen" and "there is no Questionnaire with that id".

The two catch-alls answer from three sources. Every definitional resource comes from the store, byte-faithful to what the IG published - ConceptMap included, which is read here as a document and translated through at /ConceptMap/$translate, the two being different ways to ask about the same published maps, and the guide's own conformance resources included too, which is what makes a served project self-hosting: a profile canonical found on a response is resolved with url= on the same search this module answers for every other type. QuestionnaireResponse comes from the spool, where each resource is a receipt of a submission - what a client sent, not what DHIS2 now holds. And the register's resource types - the ones the published D2TET_CM takes a tracked entity type onto - come from the DHIS2 instance, per request, and are dispatched to dhis2w_fhir_serve.routes.register before anything here reads the store. Which types those are is known only once the store has been loaded, so the dispatch is a lookup at request time rather than a second pair of routes mounted ahead of these; the register module is imported inside the handlers because it imports the search grammar below.

A receipt is answered whatever lifecycle state it is in. d2w fhir forward renames a drained receipt into forwarded/ or rejected/, and a read that started 404-ing at that moment would expire the id a client was handed at capture time on a schedule nothing told it about. Which state a receipt is in is not a QuestionnaireResponse element, so it is not stated here; GET /facade/spool answers that, along with the rest of the receipt envelope.

Search over the store is lenient in FHIR's own sense: an unrecognised parameter is ignored rather than refused, and the Bundle's self link echoes only the parameters that were honored, so a client can see what the server actually applied. The register searches this module dispatches to refuse instead, for the reason dhis2w_fhir_serve.routes.register gives: what an ignored parameter costs there is the whole register handed back as a match set.

A QuestionnaireResponse search is paged, with the same _count and page pair the register listing uses: Bundle.total is the whole searchset on every page of one walk, and a client's whole job is to follow the next link. The store searches are not paged - what the guide published is a fixed set that a client asked for by name, and this facade serves one project's worth of it - but they honor _count all the same, as the cap it is on those searches rather than as a page size. See requested_entry_cap.

alternatives, identifier_token, base_url, and bundle_response are the parts of that grammar that are not about the store at all - how FHIR spells a token, and what a searchset Bundle looks like - so dhis2w_fhir_serve.routes.register builds its answers with the same four, and a result set from DHIS2 is shaped exactly like one from the store.

Classes

HonoredParameter

Bases: BaseModel

One search parameter the facade applied, as the Bundle self link echoes it back.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
class HonoredParameter(BaseModel):
    """One search parameter the facade applied, as the Bundle `self` link echoes it back."""

    model_config = ConfigDict(frozen=True)

    name: str
    value: str

ParsedSearch

Bases: BaseModel

A store search: the query it runs, and the parameters the self link reports as applied.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
class ParsedSearch(BaseModel):
    """A store search: the query it runs, and the parameters the `self` link reports as applied."""

    model_config = ConfigDict(frozen=True)

    query: SearchQuery = Field(default_factory=SearchQuery)
    honored: tuple[HonoredParameter, ...] = ()

ParsedResponseSearch

Bases: BaseModel

A spool search: the receipts it selects, and the parameters the self link reports as applied.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
class ParsedResponseSearch(BaseModel):
    """A spool search: the receipts it selects, and the parameters the `self` link reports as applied."""

    model_config = ConfigDict(frozen=True)

    ids: tuple[str, ...] = ()
    questionnaires: tuple[str, ...] = ()
    honored: tuple[HonoredParameter, ...] = ()

Functions:

requested_entry_cap(stated)

How many entries a client asked one searchset to carry, or None when it named no _count.

_count on a store search is a cap and not a pagination scheme, and deliberately so. The store is one project's published artifacts, searched by name; a client asking for Questionnaire holding an identifier wants the artifacts that identifier names, not the first page of them. So a capped search states Bundle.total for every match and hands back the first _count of them with an honest self link, and offers no next cursor to follow: there is no walk to continue, only a result the client chose to see less of. The register's identifier search answers the same way and for the same reason - what an identifier matched is a result set, not a listing.

_count=0 is R4's request for the total alone: the Bundle states how many matched and carries no entry at all. A _count that is not a whole number, or is negative, is a malformed query rather than an ambitious one, and is refused as such.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def requested_entry_cap(stated: str | None) -> int | None:
    """How many entries a client asked one searchset to carry, or None when it named no `_count`.

    `_count` on a store search is a cap and not a pagination scheme, and deliberately so. The store
    is one project's published artifacts, searched by name; a client asking for `Questionnaire`
    holding an identifier wants the artifacts that identifier names, not the first page of them. So
    a capped search states `Bundle.total` for every match and hands back the first `_count` of them
    with an honest `self` link, and offers no `next` cursor to follow: there is no walk to continue,
    only a result the client chose to see less of. The register's identifier search answers the same
    way and for the same reason - what an identifier matched is a result set, not a listing.

    `_count=0` is R4's request for the total alone: the Bundle states how many matched and carries
    no entry at all. A `_count` that is not a whole number, or is negative, is a malformed query
    rather than an ambitious one, and is refused as such.
    """
    if stated is None:
        return None
    try:
        cap = int(stated)
    except ValueError as error:
        raise BadSearchError(f"`{COUNT_PARAMETER}` was given `{stated}`, which is not a number of rows") from error
    if cap < 0:
        raise BadSearchError(f"`{COUNT_PARAMETER}` was given `{stated}`: a result carries no negative number of rows")
    return cap

Read _id, url, and identifier into a store query, ignoring every other parameter.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def parse_store_search(params: QueryParams) -> ParsedSearch:
    """Read `_id`, `url`, and `identifier` into a store query, ignoring every other parameter."""
    ids: list[str] = []
    urls: list[str] = []
    identifiers: list[IdentifierToken] = []
    honored: list[HonoredParameter] = []
    for name, raw in params.multi_items():
        if name == "_id":
            ids.extend(alternatives(name, raw))
        elif name == "url":
            urls.extend(alternatives(name, raw))
        elif name == "identifier":
            identifiers.extend(identifier_token(name, value) for value in alternatives(name, raw))
        else:
            continue
        honored.append(HonoredParameter(name=name, value=raw))
    query = SearchQuery(ids=tuple(ids), urls=tuple(urls), identifiers=tuple(identifiers))
    return ParsedSearch(query=query, honored=tuple(honored))

Read _id and questionnaire into a spool search, ignoring every other parameter.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def parse_response_search(params: QueryParams) -> ParsedResponseSearch:
    """Read `_id` and `questionnaire` into a spool search, ignoring every other parameter."""
    ids: list[str] = []
    questionnaires: list[str] = []
    honored: list[HonoredParameter] = []
    for name, raw in params.multi_items():
        if name == "_id":
            ids.extend(alternatives(name, raw))
        elif name == "questionnaire":
            questionnaires.extend(alternatives(name, raw))
        else:
            continue
        honored.append(HonoredParameter(name=name, value=raw))
    return ParsedResponseSearch(ids=tuple(ids), questionnaires=tuple(questionnaires), honored=tuple(honored))

search_resource_type(request, resource_type) async

Search one served resource type, from the DHIS2 instance for a register type and from the store otherwise.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
@router.get("/{resource_type}")
async def search_resource_type(request: Request, resource_type: str) -> Response:
    """Search one served resource type, from the DHIS2 instance for a register type and from the store otherwise."""
    from dhis2w_fhir_serve.routes.register import register_resource_types, search_register

    context = serve_context(request)
    if resource_type in register_resource_types(request):
        return await search_register(request, resource_type)
    _require_served(resource_type)
    service_base = base_url(request)
    if resource_type == QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE:
        return await _search_receipts(request, context.spool, service_base)
    parsed = parse_store_search(request.query_params)
    entries = [
        _bundle_entry(service_base, entry.resource_type, entry.resource_id, entry.body)
        for entry in context.store.search(resource_type, parsed.query)
    ]
    return bundle_response(
        service_base,
        resource_type,
        parsed.honored,
        entries,
        requested_entry_cap(request.query_params.get(COUNT_PARAMETER)),
    )

read_resource(request, resource_type, resource_id) async

Answer one resource: from the DHIS2 instance for a register type, from the store or the spool otherwise.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
@router.get("/{resource_type}/{resource_id}")
async def read_resource(request: Request, resource_type: str, resource_id: str) -> Response:
    """Answer one resource: from the DHIS2 instance for a register type, from the store or the spool otherwise."""
    from dhis2w_fhir_serve.routes.register import read_registered_entity, register_resource_types

    context = serve_context(request)
    if resource_type in register_resource_types(request):
        return await read_registered_entity(request, resource_type, resource_id)
    _require_served(resource_type)
    if resource_type == QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE:
        receipt = await run_in_threadpool(context.spool.get, resource_id)
        if receipt is None:
            raise NotFoundError(resource_type, resource_id)
        return JSONResponse(content=receipt.response, media_type=FHIR_JSON_MEDIA_TYPE)
    entry = context.store.by_type_and_id(resource_type, resource_id)
    if entry is None:
        raise NotFoundError(resource_type, resource_id)
    return JSONResponse(content=entry.body, media_type=FHIR_JSON_MEDIA_TYPE)

alternatives(name, raw)

Split one parameter into its comma-separated alternatives, refusing an empty one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def alternatives(name: str, raw: str) -> list[str]:
    """Split one parameter into its comma-separated alternatives, refusing an empty one."""
    values = [value.strip() for value in raw.split(",")]
    if any(not value for value in values):
        raise BadSearchError(f"`{name}` was given an empty value")
    return values

identifier_token(parameter, value)

Read a system|value token; a bare value, or an empty system, matches the value in any system.

The parameter is named so the refusal names it: identifier and _tag are both token searches over the same grammar, and a client told its identifier was malformed when it wrote a _tag would look in the wrong half of its query.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def identifier_token(parameter: str, value: str) -> IdentifierToken:
    """Read a `system|value` token; a bare value, or an empty system, matches the value in any system.

    The parameter is named so the refusal names it: `identifier` and `_tag` are both token searches
    over the same grammar, and a client told its `identifier` was malformed when it wrote a `_tag`
    would look in the wrong half of its query.
    """
    if "|" not in value:
        return IdentifierToken(value=value)
    system, _, token = value.partition("|")
    if not token:
        raise BadSearchError(f"`{parameter}` token `{value}` names a system but no value")
    return IdentifierToken(system=system or None, value=token)

base_url(request)

The service base every fullUrl and self link is built from, without its trailing slash.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def base_url(request: Request) -> str:
    """The service base every `fullUrl` and `self` link is built from, without its trailing slash."""
    return str(request.base_url).rstrip("/")

bundle_response(base_url, resource_type, honored, entries, cap=None)

Serialise the result set: total is every match, entry is what _count let through.

The self link names the parameters that were applied and the cap that was applied with them, so a client reading it back sees both what selected the matches and how many of them it is holding. total is the whole match set whether or not the cap cut it short - R4 defines it as the number of resources that matched the search, not the number on this page.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def bundle_response(
    base_url: str,
    resource_type: str,
    honored: tuple[HonoredParameter, ...],
    entries: list[BundleEntry],
    cap: int | None = None,
) -> Response:
    """Serialise the result set: `total` is every match, `entry` is what `_count` let through.

    The `self` link names the parameters that were applied and the cap that was applied with them,
    so a client reading it back sees both what selected the matches and how many of them it is
    holding. `total` is the whole match set whether or not the cap cut it short - R4 defines it as
    the number of resources that matched the search, not the number on this page.
    """
    parameters = [(parameter.name, parameter.value) for parameter in honored]
    if cap is not None:
        parameters.append((COUNT_PARAMETER, str(cap)))
    query = urlencode(parameters)
    self_url = f"{base_url}/{resource_type}?{query}" if query else f"{base_url}/{resource_type}"
    served = entries if cap is None else entries[:cap]
    bundle = Bundle(
        type="searchset",
        total=len(entries),
        link=[BundleLink(relation="self", url=self_url)],
        entry=served or None,
    )
    return Response(
        content=bundle.model_dump_json(exclude_none=True, by_alias=True),
        media_type=FHIR_JSON_MEDIA_TYPE,
    )

total_only_response(base_url, resource_type, honored, total)

Answer _count=0 on a paged search: how many matched, and none of them.

The paged searches cannot express this through bundle_response, because their total is a count of the whole listing rather than of the entries they were handed. A total of None is the answer a register listing gives when the instance stated no count for one of the types in it, and the Bundle then states no total rather than a number nobody counted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/read.py
def total_only_response(
    base_url: str,
    resource_type: str,
    honored: tuple[HonoredParameter, ...],
    total: int | None,
) -> Response:
    """Answer `_count=0` on a paged search: how many matched, and none of them.

    The paged searches cannot express this through `bundle_response`, because their total is a count
    of the whole listing rather than of the entries they were handed. A total of None is the answer
    a register listing gives when the instance stated no count for one of the types in it, and the
    Bundle then states no total rather than a number nobody counted.
    """
    parameters = [(parameter.name, parameter.value) for parameter in honored]
    parameters.append((COUNT_PARAMETER, "0"))
    bundle = Bundle(
        type="searchset",
        total=total,
        link=[BundleLink(relation="self", url=f"{base_url}/{resource_type}?{urlencode(parameters)}")],
    )
    return Response(
        content=bundle.model_dump_json(exclude_none=True, by_alias=True),
        media_type=FHIR_JSON_MEDIA_TYPE,
    )

Capture route

POST /QuestionnaireResponse - the one write the facade accepts.

capture

POST /QuestionnaireResponse: the one write the facade accepts, and the refusal in its place.

A project that sets [serve] capture = false mounts refusal_router instead of router, and the address answers 405 with an OperationOutcome naming the key. The refusal is a route rather than the 405 Starlette would produce on its own from the read router's GET: both are the same status and the same shape, and only one of them says why. Nothing else about the resource type moves - the receipts already spooled are read, searched, and counted at the same paths.

The interaction is FHIR's create, and it answers the way R4 says a create answers - 201, a Location header naming where the created resource is served from, and an OperationOutcome saying what happened. What is created is a receipt: the submission as it arrived, stamped with the id it is now served under, plus every warning the server had to record about it.

The body is read as raw bytes rather than through a FastAPI request model. A capture is validated against the served IG - its questionnaires, its terminology, its profiles - which is a contract a FastAPI parameter model cannot express, and reading the bytes here is also what keeps the stored copy byte-faithful to what the client sent.

Nothing here talks to DHIS2. Accepting a capture means the submission was understood and kept, not that it has been written to an instance. Kept means durable: the receipt is fsynced and its directory entry with it before the 201 goes out, so a 201 is a promise that survives power loss.

A CORRECTION OR A WITHDRAWAL IS REFUSED HERE where the project's dials are off. [forward] corrections and [forward] withdrawals are read off fhir.toml onto the capture state, and a submission carrying status = "amended" or status = "entered-in-error" against an off dial is answered 422 with the key named. With the dial on it is stored like any other receipt, status and all - see dhis2w_fhir_serve.capture.validate.

WHO SUBMITTED IT is stamped on the receipt where this run established anybody. Under [serve] auth = "dhis2" the check has already read /api/me as the caller, so the username DHIS2 answered with is on the request and goes onto the envelope. Under every other posture there is no person to name and the field stays absent - a static token is not a submitter, and a server that authenticates nobody knows nobody.

Classes

CaptureState

Bases: BaseModel

What a capture request needs beyond the serve context: the project's names, dials, and index cache.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/capture.py
class CaptureState(BaseModel):
    """What a capture request needs beyond the serve context: the project's names, dials, and index cache."""

    model_config = ConfigDict(frozen=True)

    naming: CaptureNaming
    indexes: CaptureIndexCache = Field(default_factory=CaptureIndexCache)
    postures: CaptureLifecyclePostures = CaptureLifecyclePostures()
    """Whether this project receives a submission that corrects, or one that retracts, a forwarded receipt.

    A project-level fact read off `fhir.toml` exactly as the naming above is, and held here for the
    same reason: it is settled once by the project and never by a request. `ServeSettings` carries
    what the *run* was invoked with; these two are what the project says, and `d2w fhir forward`
    reads the same keys out of the same file.
    """
Attributes
postures = CaptureLifecyclePostures() class-attribute instance-attribute

Whether this project receives a submission that corrects, or one that retracts, a forwarded receipt.

A project-level fact read off fhir.toml exactly as the naming above is, and held here for the same reason: it is settled once by the project and never by a request. ServeSettings carries what the run was invoked with; these two are what the project says, and d2w fhir forward reads the same keys out of the same file.

Functions:

refuse_questionnaire_response(request) async

Refuse a submission this project does not receive, naming the key that decided it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/capture.py
@refusal_router.post(f"/{QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}")
async def refuse_questionnaire_response(request: Request) -> Response:
    """Refuse a submission this project does not receive, naming the key that decided it."""
    raise CaptureDisabledError(QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE)

create_questionnaire_response(request) async

Receive one QuestionnaireResponse: validate it against the served IG, store it, and say where it lives.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/capture.py
@router.post(f"/{QUESTIONNAIRE_RESPONSE_RESOURCE_TYPE}")
async def create_questionnaire_response(request: Request) -> Response:
    """Receive one QuestionnaireResponse: validate it against the served IG, store it, and say where it lives."""
    _require_json_body(request)
    context = serve_context(request)
    state = capture_state(request)
    try:
        validated = validate_response(
            await request.body(),
            state.indexes,
            state.naming,
            context.store,
            context.settings.strict_codes,
            state.postures,
        )
    except CaptureRejection as rejection:
        return JSONResponse(
            status_code=rejection.http_status,
            content=rejection_outcome(rejection.issues).model_dump(mode="json", exclude_none=True, by_alias=True),
            media_type=FHIR_JSON_MEDIA_TYPE,
        )
    envelope = _receipt(validated, request)
    # Off the event loop: the write is a temporary file, an fsync of it, a rename, and an fsync of
    # the directory - blocking work the facade must not do inline, since the point of the fsyncs is
    # that they wait for the device.
    await run_in_threadpool(context.spool.save, envelope)
    return _created(request, envelope.response_id, validated.warnings)

capture_state(request)

The capture state of this app, built from the served project the first time a form is read.

Shared with the record surface, which projects one entity's events through the same naming and the same index cache a submission is validated against - one form typed one way, whichever direction a value is travelling. dhis2w_fhir_serve.routes.history is the other caller.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/capture.py
def capture_state(request: Request) -> CaptureState:
    """The capture state of this app, built from the served project the first time a form is read.

    Shared with the record surface, which projects one entity's events through the same naming and
    the same index cache a submission is validated against - one form typed one way, whichever
    direction a value is travelling. `dhis2w_fhir_serve.routes.history` is the other caller.
    """
    held: CaptureState | None = getattr(request.app.state, CAPTURE_STATE_ATTRIBUTE, None)
    if held is not None:
        return held
    project = serve_context(request).project
    state = CaptureState(
        naming=CaptureNaming.from_project(project),
        postures=CaptureLifecyclePostures.from_project(project),
    )
    setattr(request.app.state, CAPTURE_STATE_ATTRIBUTE, state)
    return state

Generating a response

GET|POST /Questionnaire/{id}/$generate and the synthesizer behind it: one served form filled in from the very capture index a submission is validated against, so the generated response posts back at the same server for a 201.

generate

GET|POST /Questionnaire/{id}/$generate - the instance-level operation that fills a served form.

The operation answers one served Questionnaire with a synthetic QuestionnaireResponse: every question answered with a value its own type, bounds, and terminology binding admit, wrapped in the context its form kind's response profile requires. What makes it worth serving is that the answer is immediately postable - $generate output sent to this server's own POST /QuestionnaireResponse answers 201, which is the round trip a capture UI's fill-with-test-data button and an API-driven stress corpus both stand on. tests/test_generate_endpoint.py holds that invariant per form kind.

The spool is read as part of answering: a generated stage response answers against the tracked entity and the enrollment a spooled registration of the same program minted, so the operation's output is a function of the project's receipts as well as its forms - see dhis2w_fhir_serve.synthesize.

This is a custom operation, deliberately not SDC's $populate. $populate means fill this form from real context about a real subject; $generate invents its data, and a client that knows what $populate means would be misled by seeing it here. The IG publishes the OperationDefinition, and /metadata declares it on the Questionnaire resource entry beside the read interactions.

The path is fixed, so this router mounts ahead of the read catch-alls: /{resource_type}/{resource_id} matches /Questionnaire/BfMAe6Itzgt/$generate just as happily and would answer it as a read.

Classes

UngeneratableFormError

Bases: ServeError

The Questionnaire is served, but it is not one this server can generate a response to.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/generate.py
class UngeneratableFormError(ServeError):
    """The Questionnaire is served, but it is not one this server can generate a response to."""

    status_code = 422
    issue_code = "not-supported"

    def __init__(self, resource_id: str, diagnostics: str) -> None:
        super().__init__(f"`Questionnaire/{resource_id}` cannot be generated against: {diagnostics}")
        self.resource_id = resource_id

Functions:

generate_from_questionnaire(request, resource_id) async

Answer one served form with a synthetic response, drawn from the seed the query names.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/generate.py
@router.get(GENERATE_PATH)
async def generate_from_questionnaire(request: Request, resource_id: str) -> Response:
    """Answer one served form with a synthetic response, drawn from the seed the query names."""
    return _generated(request, resource_id, read_seed(request.query_params))

post_generate_from_questionnaire(request, resource_id) async

The POST spelling of the same operation, taking its seed from a Parameters body or the query.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/generate.py
@router.post(GENERATE_PATH)
async def post_generate_from_questionnaire(request: Request, resource_id: str) -> Response:
    """The POST spelling of the same operation, taking its seed from a Parameters body or the query."""
    body_seed = read_body_seed(await request.body())
    return _generated(request, resource_id, body_seed if body_seed is not None else read_seed(request.query_params))

read_seed(params)

Read the seed query parameter, refusing anything the operation's integer input cannot carry.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/generate.py
def read_seed(params: QueryParams) -> int | None:
    """Read the `seed` query parameter, refusing anything the operation's `integer` input cannot carry."""
    raw = params.get(GENERATE_SEED_PARAMETER)
    if raw is None or raw == "":
        return None
    return _checked_seed(raw)

read_body_seed(raw_body)

Read seed off a POSTed Parameters body, which R4 allows a client to invoke an operation with.

An empty body is how a client says "any seed", and is the shape a bare POST .../$generate sends. A body that is not a Parameters resource is refused rather than ignored: a client that meant to name a seed and misspelled the resource would otherwise be answered with a different response than it asked for, silently.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/generate.py
def read_body_seed(raw_body: bytes) -> int | None:
    """Read `seed` off a POSTed Parameters body, which R4 allows a client to invoke an operation with.

    An empty body is how a client says "any seed", and is the shape a bare `POST .../$generate` sends.
    A body that is not a Parameters resource is refused rather than ignored: a client that meant to
    name a seed and misspelled the resource would otherwise be answered with a different response
    than it asked for, silently.
    """
    if not raw_body.strip():
        return None
    try:
        payload: Any = json.loads(raw_body)
    except (json.JSONDecodeError, UnicodeDecodeError) as error:
        raise BadOperationError(f"the request body is not valid JSON ({error})") from error
    try:
        parameters = Parameters.model_validate(payload)
    except ValidationError as error:
        raise BadOperationError(f"the request body is not a Parameters resource ({error})") from error
    for parameter in parameters.parameter or []:
        if parameter.name != GENERATE_SEED_PARAMETER:
            continue
        if parameter.valueInteger is not None:
            return _checked_seed(str(parameter.valueInteger))
        if parameter.valueString is not None:
            return _checked_seed(parameter.valueString)
        raise BadOperationError(f"the `{GENERATE_SEED_PARAMETER}` parameter carries no `valueInteger`")
    return None

synthesize

Synthesising one QuestionnaireResponse from a served form - what Questionnaire/{id}/$generate answers.

A served Questionnaire already says everything an answer to it has to satisfy: the value[x] element each question takes, the inclusive bounds a numeric one admits, whether it repeats, and the ValueSet a coded one is drawn from. The capture index reads exactly those facts to check a submission; this module reads the same index to write one. The invariant that makes the operation worth having is the round trip - a generated response posted back to this server's own /QuestionnaireResponse answers 201 - and the way it is held is that both directions read one index, so a rule can never be enforced on receipt without being honoured on generation.

An answer is drawn on the axis DHIS2 grades it on, which is the DHIS2 value type rather than the FHIR item type the question is asked as. R4 offers one string item for a coordinate, a phone number, an email address, a letter, and a username, and DHIS2 parses all five: a [longitude,latitude] pair for a COORDINATE, an address for an EMAIL, one letter for a LETTER. A value spelled outside what the type admits is refused at import with E1302, so the value type decides the value and the item type only decides which value[x] element carries it (dhis2w_fhir.seeded_format_constrained_value, the one rule this server and the guide's example corpus both draw from). The value types DHIS2 stores a document or a UID reference for - a file, an image, GeoJSON, a REFERENCE, a TRACKER_ASSOCIATE - are left unanswered rather than invented, for the same reason a question bound to unpublished terminology is: an invented answer names a target nothing resolves.

Nothing here invents terminology. A coded answer is a concept the served CodeSystem really publishes, carried in the exact spelling the contract asks for (the concept code, never the DHIS2 code or UID fall-backs), so a strict-codes server accepts what a lenient one does. A question bound to terminology this project never published is left unanswered rather than answered with something invented. The attribute option combo an aggregate response is filed under is drawn the same way, out of the very vocabulary the form declares, so a data set on a non-default category combo generates a response its own capture path accepts.

The organisation unit a response reports for is part of the seeded draw, not a fixture: the same seed names the same unit and different seeds range over the whole admitted set - the form's published assignment where it has one, the served registry where it does not. And a unique tracked entity attribute is never answered with a constant: DHIS2 refuses the second registration carrying a repeated unique value with E1064, so the answer embeds the response's own minted tracked-entity UID - the one value no other generated registration holds - through the same rule the examples emitter uses (dhis2w_fhir.distinct_unique_value).

A generated stage response answers against a person this server already knows. A tracker event names a tracked entity and an enrollment, and DHIS2 refuses one naming a pair that never existed with E1079 and E1313, so the pair is adopted from a registration receipt in this project's own spool - the same join the capture UI's enrollment picker makes, on the program the two forms share. Only when the spool holds no registration of that program does a stage response mint a pair of its own. Which means a generated stage response is a function of (questionnaire, store, spool, seed, today): the same seed against the same spool state produces the same bytes, and running d2w fhir forward between two calls can move which pair is adopted, because a forwarded registration is one DHIS2 already holds.

A generated registration dates the enrollment it mints, and dates the incident that enrollment follows exactly when the form says its program collects one. That is read off the form's own D2CollectsIncidentDate declaration through the capture index, so a compiled store and a --live store generate the same envelope for the same program: DHIS2 refuses a registration missing the incident date of a program that collects one with E1023, and the declaration is what keeps a generated response postable in both modes.

The three instants such a registration carries are ordered rather than drawn independently: the incident is on or before the enrolment, and the enrolment on or before the moment the document was authored. That is the only order the three facts can stand in - an enrolment follows the incident it is about, and a document is written no earlier than the enrolment it states - and a reader who found a birth dated after the enrolment it opened read a document about nobody. All three are drawn from the seeded stream inside the one window and then sorted onto that order, so the same seed still spells the same three instants and no draw is ever repeated to satisfy the constraint.

A generated aggregate response reports for the period type its own form declares. Every served Questionnaire carries the data set's D2PeriodType - the FSH template writes it and so does the JSON builder, so a compiled store and a --live one state the same type - and Monthly is what a form declaring none reports under. The response always carries the newest completed period of whichever type was decided, so the value moves with the calendar and nothing else.

One fact a served Questionnaire does not carry, and how it is decided here: TRUE_ONLY versus BOOLEAN. The emitter answers both DHIS2 value types as a boolean item, so a generated answer to either is a random true or false. A TRUE_ONLY data element only ever holds true in DHIS2, and a generated false for one is a value the form admits but the instance would not store.

Classes

DateWindow

Bases: BaseModel

The span a generated date, dateTime, or time answer is drawn from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
class DateWindow(BaseModel):
    """The span a generated date, dateTime, or time answer is drawn from."""

    model_config = ConfigDict(frozen=True)

    start_date: datetime.date
    end_date: datetime.date

    @classmethod
    def of_period(cls, period: PeriodValue) -> DateWindow:
        """The window one reporting period covers."""
        return cls(start_date=period.start_date, end_date=period.end_date)

    @classmethod
    def recent(cls, today: datetime.date) -> DateWindow:
        """The thirty days before `today`, which is where an event carrying no period sits."""
        return cls(start_date=today - datetime.timedelta(days=_EVENT_WINDOW_DAYS), end_date=today)

    def pick_date(self, generator: random.Random) -> datetime.date:
        """A seeded day inside the window, its last day excluded so the value is already past."""
        span = max((self.end_date - self.start_date).days, 1)
        return self.start_date + datetime.timedelta(days=generator.randrange(span))
Methods:
of_period(period) classmethod

The window one reporting period covers.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
@classmethod
def of_period(cls, period: PeriodValue) -> DateWindow:
    """The window one reporting period covers."""
    return cls(start_date=period.start_date, end_date=period.end_date)
recent(today) classmethod

The thirty days before today, which is where an event carrying no period sits.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
@classmethod
def recent(cls, today: datetime.date) -> DateWindow:
    """The thirty days before `today`, which is where an event carrying no period sits."""
    return cls(start_date=today - datetime.timedelta(days=_EVENT_WINDOW_DAYS), end_date=today)
pick_date(generator)

A seeded day inside the window, its last day excluded so the value is already past.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
def pick_date(self, generator: random.Random) -> datetime.date:
    """A seeded day inside the window, its last day excluded so the value is already past."""
    span = max((self.end_date - self.start_date).days, 1)
    return self.start_date + datetime.timedelta(days=generator.randrange(span))

TrackerPair

Bases: BaseModel

The tracked entity and the enrollment one registration minted - what a stage response answers against.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
class TrackerPair(BaseModel):
    """The tracked entity and the enrollment one registration minted - what a stage response answers against."""

    model_config = ConfigDict(frozen=True)

    tracked_entity_uid: str
    enrollment_uid: str

Functions:

draw_seed()

Draw the seed a $generate call that named none is answered from.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
def draw_seed() -> int:
    """Draw the seed a `$generate` call that named none is answered from."""
    return random.randrange(MAXIMUM_SEED + 1)  # noqa: S311 - a reproducibility handle, not a secret

generate_response(questionnaire, index, naming, store, *, seed, today, spool=None)

Generate one synthetic response to a served form: its context, then an answer to every question.

The whole document is a function of (questionnaire, store, spool, seed, today). Two terms move on their own: today decides which completed reporting period an aggregate response is for and which thirty days an event's timestamps fall in, and spool decides which registration a stage response answers against. A caller naming no spool generates a stage response that mints its own tracker pair, which is what a form kind whose context is data rather than metadata otherwise does.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
def generate_response(
    questionnaire: Questionnaire,
    index: CaptureIndex,
    naming: CaptureNaming,
    store: ResourceStore,
    *,
    seed: int,
    today: datetime.date,
    spool: ResponseSpool | None = None,
) -> QuestionnaireResponse:
    """Generate one synthetic response to a served form: its context, then an answer to every question.

    The whole document is a function of `(questionnaire, store, spool, seed, today)`. Two terms move
    on their own: `today` decides which completed reporting period an aggregate response is for and
    which thirty days an event's timestamps fall in, and `spool` decides which registration a stage
    response answers against. A caller naming no spool generates a stage response that mints its own
    tracker pair, which is what a form kind whose context is data rather than metadata otherwise does.
    """
    period = _reporting_period(index, today) if index.form_kind == "aggregate" else None
    window = DateWindow.of_period(period) if period is not None else DateWindow.recent(today)
    generator = _Generator(
        index=index,
        naming=naming,
        resolvers=CodingResolverSet(store=store),
        seed=seed,
        window=window,
        location_id=_capture_location_id(index, store, seed),
        adopted_pair=adopted_tracker_pair(index, naming, store, spool),
    )
    return generator.build(questionnaire, period)

adopted_tracker_pair(index, naming, store, spool)

The pair a generated stage response answers against: what a spooled registration of its program minted.

This is the join the capture UI's enrollment picker makes, made server-side. A stage form names its program on the {base}/id/program identifier, the registration form of that program carries the same identifier, and every receipt answering that form holds one minted pair - so the whole lookup is local to this project and touches no instance.

The order is the picker's order. A forwarded registration is preferred over a received one because DHIS2 already holds its pair, and within a state the newest registration wins, which is the person most plausibly being followed up. A rejected registration is never adopted. A receipt holding half a pair, or one these models cannot read, is passed over rather than adopted from, because a stage event built on half a pair is refused exactly as surely as one built on a fabricated pair. None means the spool holds no registration of this program, and the caller mints.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
def adopted_tracker_pair(
    index: CaptureIndex, naming: CaptureNaming, store: ResourceStore, spool: ResponseSpool | None
) -> TrackerPair | None:
    """The pair a generated stage response answers against: what a spooled registration of its program minted.

    This is the join the capture UI's enrollment picker makes, made server-side. A stage form names
    its program on the `{base}/id/program` identifier, the registration form of that program carries
    the same identifier, and every receipt answering that form holds one minted pair - so the whole
    lookup is local to this project and touches no instance.

    The order is the picker's order. A forwarded registration is preferred over a received one
    because DHIS2 already holds its pair, and within a state the newest registration wins, which is
    the person most plausibly being followed up. A rejected registration is never adopted. A receipt
    holding half a pair, or one these models cannot read, is passed over rather than adopted from,
    because a stage event built on half a pair is refused exactly as surely as one built on a
    fabricated pair. None means the spool holds no registration of this program, and the caller mints.
    """
    if spool is None or index.form_kind != _STAGE_FORM_KIND or index.program_uid is None:
        return None
    forms = _program_form_entries(index.program_uid, naming, store)
    if not forms:
        return None
    reading = spool.search(form_kind=_REGISTRATION_FORM_KIND, lifecycles=_ADOPTABLE_LIFECYCLES)
    preferred = sorted(reading.receipts, key=lambda receipt: _ADOPTABLE_LIFECYCLES.index(receipt.lifecycle))
    for receipt in preferred:
        if not any(_answers_form(receipt, entry) for entry in forms):
            continue
        pair = _receipt_tracker_pair(receipt, naming)
        if pair is not None:
            return pair
    return None

resolve_period_type(index)

Decide which DHIS2 period type a generated aggregate response reports for.

One rule, because there is only one place the fact is written: the form's own D2PeriodType declaration. Every served Questionnaire carries it - a compiled guide's SUSHI output and a --live store's JSON builder both write the data set's type onto the form - so $generate reports for the same period in either mode. Monthly is what a form declaring none reports under, which is the type most DHIS2 data sets collect on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/synthesize.py
def resolve_period_type(index: CaptureIndex) -> str:
    """Decide which DHIS2 period type a generated aggregate response reports for.

    One rule, because there is only one place the fact is written: the form's own `D2PeriodType`
    declaration. Every served Questionnaire carries it - a compiled guide's SUSHI output and a
    `--live` store's JSON builder both write the data set's type onto the form - so `$generate`
    reports for the same period in either mode. `Monthly` is what a form declaring none reports
    under, which is the type most DHIS2 data sets collect on.
    """
    return index.period_type or DEFAULT_PERIOD_TYPE

Terminology translation

GET /ConceptMap/$translate - the DHIS2 identifiers the published ConceptMaps state for one concept. The matching itself is a pure function over the maps a store holds, so a caller with a loaded store answers the same question with no server running.

translate

GET /ConceptMap/$translate - the type-level operation over the ConceptMaps the project publishes.

One ConceptMap per option set takes the generated concept codes back to the DHIS2 option UID and, where the option carries one, the DHIS2 option code. This operation is what makes that readable over the wire: hand it a concept and it answers with every DHIS2 identifier the maps state for it.

The maps are served as documents too - ConceptMap is in SERVED_READ_RESOURCE_TYPES, so GET /ConceptMap/<id> answers the published map verbatim. The two are complementary: a read hands over the whole mapping table for a person or a UI to look at, and this operation answers the one question a forwarder has, without its caller walking groups and elements.

The path is fixed, so this router mounts ahead of the read catch-alls: /{resource_type}/{resource_id} matches /ConceptMap/$translate just as happily and would answer it as a read of a resource named $translate.

Classes

TranslateRequest

Bases: BaseModel

The concept one $translate call asks about, and the target system it will accept an answer in.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/translate.py
class TranslateRequest(BaseModel):
    """The concept one `$translate` call asks about, and the target system it will accept an answer in."""

    model_config = ConfigDict(frozen=True)

    system: str
    code: str
    target_system: str | None = None

TranslationMatch

Bases: BaseModel

One mapping the served maps state for the asked-about concept, as a match parameter reports it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/translate.py
class TranslationMatch(BaseModel):
    """One mapping the served maps state for the asked-about concept, as a `match` parameter reports it."""

    model_config = ConfigDict(frozen=True)

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

Functions:

parse_translate_request(params)

Read the query into the concept to translate, refusing a call that names no system or no code.

R4 spells the target parameter targetsystem, all lower case, and real clients send targetSystem about as often - both are read here, the lower-case spelling first, so a client that sends either is answered rather than silently given every group of a matching source.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/translate.py
def parse_translate_request(params: QueryParams) -> TranslateRequest:
    """Read the query into the concept to translate, refusing a call that names no `system` or no `code`.

    R4 spells the target parameter `targetsystem`, all lower case, and real clients send
    `targetSystem` about as often - both are read here, the lower-case spelling first, so a client
    that sends either is answered rather than silently given every group of a matching source.
    """
    system = _required(params, "system")
    code = _required(params, "code")
    target_system = params.get("targetsystem") or params.get("targetSystem")
    return TranslateRequest(system=system, code=code, target_system=target_system or None)

translate_concept(request) async

Answer with the DHIS2 identifiers the served ConceptMaps state for one concept.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/translate.py
@router.get(TRANSLATE_PATH)
async def translate_concept(request: Request) -> Response:
    """Answer with the DHIS2 identifiers the served ConceptMaps state for one concept."""
    context = serve_context(request)
    translate = parse_translate_request(request.query_params)
    matches = find_translations(context.store.concept_maps(), translate)
    return _parameters_response(_translate_parameters(matches, translate))

find_translations(concept_maps, translate)

Every mapping the maps state for the asked-about concept, in the order the maps were loaded.

A group matches on its source, and on its target too when the call named one. A target carrying no code maps the concept onto nothing statable and is passed over.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/translate.py
def find_translations(
    concept_maps: tuple[ConceptMap, ...], translate: TranslateRequest
) -> tuple[TranslationMatch, ...]:
    """Every mapping the maps state for the asked-about concept, in the order the maps were loaded.

    A group matches on its `source`, and on its `target` too when the call named one. A target
    carrying no code maps the concept onto nothing statable and is passed over.
    """
    found: list[TranslationMatch] = []
    for concept_map in concept_maps:
        for group in concept_map.group or []:
            if group.source != translate.system:
                continue
            if translate.target_system is not None and group.target != translate.target_system:
                continue
            for element in group.element or []:
                if element.code != translate.code:
                    continue
                found.extend(
                    TranslationMatch(
                        system=group.target,
                        code=target.code,
                        display=target.display or element.display,
                        equivalence=target.equivalence,
                        source=concept_map.url,
                    )
                    for target in element.target or []
                    if target.code is not None
                )
    return tuple(found)

Evaluating an expression

POST /facade/evaluate - one FHIRPath expression, CQL library, or ELM library run over one resource this facade serves, answering typed results and real diagnostics: a parse failure at the line and column the parser stopped on, a per-define refusal in the define's own row. The engine layer is a pure function over FHIR-shaped JSON, so a caller with a loaded store evaluates the same expression with no server running. The sandbox is closed by construction - no library path is ever passed to the engine and ELM is parsed to a dict before the engine sees it, so an expression reaches the supplied context and nothing else.

evaluation

Running one FHIRPath expression, CQL library, or ELM library over one resource, and saying what happened.

This is the whole of what the facade knows about dhis2w_fhir_engine. The router above it decides which resource the expression runs over and answers HTTP; this module takes that resource as FHIR-shaped JSON, hands it to the engine, and turns whatever comes back - a collection, a value per define, a parse failure, a refusal - into one typed answer. Nothing here imports FastAPI, so a caller with a loaded store evaluates the same expression the endpoint evaluates, with no server running.

THE SANDBOX IS CLOSED, AND THAT IS A PROPERTY OF THE CALLS THIS MODULE MAKES. The engine can read libraries off disk - CQLEvaluator(library_paths=...) searches directories, and ELMEvaluator.load treats a string that names an existing file as a file to open. Neither door is opened here: no library path is ever passed, and ELM is parsed to a dict by this module before the engine is given it, so a source naming /etc/passwd is JSON that will not parse rather than a file that will. The only data an expression can reach is the resource the caller supplied. FHIRHelpers 4.0.1 resolves without any of that, because the engine carries it in memory.

A BAD EXPRESSION IS AN ANSWER, NOT A FAILURE. Every way a user-supplied expression can go wrong ends in a diagnostic rather than an exception leaving this module: an expression that will not parse, a library whose define refuses at evaluation time, a define asked for that the library does not declare. That is why the running functions catch Exception rather than the engine's own error class - the source is a person's typing, and a construct the evaluator half-supports must read as "this expression did not work" rather than as a server that failed.

WHERE A REFUSAL LANDS, STATED ONCE. A refusal that belongs to one named define rides that define's own row, in EvaluationResult.refusal, so the library's other defines still answer. A refusal that stopped the whole run - nothing parsed, no library compiled, the named define does not exist - is a diagnostic, because there is no row for it to ride.

COLUMNS ARE COUNTED FROM ONE. ANTLR reports the column of a syntax error counting from zero, which is right for a parser and wrong for a person counting characters along a line. EvaluationDiagnostic states the line as ANTLR gives it and the column one higher, so "line 2, column 14" names the fourteenth character.

A PARSE MESSAGE NAMES WHAT WENT WRONG, NOT EVERY TOKEN THAT WOULD HAVE BEEN RIGHT. ANTLR ends a mismatched-input message with the whole set it expected instead - dozens of quoted operators and lexer rule names in capitals. A positioned diagnostic already points at the character, so that tail is dropped and the naming half is kept.

Classes

EvaluationLanguage

Bases: StrEnum

Which of the three languages a source is written in.

They differ in what source holds and in what an answer looks like. fhirpath is one expression answering one collection; cql is a library whose every define answers a value; elm is that same library already compiled, as the JSON another compiler emitted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
class EvaluationLanguage(StrEnum):
    """Which of the three languages a source is written in.

    They differ in what `source` holds and in what an answer looks like. `fhirpath` is one
    expression answering one collection; `cql` is a library whose every define answers a value;
    `elm` is that same library already compiled, as the JSON another compiler emitted.
    """

    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-serve/src/dhis2w_fhir_serve/evaluation.py
class DiagnosticKind(StrEnum):
    """Whether the source never parsed, or parsed and then refused to run."""

    PARSE = "parse"
    EVALUATION = "evaluation"

EvaluationDiagnostic

Bases: BaseModel

One thing that stopped the run, at the position it happened where the parser stated one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
class EvaluationDiagnostic(BaseModel):
    """One thing that stopped the run, at the position it happened where the parser stated one."""

    model_config = ConfigDict(frozen=True)

    kind: DiagnosticKind
    message: str
    """What the engine said, verbatim - including the token set a parser named as expected."""

    line: int | None = None
    """The line the parser stopped on, counted from one, or None for a refusal with no position."""

    column: int | None = None
    """The column on that line, counted from one - one higher than the zero-based column ANTLR gives."""

    expression_name: str | None = None
    """The define this diagnostic is about, for a name the library does not declare."""
Attributes
message instance-attribute

What the engine said, verbatim - including the token set a parser named as expected.

line = None class-attribute instance-attribute

The line the parser stopped on, counted from one, or None for a refusal with no position.

column = None class-attribute instance-attribute

The column on that line, counted from one - one higher than the zero-based column ANTLR gives.

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-serve/src/dhis2w_fhir_serve/evaluation.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.

Results and diagnostics are not exclusive. A library whose second define refuses still answers for its first, and a run that answered nothing at all carries the diagnostic saying why.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
class EvaluationOutcome(BaseModel):
    """One evaluation, whole: what it answered, what it declares, and what stopped it.

    Results and diagnostics are not exclusive. A library whose second define refuses still answers
    for its first, and a run that answered nothing at all carries the diagnostic saying why.
    """

    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.

Functions:

evaluate_source(language, source, subject=None, expression_name=None)

Run one source in one language over one resource, answering results and diagnostics.

subject is FHIR-shaped JSON because that is the engine's own argument type - it evaluates over documents rather than over models, and a model here would be one this facade parsed and the engine immediately re-read. A Bundle is used as the data a CQL retrieve reads through; any other resource is both the context resource and the one-entry collection a retrieve sees, so [Patient] answers over a Patient that was handed in on its own.

expression_name narrows a CQL or ELM run to one define. FHIRPath has no defines and ignores it.

This is blocking, CPU-bound work - parsing a grammar and walking a tree - so an async caller runs it off the event loop.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
def evaluate_source(
    language: EvaluationLanguage,
    source: str,
    subject: dict[str, Any] | None = None,
    expression_name: str | None = None,
) -> EvaluationOutcome:
    """Run one source in one language over one resource, answering results and diagnostics.

    `subject` is FHIR-shaped JSON because that is the engine's own argument type - it evaluates over
    documents rather than over models, and a model here would be one this facade parsed and the
    engine immediately re-read. A Bundle is used as the data a CQL retrieve reads through; any other
    resource is both the context resource and the one-entry collection a retrieve sees, so
    `[Patient]` answers over a Patient that was handed in on its own.

    `expression_name` narrows a CQL or ELM run to one define. FHIRPath has no defines and ignores it.

    This is blocking, CPU-bound work - parsing a grammar and walking a tree - so an async caller runs
    it off the event loop.
    """
    if language is EvaluationLanguage.FHIRPATH:
        return _run_fhirpath(source, subject)
    if language is EvaluationLanguage.CQL:
        return _run_cql(source, subject, expression_name)
    return _run_elm(source, subject, expression_name)

json_safe(value)

One evaluation result as JSON a browser can render, whatever Python object the engine answered.

The engine answers in its own vocabulary - CQLCode and CQLInterval are Pydantic models, a date literal is a datetime.date, a decimal is a Decimal - and none of those cross HTTP as themselves. Anything with no JSON spelling of its own is rendered as the text str() gives it, which is the engine's own word for the value rather than a shape invented here.

A float that is not finite is rendered as text for the same reason: NaN and Infinity are things JSON cannot say, and a body carrying them is a body a strict parser refuses.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
def json_safe(value: Any) -> JsonValue:
    """One evaluation result as JSON a browser can render, whatever Python object the engine answered.

    The engine answers in its own vocabulary - `CQLCode` and `CQLInterval` are Pydantic models,
    a date literal is a `datetime.date`, a decimal is a `Decimal` - and none of those cross HTTP as
    themselves. Anything with no JSON spelling of its own is rendered as the text `str()` gives it,
    which is the engine's own word for the value rather than a shape invented here.

    A float that is not finite is rendered as text for the same reason: `NaN` and `Infinity` are
    things JSON cannot say, and a body carrying them is a body a strict parser refuses.
    """
    if value is None or isinstance(value, bool | str):
        return value
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return value if math.isfinite(value) else str(value)
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, BaseModel):
        dumped: JsonValue = value.model_dump(mode="json", exclude_none=True)
        return dumped
    if isinstance(value, Enum):
        return json_safe(value.value)
    if isinstance(value, datetime | date | time):
        return value.isoformat()
    if isinstance(value, dict):
        return {str(key): json_safe(item) for key, item in value.items()}
    if isinstance(value, list | tuple | set | frozenset):
        return [json_safe(item) for item in value]
    return str(value)

syntax_diagnostic(error)

Read one engine exception as the diagnostic it is: a parse failure with a position, or a refusal.

The position is looked for through the whole __cause__ chain, because the FHIRPath evaluator wraps its listener's syntax error in a second exception naming the expression - so the line and column are one link down rather than on the exception the caller caught.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/evaluation.py
def syntax_diagnostic(error: BaseException) -> EvaluationDiagnostic:
    """Read one engine exception as the diagnostic it is: a parse failure with a position, or a refusal.

    The position is looked for through the whole `__cause__` chain, because the FHIRPath evaluator
    wraps its listener's syntax error in a second exception naming the expression - so the line and
    column are one link down rather than on the exception the caller caught.
    """
    walked: BaseException | None = error
    while walked is not None:
        found = _SYNTAX_ERROR_PATTERN.search(str(walked))
        if found is not None:
            return EvaluationDiagnostic(
                kind=DiagnosticKind.PARSE,
                message=_without_expected_token_set(found.group(3)),
                line=int(found.group(1)),
                column=int(found.group(2)) + 1,
            )
        walked = walked.__cause__
    return EvaluationDiagnostic(kind=DiagnosticKind.EVALUATION, message=str(error))

evaluate

POST /facade/evaluate - run one FHIRPath expression, CQL library, or ELM library against this facade's data.

WHY THIS SHAPE IS NOT FHIR'S. This endpoint answers a diagnostic, not a document: the line and the column a parser stopped on, one row per define whether it answered or refused, and the difference between a define that matched nothing and a define that was never run. FHIR has no empty collection and Parameters has no line number, so this answer is plain application/json and Pydantic models - /spool's shape for /spool's reasons, served under the facade API's own mount rather than at the FHIR base. dhis2w_fhir_serve.routes.spool argues that choice in full, and the capture UI's Evaluate screen is the reader this shape is for.

POST /$evaluate at the FHIR base is the wire-true sibling and answers the same evaluation as a Parameters resource, for a client that speaks operations rather than this project's JSON. It is the root's only evaluation spelling, because the root is FHIR's. dhis2w_fhir_serve.routes.evaluate_operation is that operation, and it resolves its context through evaluation_subject below - so the two addresses reach exactly the same three resources.

WHAT AN EXPRESSION MAY REACH, STATED ONCE. Exactly the resource the request named as its context, and nothing else. Three kinds of context are offered and each is a different way of naming one resource: stored reads it out of the served guide by type and id, inline is the JSON the caller posted, and registered reads one tracked entity out of the DHIS2 instance a live facade holds open and projects it the way GET /Patient/{uid} does. There is no fourth kind that searches, no file path, and no library directory - dhis2w_fhir_serve.evaluation states what keeps that true of the engine calls themselves.

THE REGISTERED CONTEXT IS LIVE-ONLY, and refuses the way every other register route refuses: the config first ([serve.tracked_entities] enabled), then the missing instance, then a guide that publishes no registration form, then a resource type the register does not serve. That order is dhis2w_fhir_serve.routes.register's and the reasons are its. It is read the way the register is read, too: under [serve] auth = "dhis2" the entity comes back under the CALLER'S own DHIS2 authorization, so an expression can only ever run over a person its caller may see.

A BAD EXPRESSION IS 200. An expression that will not parse, a library whose define refuses, a define name the library does not declare - each answers 200 with the diagnostic saying so, because the request was perfectly well formed and the server did exactly what it was asked. This is $translate's posture: a concept the maps say nothing about is an answer with a message, not an error. What does answer an OperationOutcome is a request this facade cannot serve at all - a stored resource it does not hold, a register it does not publish.

Attributes

EvaluationContext = Annotated[StoredResourceContext | InlineResourceContext | RegisteredEntityContext, Field(discriminator='kind')] module-attribute

The three ways a request names the one resource an expression runs over.

Classes

StoredResourceContext

Bases: BaseModel

One resource of the served guide, named the way a read names it: by type and id.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
class StoredResourceContext(BaseModel):
    """One resource of the served guide, named the way a read names it: by type and id."""

    model_config = ConfigDict(frozen=True)

    kind: Literal["stored"] = "stored"
    resource_type: str
    resource_id: str

InlineResourceContext

Bases: BaseModel

One resource the caller posted, evaluated as it arrived.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
class InlineResourceContext(BaseModel):
    """One resource the caller posted, evaluated as it arrived."""

    model_config = ConfigDict(frozen=True)

    kind: Literal["inline"] = "inline"
    resource: dict[str, Any]
    """The resource verbatim - the same HTTP-boundary escape hatch `StoreEntry.body` documents.

    An expression may be written against any FHIR resource type, including ones this repo has no
    model for, and the engine evaluates over FHIR-shaped JSON rather than over models. Parsing it
    into a model here would mean parsing it back out again one function later.
    """
Attributes
resource instance-attribute

The resource verbatim - the same HTTP-boundary escape hatch StoreEntry.body documents.

An expression may be written against any FHIR resource type, including ones this repo has no model for, and the engine evaluates over FHIR-shaped JSON rather than over models. Parsing it into a model here would mean parsing it back out again one function later.

RegisteredEntityContext

Bases: BaseModel

One tracked entity the DHIS2 instance holds, as the register serves it.

The resource type is the one the published map takes the entity's type onto - Patient for a project that tracks people, whatever the map states for the rest - so it is named rather than assumed, exactly as the register's own read path names it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
class RegisteredEntityContext(BaseModel):
    """One tracked entity the DHIS2 instance holds, as the register serves it.

    The resource type is the one the published map takes the entity's type onto - `Patient` for a
    project that tracks people, whatever the map states for the rest - so it is named rather than
    assumed, exactly as the register's own read path names it.
    """

    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: a language, a source, a context, and optionally one define.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
class EvaluationRequest(BaseModel):
    """One evaluation as a caller asks for it: a language, a source, a context, and optionally one define."""

    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.

Functions:

evaluate_expression(request, asked) async

Evaluate one source over one resource, answering its results and everything that went wrong.

The evaluation runs off the event loop. Parsing a grammar and walking an expression tree is blocking, CPU-bound work, and a facade doing it inline would stall every other request it is serving - including the capture that is trying to post a receipt.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
@router.post(
    EVALUATE_PATH,
    tags=[EVALUATE_TAG],
    summary="Evaluate an expression or a library",
    description=(
        "Runs one FHIRPath expression, CQL library, or compiled ELM library over one resource and "
        "answers what it produced, one row per define, together with everything that went wrong and "
        "where.\n\n"
        "An expression reaches exactly the resource the request named as its context and nothing "
        "else. There are three ways to name one and no fourth: `stored` reads it out of the served "
        "guide by type and id, `inline` is the JSON the request carries, and `registered` reads one "
        "tracked entity out of the DHIS2 instance a live run holds open. Naming no context runs the "
        "expression over no resource at all.\n\n"
        "A source that will not parse is a 200 carrying the diagnostic, not a refusal: the request "
        "was well formed and this server did exactly what it was asked. What answers an "
        "OperationOutcome is a request this facade cannot serve at all - a stored resource it does "
        "not hold, a register it does not publish.\n\n"
        "`POST /$evaluate` at the FHIR base answers the same evaluation as a `Parameters` resource."
    ),
    response_description="What each expression or define answered, and every diagnostic the run produced.",
)
async def evaluate_expression(request: Request, asked: EvaluationRequest) -> EvaluationOutcome:
    """Evaluate one source over one resource, answering its results and everything that went wrong.

    The evaluation runs off the event loop. Parsing a grammar and walking an expression tree is
    blocking, CPU-bound work, and a facade doing it inline would stall every other request it is
    serving - including the capture that is trying to post a receipt.
    """
    subject = await evaluation_subject(request, asked.context)
    return await run_in_threadpool(evaluate_source, asked.language, asked.source, subject, asked.expression_name)

evaluation_subject(request, context) async

The one resource an expression may reach, resolved from whichever way the request named it.

Public because POST /$evaluate resolves its context through this very function: the two endpoints answer in two shapes and reach exactly the same three resources, and a second resolution path would be a second answer to "what may an expression read".

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate.py
async def evaluation_subject(request: Request, context: EvaluationContext | None) -> dict[str, Any] | None:
    """The one resource an expression may reach, resolved from whichever way the request named it.

    Public because `POST /$evaluate` resolves its context through this very function: the two
    endpoints answer in two shapes and reach exactly the same three resources, and a second
    resolution path would be a second answer to "what may an expression read".
    """
    if context is None:
        return None
    if isinstance(context, InlineResourceContext):
        return context.resource
    if isinstance(context, StoredResourceContext):
        entry = serve_context(request).store.by_type_and_id(context.resource_type, context.resource_id)
        if entry is None:
            raise NotFoundError(context.resource_type, context.resource_id)
        return entry.body
    return await _registered(request, context)

The evaluation as a Parameters resource

POST /$evaluate - the same evaluation, answered as the Parameters resource a FHIR client expects from an operation: one parameter per define named by the define, value[x] for a single primitive, resource where a define answered a resource, one part per value where it answered several, an OperationOutcome part where it refused, and an outcome parameter carrying the line and column a parser stopped on. Both directions are pure functions - evaluation_ask reads a Parameters input into the request it asks for, evaluation_parameters says one EvaluationOutcome in FHIR's own terms - so a caller assembling or reading an operation body needs no server running.

evaluate_operation

POST /$evaluate - the same evaluation as /facade/evaluate, answered as the Parameters resource FHIR asks for.

THE ADDRESS IS SYSTEM-LEVEL, because what is evaluated is not one resource type's business. FHIR spells an operation at the service base [base]/$op, and this one runs over whatever the request names as its context - a published Questionnaire, a posted Bundle, one tracked entity of the register - so no resource type owns it. It rides one segment beginning with $, which no PascalCase resource type can shadow, and mounts ahead of the read catch-alls for the reason dhis2w_fhir_serve.routes.translate states: /{resource_type} matches /$evaluate just as happily.

THE ANSWER FOLLOWS THE CQL-ON-FHIR CONVENTION. Clinical Reasoning's Library/$evaluate and the CPG-on-FHIR $cql operation both answer one Parameters whose parameters are the defines the library declared, and both take the source and the data to run it over as parameters in. This is that shape over this facade's three languages: one parameter per define, named by the define; value[x] where the define answered one primitive; resource where it answered a FHIR resource; one part per value where it answered several; and an OperationOutcome part where it refused. What stopped the whole run - an expression that would not parse, a define the library does not declare - is the outcome parameter, an OperationOutcome whose issue carries the line and column the parser stopped on.

A DEFINE THAT MATCHED NOTHING IS ABSENT, and that is FHIR's own answer rather than a fact thrown away. FHIR has no empty collection: a value is present or the element is not there, which dhis2w_fhir_engine.r4.resources states as the rule every model here is built on. POST /facade/evaluate keeps the distinction between "matched nothing" and "was not run", because its own shape can carry it; this one cannot, and inventing a spelling for it would be this server writing FHIR nobody else reads.

PARAMETERS IN IS CANONICAL, AND THE PLAIN JSON BODY IS ALSO READ. An operation's input is a Parameters resource and that is what this address documents, but the same body /facade/evaluate takes is accepted here too - the two endpoints run the same evaluation over the same three contexts, and making a caller rewrite a body to change which shape comes back would be a difference about nothing. Which one arrived is decided by resourceType, and nothing else about the request changes.

A BAD EXPRESSION IS 200 HERE TOO. dhis2w_fhir_serve.routes.evaluate argues that posture in full: a request this facade cannot serve at all is an OperationOutcome with a 4xx status, and everything a person's typing can do wrong is an answer.

Attributes

Classes

Functions:

evaluate_operation(request) async

Evaluate one source over one resource, answering the defines as a Parameters resource.

The evaluation runs off the event loop for the reason POST /facade/evaluate does: parsing a grammar and walking an expression tree is blocking, CPU-bound work, and a facade doing it inline would stall every other request it is serving.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate_operation.py
@router.post(EVALUATE_OPERATION_PATH)
async def evaluate_operation(request: Request) -> Response:
    """Evaluate one source over one resource, answering the defines as a `Parameters` resource.

    The evaluation runs off the event loop for the reason `POST /facade/evaluate` does: parsing a grammar
    and walking an expression tree is blocking, CPU-bound work, and a facade doing it inline would
    stall every other request it is serving.
    """
    asked = await _asked(request)
    subject = await evaluation_subject(request, asked.context)
    outcome = await run_in_threadpool(evaluate_source, asked.language, asked.source, subject, asked.expression_name)
    return Response(
        content=evaluation_parameters(outcome).model_dump_json(exclude_none=True, by_alias=True),
        media_type=FHIR_JSON_MEDIA_TYPE,
    )

evaluation_ask(parameters)

Read one Parameters input into the evaluation it asks for, refusing a body that names none.

language and source are required, because an evaluation with neither is not a narrower question - it is no question. expression names one define, and context names the one resource the expression may reach; both are optional and both mean here exactly what they mean in the /facade/evaluate body.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate_operation.py
def evaluation_ask(parameters: Parameters) -> EvaluationRequest:
    """Read one `Parameters` input into the evaluation it asks for, refusing a body that names none.

    `language` and `source` are required, because an evaluation with neither is not a narrower
    question - it is no question. `expression` names one define, and `context` names the one resource
    the expression may reach; both are optional and both mean here exactly what they mean in the
    `/facade/evaluate` body.
    """
    stated = parameters.parameter or []
    language = _language(_text(stated, LANGUAGE_PARAMETER))
    source = _text(stated, SOURCE_PARAMETER)
    if source is None:
        raise BadOperationError(f"`$evaluate` needs a `{SOURCE_PARAMETER}` parameter carrying the source to evaluate")
    return EvaluationRequest(
        language=language,
        source=source,
        expression_name=_text(stated, EXPRESSION_PARAMETER),
        context=_context(_named(stated, CONTEXT_PARAMETER)),
    )

evaluation_parameters(outcome)

One evaluation as the Parameters a FHIR client reads: a parameter per define, and the outcome.

A define that matched nothing carries no parameter, because FHIR has no empty collection. A define that refused carries its own OperationOutcome, so the rest of the library still answers.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/evaluate_operation.py
def evaluation_parameters(outcome: EvaluationOutcome) -> Parameters:
    """One evaluation as the `Parameters` a FHIR client reads: a parameter per define, and the outcome.

    A define that matched nothing carries no parameter, because FHIR has no empty collection. A
    define that refused carries its own OperationOutcome, so the rest of the library still answers.
    """
    answered = [stated for result in outcome.results if (stated := _result_parameter(result)) is not None]
    if outcome.diagnostics:
        answered.append(
            ParametersParameter(
                name=OUTCOME_PARAMETER,
                resource=json_resource(
                    OperationOutcome(issue=[_diagnostic_issue(diagnostic) for diagnostic in outcome.diagnostics])
                ),
            )
        )
    return Parameters(parameter=answered or None)

This guide's vocabularies

GET /facade/terminology/validate-code and GET /facade/terminology/lookup - is this code in that published value set, and what is this code called. It answers about the CodeSystems and ValueSets this project publishes and is not a general terminology server: a SNOMED CT or LOINC code is answered "this server publishes no code system under that url". Membership goes to the engine's in-memory terminology service, so the composition rules are the engine's; the code systems are indexed here, because that service takes none through its public surface.

terminology

The served guide's own vocabularies, answered over: is this code in that value set, and what is it called.

WHAT THIS IS, AND WHAT IT IS NOT. This is not a terminology server. It answers about the CodeSystems and ValueSets this project publishes - the option sets its forms bind, the data dictionaries its questions are coded through, the category combinations its aggregate forms report on - and it knows nothing about SNOMED CT, LOINC, ICD, or any other vocabulary an implementation guide merely points at. A code from one of those is answered "this server publishes no code system under that url", which is the truth and is a more useful answer than a guess. Nothing here expands a value set that composes another server's system, and there is no $expand at all.

WHY IT EXISTS ANYWAY. Capture already validates a coded answer against the served terminology, and d2w fhir forward already resolves a concept back to DHIS2 identifiers through the ConceptMaps - but both of those are things that happen to a submission. There was no way to ask the running server the question directly, which is what turns "the guide says this option set holds three codes" from a claim about a document into an answer from the process.

WHERE THE ANSWERS COME FROM. Two halves, because the engine's in-memory terminology service holds one of them. Every published ValueSet is fed to InMemoryTerminologyService, which is what answers a code's membership from an expansion or from an enumerated include - so those composition rules are the engine's, and this facade does not reimplement them. Every published CodeSystem is indexed here instead, because that service takes no code systems through its public surface: a lookup reads the concept out of the CodeSystem the guide published, and a validation naming a system rather than a value set is answered from the same index.

ONE COMPOSITION RULE IS RESOLVED HERE, and only one. d2w fhir generate writes a value set per option set as an include naming the option set's CodeSystem and enumerating nothing, which in FHIR means every code of that system - and which a service holding no code systems can only read as a set enumerating nothing at all. LookupValueSet states that rule, its edges, and what happens to a composition it does not cover, which is that the engine's answer stands and the message says so.

THE STATE IS BUILT ON FIRST USE, like the capture state and for its reason: a facade nobody ever asks a terminology question of should not pay to parse every ValueSet it serves at startup. It is held on the application rather than in ServeContext, which is everything the lifespan loaded.

Classes

ConceptProperty

Bases: BaseModel

One property a CodeSystem states about one concept, as text.

Rendered as text rather than as R4's value[x] union, because a property table shows the value and the reader does not care which of six elements carried it. The DHIS2 code of a data element, the value type of a tracked entity attribute, and whether DHIS2 enforces uniqueness are all properties, and all three are read the same way.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class ConceptProperty(BaseModel):
    """One property a CodeSystem states about one concept, as text.

    Rendered as text rather than as R4's `value[x]` union, because a property table shows the value
    and the reader does not care which of six elements carried it. The DHIS2 code of a data element,
    the value type of a tracked entity attribute, and whether DHIS2 enforces uniqueness are all
    properties, and all three are read the same way.
    """

    model_config = ConfigDict(frozen=True)

    code: str
    value: str

LookupConceptProperty

Bases: BaseModel

One concept.property element as the lookup reads it: the code, and whichever value[x] carried it.

A deliberate projection rather than dhis2w_fhir.r4.CodeSystemConceptProperty, for the reason LookupConcept gives - the shared model forbids elements it does not declare, and one such element in one published system would cost the whole system's lookups.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupConceptProperty(BaseModel):
    """One `concept.property` element as the lookup reads it: the code, and whichever `value[x]` carried it.

    A deliberate projection rather than `dhis2w_fhir.r4.CodeSystemConceptProperty`, for the reason
    `LookupConcept` gives - the shared model forbids elements it does not declare, and one such
    element in one published system would cost the whole system's lookups.
    """

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

    code: str | None = None
    valueCode: str | None = None
    valueString: str | None = None
    valueBoolean: bool | None = None
    valueInteger: int | None = None
    valueDecimal: float | None = None
    valueCoding: dict[str, Any] | None = None
    """One coding-valued property, verbatim - the HTTP-boundary escape hatch `StoreEntry.body` documents."""

    def stated(self) -> str | None:
        """The value this property carries, as the one string a table cell shows, or None for an empty one."""
        if self.valueCoding is not None:
            coded = self.valueCoding.get("code") or self.valueCoding.get("display")
            return str(coded) if coded is not None else None
        for carried in (self.valueCode, self.valueString, self.valueInteger, self.valueDecimal):
            if carried is not None:
                return str(carried)
        return None if self.valueBoolean is None else str(self.valueBoolean).lower()
Attributes
valueCoding = None class-attribute instance-attribute

One coding-valued property, verbatim - the HTTP-boundary escape hatch StoreEntry.body documents.

Methods:
stated()

The value this property carries, as the one string a table cell shows, or None for an empty one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def stated(self) -> str | None:
    """The value this property carries, as the one string a table cell shows, or None for an empty one."""
    if self.valueCoding is not None:
        coded = self.valueCoding.get("code") or self.valueCoding.get("display")
        return str(coded) if coded is not None else None
    for carried in (self.valueCode, self.valueString, self.valueInteger, self.valueDecimal):
        if carried is not None:
            return str(carried)
    return None if self.valueBoolean is None else str(self.valueBoolean).lower()

LookupConcept

Bases: BaseModel

One concept as the lookup reads it: the code, its display, its properties, and the concepts under it.

A deliberate projection rather than dhis2w_fhir.r4.CodeSystemConcept. That model forbids elements it does not declare and declares no concept child, so a guide that hand-writes a code hierarchy into ig/input/resources, or states a concept definition, would fail to validate against it - and a lookup that dropped a whole CodeSystem over one unread element would answer "no such code" about codes this server is serving.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupConcept(BaseModel):
    """One concept as the lookup reads it: the code, its display, its properties, and the concepts under it.

    A deliberate projection rather than `dhis2w_fhir.r4.CodeSystemConcept`. That model forbids
    elements it does not declare and declares no `concept` child, so a guide that hand-writes a code
    hierarchy into `ig/input/resources`, or states a concept `definition`, would fail to validate
    against it - and a lookup that dropped a whole CodeSystem over one unread element would answer
    "no such code" about codes this server is serving.
    """

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

    code: str
    display: str | None = None
    definition: str | None = None
    property: tuple[LookupConceptProperty, ...] = ()
    concept: tuple[LookupConcept, ...] = ()

LookupCodeSystem

Bases: BaseModel

One published CodeSystem as the lookup reads it: where it lives, what it is called, its concepts.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupCodeSystem(BaseModel):
    """One published CodeSystem as the lookup reads it: where it lives, what it is called, its concepts."""

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

    url: str | None = None
    version: str | None = None
    title: str | None = None
    name: str | None = None
    concept: tuple[LookupConcept, ...] = ()

LookupValueSetInclude

Bases: BaseModel

One compose.include or compose.exclude clause, read for the one composition rule this server resolves.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupValueSetInclude(BaseModel):
    """One `compose.include` or `compose.exclude` clause, read for the one composition rule this server resolves."""

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

    system: str | None = None
    concept: tuple[LookupConcept, ...] = ()
    filter: tuple[dict[str, Any], ...] = ()
    """Filter clauses verbatim, read only for whether there are any - see `LookupValueSet.whole_systems`."""

    valueSet: tuple[str, ...] = ()

    def names_a_whole_system(self) -> bool:
        """True when this clause means every code of one system, with nothing narrowing it."""
        return self.system is not None and not (self.concept or self.filter or self.valueSet)
Attributes
filter = () class-attribute instance-attribute

Filter clauses verbatim, read only for whether there are any - see LookupValueSet.whole_systems.

Methods:
names_a_whole_system()

True when this clause means every code of one system, with nothing narrowing it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def names_a_whole_system(self) -> bool:
    """True when this clause means every code of one system, with nothing narrowing it."""
    return self.system is not None and not (self.concept or self.filter or self.valueSet)

LookupValueSetCompose

Bases: BaseModel

How one value set is put together, as far as this server reads it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupValueSetCompose(BaseModel):
    """How one value set is put together, as far as this server reads it."""

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

    include: tuple[LookupValueSetInclude, ...] = ()
    exclude: tuple[LookupValueSetInclude, ...] = ()

LookupValueSet

Bases: BaseModel

One published ValueSet, held beside the engine's service so one composition rule can be answered here.

THE RULE, AND ITS EDGES. d2w fhir generate writes a value set per option set as include: [{system: <the option set's CodeSystem>}] with no concept list - which in FHIR means every code of that system, and which the engine's in-memory service reads as a set enumerating nothing. That is the shape almost every value set this facade serves has, so answering it is the difference between a useful check and one that says false about every code a form binds.

So exactly one rule is resolved here: an include that names a system and narrows it with nothing - no concept list, no filter, no nested value set - means every code that system publishes. A composition with any exclude, or with any include this rule does not cover, is left entirely to the engine's own answer, and ValidatedCode.message says the composition was not resolved here rather than pretending it was.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookupValueSet(BaseModel):
    """One published ValueSet, held beside the engine's service so one composition rule can be answered here.

    THE RULE, AND ITS EDGES. `d2w fhir generate` writes a value set per option set as
    `include: [{system: <the option set's CodeSystem>}]` with no concept list - which in FHIR means
    every code of that system, and which the engine's in-memory service reads as a set enumerating
    nothing. That is the shape almost every value set this facade serves has, so answering it is the
    difference between a useful check and one that says false about every code a form binds.

    So exactly one rule is resolved here: an include that names a system and narrows it with nothing
    - no concept list, no filter, no nested value set - means every code that system publishes.
    A composition with any `exclude`, or with any include this rule does not cover, is left entirely
    to the engine's own answer, and `ValidatedCode.message` says the composition was not resolved
    here rather than pretending it was.
    """

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

    url: str | None = None
    version: str | None = None
    title: str | None = None
    name: str | None = None
    compose: LookupValueSetCompose | None = None

    def whole_systems(self) -> tuple[str, ...]:
        """The systems this set includes in their entirety, or nothing when the composition is beyond the rule."""
        if self.compose is None or self.compose.exclude:
            return ()
        if not all(include.names_a_whole_system() for include in self.compose.include):
            return ()
        return tuple(include.system for include in self.compose.include if include.system is not None)
Methods:
whole_systems()

The systems this set includes in their entirety, or nothing when the composition is beyond the rule.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def whole_systems(self) -> tuple[str, ...]:
    """The systems this set includes in their entirety, or nothing when the composition is beyond the rule."""
    if self.compose is None or self.compose.exclude:
        return ()
    if not all(include.names_a_whole_system() for include in self.compose.include):
        return ()
    return tuple(include.system for include in self.compose.include if include.system is not None)

LookedUpCode

Bases: BaseModel

What one code means in the vocabulary this server publishes it under.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class LookedUpCode(BaseModel):
    """What one code means in the vocabulary this server publishes it under."""

    model_config = ConfigDict(frozen=True)

    found: bool
    system: str
    code: str
    display: str | None = None
    definition: str | None = None
    code_system_title: str | None = None
    """What the guide calls the system this code is in, for a reader who has only the url."""

    properties: tuple[ConceptProperty, ...] = ()
    message: str | None = None
    """Why nothing was found, on a miss - never set when `found` is true."""
Attributes
code_system_title = None class-attribute instance-attribute

What the guide calls the system this code is in, for a reader who has only the url.

message = None class-attribute instance-attribute

Why nothing was found, on a miss - never set when found is true.

ValidatedCode

Bases: BaseModel

Whether one code is in one value set, or is a code of one system this server publishes.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class ValidatedCode(BaseModel):
    """Whether one code is in one value set, or is a code of one system this server publishes."""

    model_config = ConfigDict(frozen=True)

    result: bool
    code: str
    system: str | None = None
    valueset: str | None = None
    """The value set the code was checked against, when the caller named one."""

    display: str | None = None
    message: str | None = None
    """Why the answer is false, or what made the check inconclusive - never set when `result` is true."""
Attributes
valueset = None class-attribute instance-attribute

The value set the code was checked against, when the caller named one.

message = None class-attribute instance-attribute

Why the answer is false, or what made the check inconclusive - never set when result is true.

TerminologyState

Bases: BaseModel

The served vocabularies, loaded once: the engine's value-set service, and the code systems beside it.

The service is the reason this model allows arbitrary types. It is the engine's own object rather than a value, exactly as ServeRuntime holds a DHIS2 client, and it is what answers a membership question so this facade never reimplements value-set composition.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
class TerminologyState(BaseModel):
    """The served vocabularies, loaded once: the engine's value-set service, and the code systems beside it.

    The service is the reason this model allows arbitrary types. It is the engine's own object rather
    than a value, exactly as `ServeRuntime` holds a DHIS2 client, and it is what answers a membership
    question so this facade never reimplements value-set composition.
    """

    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)

    service: InMemoryTerminologyService
    code_systems: tuple[LookupCodeSystem, ...] = ()
    value_sets: tuple[LookupValueSet, ...] = ()
    """Every published value set, held beside the service for the one composition rule read here."""

    unreadable: tuple[str, ...] = Field(default=())
    """The sources of the resources this surface could not read, so a miss can name what was skipped."""

    _by_system_and_code: dict[tuple[str, str], tuple[LookupConcept, LookupCodeSystem]] = PrivateAttr(
        default_factory=dict
    )
    _by_value_set_url: dict[str, LookupValueSet] = PrivateAttr(default_factory=dict)

    def model_post_init(self, context: Any, /) -> None:
        """Index every concept by `(url, code)` and every value set by url (private attributes stay settable)."""
        for system in self.code_systems:
            if system.url is None:
                continue
            for concept in _flattened(system.concept):
                self._by_system_and_code.setdefault((system.url, concept.code), (concept, system))
        for value_set in self.value_sets:
            if value_set.url is not None:
                self._by_value_set_url.setdefault(value_set.url, value_set)

    def code_system_urls(self) -> tuple[str, ...]:
        """Every code system url this surface can look a code up in, sorted."""
        return tuple(sorted({system.url for system in self.code_systems if system.url is not None}))

    def value_set_urls(self) -> tuple[str, ...]:
        """Every value set url this surface answers membership for, sorted."""
        return tuple(sorted(self._by_value_set_url))

    def look_up(self, system: str, code: str) -> LookedUpCode:
        """What one code is called in one published system, and what that system states about it."""
        held = self._by_system_and_code.get((system, code))
        if held is None:
            return LookedUpCode(found=False, system=system, code=code, message=self._miss_message(system, code))
        concept, code_system = held
        return LookedUpCode(
            found=True,
            system=system,
            code=code,
            display=concept.display,
            definition=concept.definition,
            code_system_title=code_system.title or code_system.name,
            properties=tuple(
                ConceptProperty(code=held_property.code, value=stated)
                for held_property in concept.property
                if held_property.code is not None
                for stated in [held_property.stated()]
                if stated is not None
            ),
        )

    def validate_code(self, code: str, system: str | None = None, valueset: str | None = None) -> ValidatedCode:
        """Answer whether one code is in one value set, or - naming no value set - is a code of one system.

        The value-set question goes to the engine's service, which owns the composition rules. The
        system question is answered from the published code systems, and is the honest fallback for a
        caller who has a `system|code` pair in hand and no value set to check it against.
        """
        if valueset is not None:
            return self._in_value_set(code, system, valueset)
        if system is None:
            return ValidatedCode(
                result=False, code=code, message="name a `system` or a `valueset` to check this code against"
            )
        found = self.look_up(system, code)
        return ValidatedCode(
            result=found.found,
            code=code,
            system=system,
            display=found.display,
            message=found.message,
        )

    def _in_value_set(self, code: str, system: str | None, valueset: str) -> ValidatedCode:
        """Answer one membership question: the engine's service first, then the whole-system rule.

        The service owns composition - an expansion, an enumerated include - and its answer stands
        wherever it is yes. Where it is no, the set is checked against the one rule stated on
        `LookupValueSet`: an include naming a system and narrowing it with nothing means every code
        that system publishes, which is the shape `d2w fhir generate` writes and the one the
        service's own reading cannot see.
        """
        held = self._by_value_set_url.get(valueset)
        if held is None:
            return ValidatedCode(
                result=False,
                code=code,
                system=system,
                valueset=valueset,
                message=f"this server publishes no ValueSet under `{valueset}`",
            )
        answered = self.service.validate_code(ValidateCodeRequest(url=valueset, code=code, system=system))
        if answered.result:
            # The engine answers a display only when the caller supplied one, so the published system
            # is what names the code - which is most of what makes a validation worth reading.
            named = None if system is None else self.look_up(system, code).display
            return ValidatedCode(
                result=True,
                code=code,
                system=system,
                valueset=valueset,
                display=answered.display or named,
            )
        return self._in_a_whole_system(code, system, valueset, held)

    def _in_a_whole_system(
        self, code: str, system: str | None, valueset: str, value_set: LookupValueSet
    ) -> ValidatedCode:
        """The one composition rule this server resolves itself, and the honest answer when it does not apply."""
        whole = value_set.whole_systems()
        if not whole:
            return ValidatedCode(
                result=False,
                code=code,
                system=system,
                valueset=valueset,
                message=(
                    f"`{valueset}` composes its codes in a way this server does not resolve; it answers "
                    "for the systems a set includes whole and for the concepts it enumerates"
                ),
            )
        for candidate in whole if system is None else [held for held in whole if held == system]:
            found = self.look_up(candidate, code)
            if found.found:
                return ValidatedCode(result=True, code=code, system=candidate, valueset=valueset, display=found.display)
        return ValidatedCode(
            result=False,
            code=code,
            system=system,
            valueset=valueset,
            message=f"`{code}` is not a code of any system `{valueset}` includes",
        )

    def _miss_message(self, system: str, code: str) -> str:
        """Why a lookup found nothing: an unpublished system, or a system that states no such code."""
        if system not in self.code_system_urls():
            return (
                f"this server publishes no CodeSystem under `{system}`; it serves this project's own "
                "vocabularies and is not a general terminology server"
            )
        return f"`{system}` states no concept with the code `{code}`"
Attributes
value_sets = () class-attribute instance-attribute

Every published value set, held beside the service for the one composition rule read here.

unreadable = Field(default=()) class-attribute instance-attribute

The sources of the resources this surface could not read, so a miss can name what was skipped.

Methods:
model_post_init(context)

Index every concept by (url, code) and every value set by url (private attributes stay settable).

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def model_post_init(self, context: Any, /) -> None:
    """Index every concept by `(url, code)` and every value set by url (private attributes stay settable)."""
    for system in self.code_systems:
        if system.url is None:
            continue
        for concept in _flattened(system.concept):
            self._by_system_and_code.setdefault((system.url, concept.code), (concept, system))
    for value_set in self.value_sets:
        if value_set.url is not None:
            self._by_value_set_url.setdefault(value_set.url, value_set)
code_system_urls()

Every code system url this surface can look a code up in, sorted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def code_system_urls(self) -> tuple[str, ...]:
    """Every code system url this surface can look a code up in, sorted."""
    return tuple(sorted({system.url for system in self.code_systems if system.url is not None}))
value_set_urls()

Every value set url this surface answers membership for, sorted.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def value_set_urls(self) -> tuple[str, ...]:
    """Every value set url this surface answers membership for, sorted."""
    return tuple(sorted(self._by_value_set_url))
look_up(system, code)

What one code is called in one published system, and what that system states about it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def look_up(self, system: str, code: str) -> LookedUpCode:
    """What one code is called in one published system, and what that system states about it."""
    held = self._by_system_and_code.get((system, code))
    if held is None:
        return LookedUpCode(found=False, system=system, code=code, message=self._miss_message(system, code))
    concept, code_system = held
    return LookedUpCode(
        found=True,
        system=system,
        code=code,
        display=concept.display,
        definition=concept.definition,
        code_system_title=code_system.title or code_system.name,
        properties=tuple(
            ConceptProperty(code=held_property.code, value=stated)
            for held_property in concept.property
            if held_property.code is not None
            for stated in [held_property.stated()]
            if stated is not None
        ),
    )
validate_code(code, system=None, valueset=None)

Answer whether one code is in one value set, or - naming no value set - is a code of one system.

The value-set question goes to the engine's service, which owns the composition rules. The system question is answered from the published code systems, and is the honest fallback for a caller who has a system|code pair in hand and no value set to check it against.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def validate_code(self, code: str, system: str | None = None, valueset: str | None = None) -> ValidatedCode:
    """Answer whether one code is in one value set, or - naming no value set - is a code of one system.

    The value-set question goes to the engine's service, which owns the composition rules. The
    system question is answered from the published code systems, and is the honest fallback for a
    caller who has a `system|code` pair in hand and no value set to check it against.
    """
    if valueset is not None:
        return self._in_value_set(code, system, valueset)
    if system is None:
        return ValidatedCode(
            result=False, code=code, message="name a `system` or a `valueset` to check this code against"
        )
    found = self.look_up(system, code)
    return ValidatedCode(
        result=found.found,
        code=code,
        system=system,
        display=found.display,
        message=found.message,
    )

Functions:

load_terminology(store)

Read every published CodeSystem and ValueSet out of one store into the state a lookup answers from.

A resource this surface cannot read is skipped and named in the log rather than failing the load, on the rule ResourceStore._parse_concept_maps follows: one unreadable document costs its own codes rather than every code the guide publishes.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/terminology.py
def load_terminology(store: ResourceStore) -> TerminologyState:
    """Read every published CodeSystem and ValueSet out of one store into the state a lookup answers from.

    A resource this surface cannot read is skipped and named in the log rather than failing the
    load, on the rule `ResourceStore._parse_concept_maps` follows: one unreadable document costs its
    own codes rather than every code the guide publishes.
    """
    service = InMemoryTerminologyService()
    code_systems: list[LookupCodeSystem] = []
    value_sets: list[LookupValueSet] = []
    unreadable: list[str] = []
    for entry in store.entries:
        if entry.resource_type == CODE_SYSTEM_RESOURCE_TYPE:
            try:
                code_systems.append(LookupCodeSystem.model_validate(entry.body))
            except ValidationError as error:
                logger.warning("%s: CodeSystem holds elements this server cannot read (%s)", entry.source, error)
                unreadable.append(entry.source)
            continue
        if entry.resource_type != VALUE_SET_RESOURCE_TYPE:
            continue
        try:
            service.add_value_set_from_json(entry.body)
            value_sets.append(LookupValueSet.model_validate(entry.body))
        except ValidationError as error:
            logger.warning("%s: ValueSet holds elements this server cannot read (%s)", entry.source, error)
            unreadable.append(entry.source)
    return TerminologyState(
        service=service,
        code_systems=tuple(code_systems),
        value_sets=tuple(value_sets),
        unreadable=tuple(unreadable),
    )

terminology

GET /facade/terminology/validate-code and /facade/terminology/lookup - asking this guide's vocabularies.

THIS SERVES ONE PROJECT'S VOCABULARIES AND IS NOT A TERMINOLOGY SERVER. It answers about the CodeSystems and ValueSets this facade publishes - the option sets its forms bind, the dictionaries its questions are coded through - and about nothing else. A SNOMED CT or LOINC code is answered "this server publishes no code system under that url", which is true and is more useful than a guess. dhis2w_fhir_serve.terminology states the whole of what is and is not known here.

WHY GET, AND WHY NOT $validate-code. R4 spells these as operations on the resource that holds the vocabulary - ValueSet/$validate-code and CodeSystem/$lookup - and both answer a Parameters resource. Neither is implemented here, because implementing them properly means implementing $expand behind them and answering for the external systems a real IG composes, which this facade cannot do and should not appear to. Two honestly-named plain reads say what they are: this server knows its own codes. They are GETs because they read, which also gives them HEAD parity from the mount sweep, and they are served under the facade API's own mount rather than at the FHIR base - /spool's shape for /spool's reasons, at /spool's address. A FHIR base that answered /terminology/lookup would be claiming a path FHIR has no interaction for.

THE STATE IS BUILT BY THE FIRST QUESTION, not by the lifespan, exactly as the capture state is: a facade nobody asks a terminology question of never pays to parse every ValueSet it serves.

Classes

Functions:

validate_code(request, code, system=None, valueset=None) async

Answer whether one code is in one published value set, or is a code of one published system.

Naming a value set asks the membership question and is what a form binding is checked against. Naming a system alone asks the weaker one this server can still answer honestly: is this a code the guide publishes at all. Naming neither is refused rather than answered, because a check with nothing to check against would answer false about every code there is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/terminology.py
@router.get(
    VALIDATE_CODE_PATH,
    tags=[TERMINOLOGY_TAG],
    summary="Check a code against this guide",
    description=(
        "Answers whether one code is a member of one value set this guide publishes, or - naming a "
        "system alone - whether it is a code of one system this guide publishes. Naming a value set "
        "asks the membership question and is what a form binding is checked against; naming neither "
        "is refused, because a check with nothing to check against would answer false about every "
        "code there is.\n\n"
        "This serves one project's vocabularies and is not a terminology server. A SNOMED CT or "
        "LOINC code is answered `this server publishes no code system under that url`, which is true "
        "and more useful than a guess."
    ),
    response_description="Whether the code holds, and what this server knows about the vocabulary it was checked in.",
)
async def validate_code(
    request: Request,
    code: Annotated[str, Query(description="The code to check.")],
    system: Annotated[str | None, Query(description="The code system the code is stated in.")] = None,
    valueset: Annotated[str | None, Query(description="The canonical of the value set to check.")] = None,
) -> ValidatedCode:
    """Answer whether one code is in one published value set, or is a code of one published system.

    Naming a value set asks the membership question and is what a form binding is checked against.
    Naming a system alone asks the weaker one this server can still answer honestly: is this a code
    the guide publishes at all. Naming neither is refused rather than answered, because a check with
    nothing to check against would answer false about every code there is.
    """
    if system is None and valueset is None:
        raise BadOperationError("name a `system` or a `valueset` to check the code against")
    return terminology_state(request).validate_code(code, system=system, valueset=valueset)

look_up_code(request, system, code) async

Answer what one code is called in one published system, and what that system states about it.

A code the guide does not publish answers 200 with found false and the reason - the same posture $translate takes to a concept its maps say nothing about. The question was well formed and this is the answer to it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/terminology.py
@router.get(
    CODE_LOOKUP_PATH,
    tags=[TERMINOLOGY_TAG],
    summary="Look one code up in a published system",
    description=(
        "Answers what one code is called in one code system this guide publishes, and what that "
        "system states about it.\n\n"
        "A code the guide does not publish answers 200 with `found` false and the reason - the same "
        "posture `$translate` takes to a concept its maps say nothing about. The question was well "
        "formed and this is the answer to it."
    ),
    response_description="What the published system calls the code, or why this server knows nothing about it.",
)
async def look_up_code(
    request: Request,
    system: Annotated[str, Query(description="The code system to look the code up in.")],
    code: Annotated[str, Query(description="The code to look up.")],
) -> LookedUpCode:
    """Answer what one code is called in one published system, and what that system states about it.

    A code the guide does not publish answers 200 with `found` false and the reason - the same
    posture `$translate` takes to a concept its maps say nothing about. The question was well formed
    and this is the answer to it.
    """
    return terminology_state(request).look_up(system, code)

terminology_state(request)

The served vocabularies of this app, loaded from the store the first time one is asked about.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/terminology.py
def terminology_state(request: Request) -> TerminologyState:
    """The served vocabularies of this app, loaded from the store the first time one is asked about."""
    held: TerminologyState | None = getattr(request.app.state, TERMINOLOGY_STATE_ATTRIBUTE, None)
    if held is not None:
        return held
    state = load_terminology(serve_context(request).store)
    setattr(request.app.state, TERMINOLOGY_STATE_ATTRIBUTE, state)
    return state

Metadata health

GET /facade/metadata-health - what the DHIS2 instance behind a live run holds that the guide cannot carry cleanly. The findings are d2w fhir validate's own, reread over the connection this process already holds, and the translation coverage beside them is this module's: which locales the selection carries translations in, how much of it each covers, and which objects are worth naming for it.

Each locale is answered on whichever side of it is the shorter list. Below MAJORITY_SHARE of the selection's translatable strings a locale is sparse and carries carriers - the objects somebody wrote it on - with an empty missing; at or above it the locale is majority and carries missing - the objects nobody has written it for - with an empty carriers. Three stray Spanish translations on a three-thousand-object instance are three translations, not three thousand absences, and no absent translation is graded anywhere: the severities on this answer are the validator's own.

translation_coverage is a pure function over the projected objects, so the arithmetic behind every number on the page answers with no server running. A run serving a compiled guide answers available: false with the reason in words rather than a refusal.

health

What is not right about the DHIS2 metadata behind this run: the validate findings, plus translation coverage.

TWO ANALYSES, ONE ANSWER. The first is d2w fhir validate run over the connection this process already holds - the same passes, the same graders, the same severity grading, and the same wording, because a finding a reader acts on must not be phrased one way in a terminal and another way in a browser. dhis2w_fhir.service.validate_instance_codes is the whole of it, and nothing here re-implements a predicate that lives there.

The second is new here: how much of the selection is translated. DHIS2 holds a translation per object, per property, per locale, and nothing in a published guide states which locales an instance is being maintained in - so the answer is read off the objects themselves. The locales in use are the union of the tags the selection's own translations carry, which needs no system-settings read and is honest on an instance nobody has configured.

THE SMALLER SIDE IS THE STORY, PER LOCALE. A tag three objects out of three thousand carry is not a language the instance is being maintained in, and listing the other two thousand nine hundred and ninety-seven as short of it turns three stray translations into a wall of deficiency. So each locale is read on its own: below half the selection's translatable strings it is SPARSE, and what it states is the objects that carry it; at or above half it is a MAJORITY locale, and what it states is the objects that do not. Either way the counts are the same two numbers - this decides which side is worth naming object by object.

COVERAGE IS A FACT, NOT A GRADE. No absence of a translation is a finding and none is a warning: the severities on this answer are d2w fhir validate's own, and the validator grades names and codes. A locale nobody has finished is a number here and nothing else.

READ ONCE PER RESOURCE KIND. The translation read is one request per scope surface - ten of them - rather than one per object, and the selection is applied to what comes back rather than written into a filter that would put a national instance's organisation-unit UIDs into a query string.

REPORTING ONLY. Nothing here writes to DHIS2, and nothing here offers to. Acting on a finding - changing the name, the code, or the translation in the instance - is the next slice, and the roadmap is where it is stated.

Classes

MetadataHealthFinding

Bases: BaseModel

One thing d2w fhir validate found about a DHIS2 object, as a row states it.

Every field but field and cost is the validator's own, carried across unchanged - the severity it graded, the scope it graded against, and the sentence it wrote. The two that are added here are derived from those: which DHIS2 field the finding is about, and what the grade costs this project.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class MetadataHealthFinding(BaseModel):
    """One thing `d2w fhir validate` found about a DHIS2 object, as a row states it.

    Every field but `field` and `cost` is the validator's own, carried across unchanged - the
    severity it graded, the scope it graded against, and the sentence it wrote. The two that are
    added here are derived from those: which DHIS2 field the finding is about, and what the grade
    costs this project.
    """

    model_config = ConfigDict(frozen=True)

    severity: Literal["error", "warning", "info"]
    scope: Literal["selection", "instance"]
    category: str
    """The validator's own name for the kind of defect - `invalid-code`, `template-hostile-name`."""

    resource_type: str
    """The DHIS2 metadata collection the object belongs to, in DHIS2's own spelling."""

    uid: str
    name: str
    code: str | None = None
    field: str | None = None
    """The DHIS2 field at fault, or None where the category is about none of an object's own fields."""

    message: str
    """The exact problem, in the validator's own words - the same sentence the report file carries."""

    cost: str
    """What this grade costs the project, said in one sentence rather than as a severity word."""
Attributes
category instance-attribute

The validator's own name for the kind of defect - invalid-code, template-hostile-name.

resource_type instance-attribute

The DHIS2 metadata collection the object belongs to, in DHIS2's own spelling.

field = None class-attribute instance-attribute

The DHIS2 field at fault, or None where the category is about none of an object's own fields.

message instance-attribute

The exact problem, in the validator's own words - the same sentence the report file carries.

cost instance-attribute

What this grade costs the project, said in one sentence rather than as a severity word.

MetadataHealthCounts

Bases: BaseModel

How many findings there are of each severity, over the whole answer.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class MetadataHealthCounts(BaseModel):
    """How many findings there are of each severity, over the whole answer."""

    model_config = ConfigDict(frozen=True)

    errors: int = 0
    warnings: int = 0
    infos: int = 0

LocaleCarrier

Bases: BaseModel

One selected object that carries a translation in a locale little of the selection carries.

Listed for a sparse locale and for no other, because on a sparse locale this is the short list: three objects out of three thousand is a fact somebody can read, and the two thousand nine hundred and ninety-seven that do not carry it are not news about anything.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class LocaleCarrier(BaseModel):
    """One selected object that carries a translation in a locale little of the selection carries.

    Listed for a sparse locale and for no other, because on a sparse locale this is the short list:
    three objects out of three thousand is a fact somebody can read, and the two thousand nine
    hundred and ninety-seven that do not carry it are not news about anything.
    """

    model_config = ConfigDict(frozen=True)

    resource_type: str
    uid: str
    name: str
    carries_name: bool = False
    """Whether this object carries a NAME translation in this locale."""

    carries_form_name: bool = False
    """Whether this object has a DHIS2 form name and carries a FORM_NAME translation in this locale."""
Attributes
carries_name = False class-attribute instance-attribute

Whether this object carries a NAME translation in this locale.

carries_form_name = False class-attribute instance-attribute

Whether this object has a DHIS2 form name and carries a FORM_NAME translation in this locale.

LocaleUntranslated

Bases: BaseModel

One selected object holding no translation in a locale most of the selection is translated into.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class LocaleUntranslated(BaseModel):
    """One selected object holding no translation in a locale most of the selection is translated into."""

    model_config = ConfigDict(frozen=True)

    resource_type: str
    uid: str
    name: str
    name_untranslated: bool = False
    """Whether this object holds no NAME translation in this locale."""

    form_name_untranslated: bool = False
    """Whether this object has a DHIS2 form name and holds no FORM_NAME translation in it."""
Attributes
name_untranslated = False class-attribute instance-attribute

Whether this object holds no NAME translation in this locale.

form_name_untranslated = False class-attribute instance-attribute

Whether this object has a DHIS2 form name and holds no FORM_NAME translation in it.

LocaleCoverage

Bases: BaseModel

How much of the selection one locale covers, and which side of it is worth listing.

The two counts are the whole arithmetic. standing decides which of the two lists is filled: a sparse locale carries carriers and an empty missing, a majority locale the other way round, so nothing on the wire is a list whose meaning depends on a sibling field.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class LocaleCoverage(BaseModel):
    """How much of the selection one locale covers, and which side of it is worth listing.

    The two counts are the whole arithmetic. `standing` decides which of the two lists is filled:
    a sparse locale carries `carriers` and an empty `missing`, a majority locale the other way
    round, so nothing on the wire is a list whose meaning depends on a sibling field.
    """

    model_config = ConfigDict(frozen=True)

    locale: str
    """The BCP-47 tag, normalised from the Java locale DHIS2 stores - `pt_BR` reaches here as `pt-BR`."""

    name_count: int = 0
    """Selected objects carrying a NAME translation in this locale."""

    form_name_count: int = 0
    """Selected objects that have a DHIS2 form name and carry a FORM_NAME translation in this locale."""

    standing: Literal["sparse", "majority"] = "sparse"
    """Whether this locale covers less than half the selection's translatable strings, or half or more."""

    carriers: list[LocaleCarrier] = Field(default_factory=list)
    """The objects that carry this locale, filled for a sparse locale and empty for a majority one."""

    missing: list[LocaleUntranslated] = Field(default_factory=list)
    """The objects that do not, filled for a majority locale and empty for a sparse one."""
Attributes
locale instance-attribute

The BCP-47 tag, normalised from the Java locale DHIS2 stores - pt_BR reaches here as pt-BR.

name_count = 0 class-attribute instance-attribute

Selected objects carrying a NAME translation in this locale.

form_name_count = 0 class-attribute instance-attribute

Selected objects that have a DHIS2 form name and carry a FORM_NAME translation in this locale.

standing = 'sparse' class-attribute instance-attribute

Whether this locale covers less than half the selection's translatable strings, or half or more.

carriers = Field(default_factory=list) class-attribute instance-attribute

The objects that carry this locale, filled for a sparse locale and empty for a majority one.

missing = Field(default_factory=list) class-attribute instance-attribute

The objects that do not, filled for a majority locale and empty for a sparse one.

TranslationCoverage

Bases: BaseModel

How far the selection is translated, per locale.

locales is the union of the tags the selection's own translations carry, which is what "in use on this instance" means here: an instance is being maintained in the languages somebody has written into it, and no system setting states that more honestly than the objects do. An empty list is an instance nobody has translated, which is one language and a whole state.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class TranslationCoverage(BaseModel):
    """How far the selection is translated, per locale.

    `locales` is the union of the tags the selection's own translations carry, which is what "in use
    on this instance" means here: an instance is being maintained in the languages somebody has
    written into it, and no system setting states that more honestly than the objects do. An empty
    list is an instance nobody has translated, which is one language and a whole state.
    """

    model_config = ConfigDict(frozen=True)

    locales: list[str] = Field(default_factory=list)
    object_count: int = 0
    """Selected objects the translation read covered."""

    form_named_count: int = 0
    """Of those, how many DHIS2 gives a form name - the denominator the form-name counts are read against."""

    per_locale: list[LocaleCoverage] = Field(default_factory=list)
Attributes
object_count = 0 class-attribute instance-attribute

Selected objects the translation read covered.

form_named_count = 0 class-attribute instance-attribute

Of those, how many DHIS2 gives a form name - the denominator the form-name counts are read against.

MetadataHealth

Bases: BaseModel

The whole answer: whether this run could look, what it found, and how far the selection is translated.

available false is a compiled run and nothing else. It is answered as a body rather than as a refusal because it is a state a screen renders in words - there is nothing wrong with serving a compiled guide, and a page that read a 4xx here would have to invent the sentence this carries.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class MetadataHealth(BaseModel):
    """The whole answer: whether this run could look, what it found, and how far the selection is translated.

    `available` false is a compiled run and nothing else. It is answered as a body rather than as a
    refusal because it is a state a screen renders in words - there is nothing wrong with serving a
    compiled guide, and a page that read a 4xx here would have to invent the sentence this carries.
    """

    model_config = ConfigDict(frozen=True)

    available: bool = True
    reason: str | None = None
    """Why there is nothing to report, stated only when `available` is false."""

    graded_under: str | None = None
    """The `[generate] hostile_names` posture the severities were graded under, in the validator's own line."""

    object_count: int = 0
    """Metadata objects the validator swept, across every collection the instance holds."""

    counts: MetadataHealthCounts = Field(default_factory=MetadataHealthCounts)
    findings: list[MetadataHealthFinding] = Field(default_factory=list)
    translations: TranslationCoverage = Field(default_factory=TranslationCoverage)
Attributes
reason = None class-attribute instance-attribute

Why there is nothing to report, stated only when available is false.

graded_under = None class-attribute instance-attribute

The [generate] hostile_names posture the severities were graded under, in the validator's own line.

object_count = 0 class-attribute instance-attribute

Metadata objects the validator swept, across every collection the instance holds.

TranslatedObject

Bases: BaseModel

One selected DHIS2 object as the translation read projects it - what it is called, and in what locales.

The step between the wire and the coverage: read_selected_translations builds these off what DHIS2 answered, and translation_coverage is a pure function over them, so the arithmetic every number on the page is made of is testable without a request.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
class TranslatedObject(BaseModel):
    """One selected DHIS2 object as the translation read projects it - what it is called, and in what locales.

    The step between the wire and the coverage: `read_selected_translations` builds these off what
    DHIS2 answered, and `translation_coverage` is a pure function over them, so the arithmetic every
    number on the page is made of is testable without a request.
    """

    model_config = ConfigDict(frozen=True)

    resource_type: str
    uid: str
    name: str
    form_named: bool
    """Whether DHIS2 gives this object a form name - so whether it has one to be short a translation of."""

    name_locales: frozenset[str]
    """The normalised tags this object holds a NAME translation in."""

    form_name_locales: frozenset[str]
    """The normalised tags this object holds a FORM_NAME translation in, empty where it has no form name."""
Attributes
form_named instance-attribute

Whether DHIS2 gives this object a form name - so whether it has one to be short a translation of.

name_locales instance-attribute

The normalised tags this object holds a NAME translation in.

form_name_locales instance-attribute

The normalised tags this object holds a FORM_NAME translation in, empty where it has no form name.

Functions:

compiled_run_health()

What a run serving a compiled guide answers: nothing found, and the reason there is nothing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def compiled_run_health() -> MetadataHealth:
    """What a run serving a compiled guide answers: nothing found, and the reason there is nothing."""
    return MetadataHealth(available=False, reason=COMPILED_RUN_REASON)

read_metadata_health(client, config) async

Grade the instance behind this run: the validate findings over the selection, and its translations.

The selection is resolved once and read by both halves - the validator grades severity against it, and the translation read narrows to it - so a national instance is scoped by one set of small reads rather than by two.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
async def read_metadata_health(client: Dhis2Client, config: GenerateConfig) -> MetadataHealth:
    """Grade the instance behind this run: the validate findings over the selection, and its translations.

    The selection is resolved once and read by both halves - the validator grades severity against
    it, and the translation read narrows to it - so a national instance is scoped by one set of
    small reads rather than by two.
    """
    scope = await resolve_validation_scope(client, config)
    report = await validate_instance_codes(client, config, scope=scope)
    objects = await read_selected_translations(client, scope)
    return MetadataHealth(
        graded_under=report.hostile_names_line,
        object_count=report.object_count,
        counts=MetadataHealthCounts(errors=report.error_count, warnings=report.warning_count, infos=report.info_count),
        findings=[health_finding(finding) for finding in report.findings],
        translations=translation_coverage(objects),
    )

health_finding(finding)

One validate finding as a row states it: its own grade and wording, plus the field and the cost.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def health_finding(finding: ValidationFinding) -> MetadataHealthFinding:
    """One validate finding as a row states it: its own grade and wording, plus the field and the cost."""
    return MetadataHealthFinding(
        severity=finding.severity,
        scope=finding.scope,
        category=finding.category,
        resource_type=finding.resource_type,
        uid=finding.uid,
        name=finding.name,
        code=finding.code,
        field=field_at_fault(finding),
        message=finding.message,
        cost=cost_of(finding),
    )

field_at_fault(finding)

Which DHIS2 field one finding is about, or None where the category is about no field of the object.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def field_at_fault(finding: ValidationFinding) -> str | None:
    """Which DHIS2 field one finding is about, or None where the category is about no field of the object."""
    if finding.category in _NAME_CATEGORIES:
        return next(
            (label for label in _FIELD_LABELS_BY_MESSAGE_PREFIX if finding.message.startswith(f"{label} ")),
            "name",
        )
    return FIELD_BY_CATEGORY.get(finding.category)

cost_of(finding)

What one finding's grade costs this project, in a sentence rather than in a severity word.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def cost_of(finding: ValidationFinding) -> str:
    """What one finding's grade costs this project, in a sentence rather than in a severity word."""
    if finding.scope == "instance":
        return _INSTANCE_SCOPE_COST
    return _COST_BY_SEVERITY[finding.severity]

read_selected_translations(client, scope) async

Read the translations the selection's objects carry, one bounded request per resource kind.

Every collection a ValidationScope answers for is read whole and then narrowed to the scope, rather than filtered by a id:in:[...] a national organisation-unit selection would blow the query string with. A collection the selection holds nothing of costs no request at all.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
async def read_selected_translations(client: Dhis2Client, scope: ValidationScope) -> list[TranslatedObject]:
    """Read the translations the selection's objects carry, one bounded request per resource kind.

    Every collection a `ValidationScope` answers for is read whole and then narrowed to the scope,
    rather than filtered by a `id:in:[...]` a national organisation-unit selection would blow the
    query string with. A collection the selection holds nothing of costs no request at all.
    """
    objects: list[TranslatedObject] = []
    for resource_type in sorted(SCOPE_SURFACE_FIELDS):
        surface: frozenset[str] = getattr(scope, SCOPE_SURFACE_FIELDS[resource_type])
        if not surface:
            continue
        body = await client.get_raw(f"/api/{resource_type}", params={"fields": _TRANSLATION_FIELDS, "paging": "false"})
        objects.extend(_translated_objects(resource_type, body, surface))
    return objects

translation_coverage(objects)

How far the selection is translated: the locales in use, and each one read on the side that is smaller.

"In use" is the union of the tags these objects carry, so an instance nobody has translated has no locales and no coverage rows at all - which is the honest reading of an instance working in one language rather than a page full of everything being missing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def translation_coverage(objects: list[TranslatedObject]) -> TranslationCoverage:
    """How far the selection is translated: the locales in use, and each one read on the side that is smaller.

    "In use" is the union of the tags these objects carry, so an instance nobody has translated has
    no locales and no coverage rows at all - which is the honest reading of an instance working in
    one language rather than a page full of everything being missing.
    """
    locales = sorted({locale for item in objects for locale in item.name_locales | item.form_name_locales})
    return TranslationCoverage(
        locales=locales,
        object_count=len(objects),
        form_named_count=_form_named(objects),
        per_locale=[locale_coverage(objects, locale) for locale in locales],
    )

locale_coverage(objects, locale)

One locale's counts, and the shorter of the two lists it can be told through.

A locale under half the selection's translatable strings is told through the objects that carry it; one at or above half is told through the objects that do not. The threshold is the share the page draws its meter from, so the meter and the list under it are readings of one number.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/health.py
def locale_coverage(objects: list[TranslatedObject], locale: str) -> LocaleCoverage:
    """One locale's counts, and the shorter of the two lists it can be told through.

    A locale under half the selection's translatable strings is told through the objects that carry
    it; one at or above half is told through the objects that do not. The threshold is the share the
    page draws its meter from, so the meter and the list under it are readings of one number.
    """
    name_count = sum(1 for item in objects if locale in item.name_locales)
    form_name_count = sum(1 for item in objects if item.form_named and locale in item.form_name_locales)
    total = len(objects) + _form_named(objects)
    covered = name_count + form_name_count
    sparse = total > 0 and covered / total < MAJORITY_SHARE
    return LocaleCoverage(
        locale=locale,
        name_count=name_count,
        form_name_count=form_name_count,
        standing="sparse" if sparse else "majority",
        carriers=_carriers(objects, locale) if sparse else [],
        missing=[] if sparse else _untranslated(objects, locale),
    )

metadata_health

GET /facade/metadata-health - what is not right about the DHIS2 metadata this run publishes from.

WHAT IT ANSWERS. The d2w fhir validate analysis, run over the connection this process already holds, plus one analysis the command does not do: how far the selection is translated. A name the IG publisher cannot survive, a code no FHIR system will take, an object with no code at all, and a locale somebody stopped translating into halfway through are four different problems with one audience - whoever maintains the instance - so they are one page rather than four.

WHY IT IS NOT FHIR. There is no FHIR shape for "this DHIS2 name has a < in it". An OperationOutcome is what a server answers a request with, not a report about somebody else's metadata, and the translation coverage has no resource at all. So this is /spool's shape for /spool's reasons, at /spool's address: plain application/json, Pydantic models rather than a Bundle, served under the facade API's own mount rather than at the FHIR base. dhis2w_fhir_serve.routes.spool argues that choice in full.

The name carries a hyphen and the address carries the mount, so nothing about it can be mistaken for /metadata - which is the FHIR base's CapabilityStatement and a different document about a different thing.

LIVE RUNS ONLY, AND A COMPILED RUN SAYS SO IN THE BODY. Grading metadata needs metadata to grade, and a compiled guide is a directory of resources with no instance behind it. The register routes refuse that state as an OperationOutcome because a FHIR client asked them a FHIR question; this route answers a body carrying available: false and the sentence a screen renders, because "there is nothing here to check and here is why" is a state rather than a failure - and a page that had to read it off a 4xx would end up inventing the sentence itself.

REPORTING ONLY. The route reads. Changing a name, a code, or a translation in DHIS2 from a finding is the next slice, and the FHIR roadmap's near-term section is where it is stated.

Classes

Functions:

read_metadata_health_report(request) async

Answer what the DHIS2 instance behind this run holds that the guide cannot carry cleanly.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/metadata_health.py
@router.get(
    METADATA_HEALTH_PATH,
    tags=[METADATA_HEALTH_TAG],
    summary="Grade the DHIS2 metadata behind this run",
    description=(
        "The `d2w fhir validate` analysis over the connection this process already holds, plus how "
        "far the selection is translated: a name the IG publisher cannot survive, a code no FHIR "
        "system will take, an object with no code at all, and a locale somebody stopped translating "
        "into halfway.\n\n"
        "A run serving a compiled guide off disk answers 200 with `available` false and the sentence "
        "saying there is no instance to grade - a state rather than a failure, so a screen renders "
        "this server's own words instead of inventing them off a 4xx. Reporting only: nothing here "
        "changes anything in DHIS2."
    ),
    response_description=(
        "What the instance holds that the guide cannot carry cleanly, or why there is nothing to grade."
    ),
)
async def read_metadata_health_report(request: Request) -> MetadataHealth:
    """Answer what the DHIS2 instance behind this run holds that the guide cannot carry cleanly."""
    client = live_client(request)
    if client is None:
        return compiled_run_health()
    return await read_metadata_health(client, serve_context(request).project.config.generate)

CDS Hooks

GET /cds-services and POST /cds-services/{id} - the discovery document and one service, which evaluates a caller-supplied CQL library (or a Library this guide publishes) over the resources the hook prefetched and answers one card per define that resolves to true or to a message. There is no feedback endpoint, no suggestions, and no second service; each would mean an EHR state this facade does not hold.

cds

GET /cds-services and POST /cds-services/{id} - CDS Hooks, one service wide.

IT IS AT THE BASE URL AND IT IS NOT FHIR, WHICH IS THE ONE THING TO SAY FIRST. Everything else this facade answers in plain JSON about itself - the receipts, the settings, the evaluator, the vocabularies - is served under /facade, and this is not, because the path is not ours. CDS Hooks fixes discovery at {base}/cds-services exactly as FHIR fixes {base}/metadata: an EHR configured with this server's base URL asks for that path and no other, and a specification's path is not a thing an implementation may move. So the base URL carries three families - FHIR's, CDS Hooks', and the /facade mount - and two of the three are somebody else's specification. dhis2w_fhir_serve.routes states the mounting.

WHAT IS HERE. The discovery document CDS Hooks defines, listing exactly one service, and that service's invocation endpoint. The service evaluates a CQL library over the resources the hook prefetched and answers one card per define that had something to say. That is the whole of it: there is no feedback endpoint, no systemActions, no suggestions, no SMART links, and no second service. Each of those is a real part of the specification and none of them would mean anything from a facade that holds no EHR state - a suggestion is an offer to write into a record this server does not have.

WHERE THE LIBRARY COMES FROM. Two ways, both stated in the request's own hook context. library is CQL text the caller wrote, which is what makes this useful before a guide publishes any Library at all; libraryId names a Library resource in the served guide, whose inline content this server decodes. Naming neither is refused, because a decision-support service with no logic in it is a service that would answer no cards and teach nobody why.

Both of those are extensions of the hook context this facade invented, and they are the honest place for them: CDS Hooks fixes the shape of a patient-view context and has no slot for "the rules to run", because in the specification's world the service is the rules. Here the rules are the caller's, which is what makes this a playground for a guide's own CQL rather than a deployed decision support service.

WHAT BECOMES A CARD. A define answering true becomes an info card summarising the define's name; a define answering a non-empty string becomes an info card whose summary is that string. Everything else - false, null, an empty collection, a number, a resource - becomes no card, because CDS Hooks cards are sentences shown to a clinician and there is no honest sentence to make out of a Patient. A library that will not parse becomes one warning card carrying the parser's own message: an EHR gets an answer it can render rather than a 500 it can only log.

THE DATA IS THE PREFETCH, AND NOTHING ELSE. fhirServer and fhirAuthorization are read off the request and deliberately never followed: a facade that called back out to whatever URL a hook named would be a request-forgery engine wearing a decision-support hat. What the EHR prefetched is what the library sees.

Attributes

CardIndicator = Literal['info', 'warning', 'critical'] module-attribute

How urgent a card is, in CDS Hooks' own three words.

Classes

CdsService

Bases: BaseModel

One service in the discovery document: what it answers on, what it is, and what it wants prefetched.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsService(BaseModel):
    """One service in the discovery document: what it answers on, what it is, and what it wants prefetched."""

    model_config = ConfigDict(frozen=True)

    id: str
    hook: str
    title: str
    description: str
    prefetch: dict[str, str] = Field(default_factory=dict)
    """The prefetch templates an EHR fills before invoking, keyed the way the request carries them."""

    usageRequirements: str | None = None
    """What a caller must supply beyond the hook's own context for this service to answer anything."""
Attributes
prefetch = Field(default_factory=dict) class-attribute instance-attribute

The prefetch templates an EHR fills before invoking, keyed the way the request carries them.

usageRequirements = None class-attribute instance-attribute

What a caller must supply beyond the hook's own context for this service to answer anything.

CdsDiscovery

Bases: BaseModel

GET /cds-services - every service this facade offers, which is one.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsDiscovery(BaseModel):
    """`GET /cds-services` - every service this facade offers, which is one."""

    model_config = ConfigDict(frozen=True)

    services: tuple[CdsService, ...] = ()

CqlLibraryHookContext

Bases: BaseModel

The hook's context, plus the two elements this facade adds to say which rules to run.

extra="allow" because a hook context is the EHR's to shape: patient-view carries userId, patientId, and encounterId today, and a hook this service is later pointed at will carry something else entirely. Refusing a context element this facade does not read would refuse perfectly valid invocations.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CqlLibraryHookContext(BaseModel):
    """The hook's context, plus the two elements this facade adds to say which rules to run.

    `extra="allow"` because a hook context is the EHR's to shape: `patient-view` carries `userId`,
    `patientId`, and `encounterId` today, and a hook this service is later pointed at will carry
    something else entirely. Refusing a context element this facade does not read would refuse
    perfectly valid invocations.
    """

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

    patient_id: str | None = Field(default=None, alias="patientId")
    user_id: str | None = Field(default=None, alias="userId")
    library: str | None = None
    """The CQL library text to evaluate, when the caller brings its own rules."""

    library_id: str | None = Field(default=None, alias="libraryId")
    """The id of a Library resource in the served guide, when the guide publishes the rules."""

    expression_name: str | None = Field(default=None, alias="expressionName")
    """One define to answer. Omitted, every define the library declares is considered for a card."""
Attributes
library = None class-attribute instance-attribute

The CQL library text to evaluate, when the caller brings its own rules.

library_id = Field(default=None, alias='libraryId') class-attribute instance-attribute

The id of a Library resource in the served guide, when the guide publishes the rules.

expression_name = Field(default=None, alias='expressionName') class-attribute instance-attribute

One define to answer. Omitted, every define the library declares is considered for a card.

CdsHookRequest

Bases: BaseModel

One hook invocation as CDS Hooks defines it, read down to what this service acts on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsHookRequest(BaseModel):
    """One hook invocation as CDS Hooks defines it, read down to what this service acts on."""

    model_config = ConfigDict(populate_by_name=True)

    hook: str
    hook_instance: str = Field(alias="hookInstance")
    context: CqlLibraryHookContext = Field(default_factory=CqlLibraryHookContext)
    prefetch: dict[str, Any] = Field(default_factory=dict)
    """What the EHR read for this invocation - the same HTTP-boundary escape hatch `StoreEntry.body` documents.

    Each value is a FHIR resource or a Bundle of them, of whatever types the EHR's prefetch templates
    named, and all of it goes to the evaluator as the FHIR-shaped JSON the engine reads.
    """

    fhir_server: str | None = Field(default=None, alias="fhirServer")
    """The EHR's own FHIR base. Read and never followed - see this module's docstring."""
Attributes
prefetch = Field(default_factory=dict) class-attribute instance-attribute

What the EHR read for this invocation - the same HTTP-boundary escape hatch StoreEntry.body documents.

Each value is a FHIR resource or a Bundle of them, of whatever types the EHR's prefetch templates named, and all of it goes to the evaluator as the FHIR-shaped JSON the engine reads.

fhir_server = Field(default=None, alias='fhirServer') class-attribute instance-attribute

The EHR's own FHIR base. Read and never followed - see this module's docstring.

CdsCardSource

Bases: BaseModel

Who is saying this: the guide whose logic produced the card.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsCardSource(BaseModel):
    """Who is saying this: the guide whose logic produced the card."""

    model_config = ConfigDict(frozen=True)

    label: str
    url: str | None = None

CdsCard

Bases: BaseModel

One thing this service has to say about the patient in front of the user.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsCard(BaseModel):
    """One thing this service has to say about the patient in front of the user."""

    model_config = ConfigDict(frozen=True)

    summary: str
    indicator: CardIndicator
    source: CdsCardSource
    detail: str | None = None

CdsHookResponse

Bases: BaseModel

What one invocation answered. An empty card list is an answer: the rules had nothing to say.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
class CdsHookResponse(BaseModel):
    """What one invocation answered. An empty card list is an answer: the rules had nothing to say."""

    model_config = ConfigDict(frozen=True)

    cards: tuple[CdsCard, ...] = ()

Functions:

discover_services(request) async

List the services this facade offers, which is the one that runs a CQL library over a prefetch.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
@router.get(CDS_SERVICES_PATH)
async def discover_services(request: Request) -> CdsDiscovery:
    """List the services this facade offers, which is the one that runs a CQL library over a prefetch."""
    return CdsDiscovery(
        services=(
            CdsService(
                id=CQL_LIBRARY_SERVICE_ID,
                hook=CQL_LIBRARY_SERVICE_HOOK,
                title="Evaluate a CQL library",
                description=(
                    "Evaluates a CQL library over the prefetched resources and answers one card per define "
                    "that resolves to true or to a message. The library is the caller's: send it as "
                    "`context.library`, or name a Library this guide publishes as `context.libraryId`."
                ),
                prefetch={
                    "patient": "Patient/{{context.patientId}}",
                    "conditions": "Condition?patient={{context.patientId}}",
                    "observations": "Observation?patient={{context.patientId}}",
                },
                usageRequirements=(
                    "Send the rules to run as `context.library` (CQL text) or `context.libraryId` "
                    "(a Library published by this guide). This service holds no rules of its own."
                ),
            ),
        )
    )

invoke_service(request, service_id, invocation) async

Run the named service's library over what the hook prefetched, and answer the cards it produced.

The evaluation runs off the event loop for the reason POST /facade/evaluate runs off it: parsing a grammar and walking a tree is blocking work a facade must not do inline.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/routes/cds.py
@router.post(CDS_SERVICE_PATH)
async def invoke_service(request: Request, service_id: str, invocation: CdsHookRequest) -> CdsHookResponse:
    """Run the named service's library over what the hook prefetched, and answer the cards it produced.

    The evaluation runs off the event loop for the reason `POST /facade/evaluate` runs off it: parsing
    a grammar and walking a tree is blocking work a facade must not do inline.
    """
    if service_id != CQL_LIBRARY_SERVICE_ID:
        raise NotFoundError("cds-services", service_id)
    context = serve_context(request)
    source = _library_source(invocation.context, context.store)
    outcome = await run_in_threadpool(
        evaluate_source,
        EvaluationLanguage.CQL,
        source,
        _prefetched_bundle(invocation),
        invocation.context.expression_name,
    )
    return CdsHookResponse(cards=_cards(outcome, CdsCardSource(label=context.project.config.ig.title)))

Conformance

GET /metadata, and the kind #instance CapabilityStatement it answers with.

metadata

GET /metadata - the conformance endpoint, answered from a body built once at startup.

The statement describes what this process serves, so it is built in the lifespan and served verbatim from then on. Rebuilding it per request would re-read nothing new: the compiled store is fixed for the life of the process. It states no spool count for the opposite reason - d2w fhir forward moves receipts while this server runs, so a number frozen at startup is wrong within minutes, and no number is better than a wrong one. GET /facade/spool answers that question against the directory as it now is.

Classes

Functions:

build_metadata_body(project, store_summary, settings, register_surface, server_version)

Render the server's CapabilityStatement as the JSON body /metadata answers with.

The dict is the wire document itself - the same HTTP-boundary escape hatch StoreEntry.body documents - held pre-rendered so the endpoint serialises nothing per request.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/metadata.py
def build_metadata_body(
    project: FhirProject,
    store_summary: StoreSummary,
    settings: ServeSettings,
    register_surface: RegisterSurface,
    server_version: str,
) -> dict[str, Any]:
    """Render the server's CapabilityStatement as the JSON body `/metadata` answers with.

    The dict is the wire document itself - the same HTTP-boundary escape hatch `StoreEntry.body`
    documents - held pre-rendered so the endpoint serialises nothing per request.
    """
    capability = build_server_capability(
        project=project,
        store_summary=store_summary,
        settings=settings,
        register_surface=register_surface,
        server_version=server_version,
    )
    return capability.model_dump(mode="json", exclude_none=True, by_alias=True)

read_metadata(request) async

Answer with the CapabilityStatement this server started with.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/metadata.py
@router.get("/metadata")
async def read_metadata(request: Request) -> Response:
    """Answer with the CapabilityStatement this server started with."""
    return JSONResponse(content=serve_context(request).capability_body, media_type=FHIR_JSON_MEDIA_TYPE)

capability

The running server's CapabilityStatement: what this process actually serves, right now.

The IG publishes a kind #requirements statement declaring what any DHIS2 capture server has to support. This one is its kind #instance counterpart: it instantiates the IG's statement and then narrows it to this installation - the profiles this project generated, and the read types this store actually holds, so a client that reads /metadata never sees a resource type advertised that the store cannot answer for.

$evaluate is the one operation declared at rest.operation, the server-level slot, because it is the one operation whose URL is the service base's: it runs over whichever resource the request names as its context, so no resource type owns it. A client following this document reaches [base]/$evaluate and is answered there.

The facade surfaces that are not FHIR at all are named in the description rather than declared, and they now live at an address that says so: the facade's own API is mounted at [base]/facade and publishes its own OpenAPI document at [base]/facade/openapi.json, which is where the receipts listing, this project's own JSON evaluation shape, the terminology reads, and the tracked entity record are described in full. The CDS Hooks discovery is the exception that stays at [base], since that specification fixes its path exactly as FHIR fixes this one. A CapabilityStatement describes the FHIR interface, and a slot pointing at [base]/facade/terminology/lookup would be naming a path FHIR has no interaction for. The sentence is what a person reading the conformance document needs; the OpenAPI document is what a client of that surface reads.

The QuestionnaireResponse entry is the one that says what the facade is: responses are received and stored as receipts. Reading one back returns the submission as it arrived, never a live view of what DHIS2 now holds.

Where a live run serves the record, that entry says the other half too: what DHIS2 now holds about one tracked entity is read at /facade/tracked-entities/{uid}/events, in the same shape and under the same profiles. It is stated in the documentation rather than declared as an interaction because it is not one - the address is the tracked entity's, and read and search-type on this type are the receipts. The register's own entries carry the pointer as well, so a client that found somebody learns where their record is without reading the whole statement.

The aggregate half is stated on the same entry and for the same reason: what DHIS2 now holds for one data set, at one organisation unit, over the periods a client names, is read at /facade/data-sets/{uid}/responses, in the shape that data set's own published form describes. Its address is the data set's, so it is prose here and no interaction anywhere.

A process serving [serve] capture = false declares that entry without create, and with nothing else about it changed. The receipts it already holds are read and searched at the same address, so dropping their interactions would be this statement claiming less than the server does. $generate stays for the same reason: it reads a published form and answers with a draft, and writes nothing.

The read set is the capture contract's read types, plus ConceptMap, plus the guide's own conformance resources. The IG's kind #requirements statement names the resources a capture client resolves a form from, and neither of the other two groups is among them - ConceptMap is what a forwarder reads a concept back into DHIS2 identifiers with, and a StructureDefinition is what a validator resolves a profile from. This installation serves both all the same, because they are published IG artifacts sitting in the same store as everything else, and an instance is free to support more than the statement it instantiates.

The conformance entries are the ones that make a served project self-hosting: a guide's canonicals have to resolve somewhere, and until the guide is published under its own canonical this server is the only address they have. They carry the same interactions and the same three search parameters as every other read type, and CONFORMANCE_DOCUMENTATION is where the entry says why it is there - url is the parameter that matters on them, because a client holding a profile canonical it found on a response has a canonical and no id. Each is declared exactly when the store holds that type, on the same terms as ConceptMap: a project served before it was ever compiled declares none of them.

The resource operations are declared on the resource entry they are answered under, which is the entry a client resolves the URL from. $translate is answered at /ConceptMap/$translate and is declared on the ConceptMap entry; $generate is answered at /Questionnaire/{id}/$generate and is declared on the Questionnaire entry. rest.operation would send a client following the statement to [base]/$translate, which this server does not serve - a server-level slot is for a server-level URL, and declaring a resource operation there names an endpoint that answers 404. Each rides its entry, so each is declared exactly when the store holds that type: no ConceptMaps, no $translate.

$generate's definition is the OperationDefinition the project's own IG publishes, not an HL7 one: it is a custom operation, deliberately not SDC's $populate. $translate conforms to R4's own ConceptMap definition and names it.

rest.security is declared in EVERY posture, none included. A conformance document that carries the element only where there is something to protect leaves a client unable to tell "this server checks no credential" from "this server did not say", and those are opposite facts. So the none posture gets a statement of its own, in words. /metadata itself is never behind the check, whatever [serve] auth_scope says, because a client has to be able to read the posture it is expected to meet.

The jwt posture is the one that has to say more than a scheme. A caller who knows this server takes a bearer token still cannot get one without knowing which issuer to ask, so the issuer rides on the element as an extension - JWT_ISSUER_EXTENSION_URL - beside the OAuth code that names the scheme. Never a key, never an audience, never a claim name: the issuer is public by construction, since it is printed inside every token that issuer signs, and the rest is this deployment's own business.

The register's entries are the ones that are not about the store at all. They are answered from the DHIS2 instance per request, and which resource types they are is the published D2TET_CM's to say - one entry per FHIR resource the map takes an in-scope tracked entity type onto. They are declared only by a process that has an instance - --live, over a project that publishes a registration form and therefore names a tracked entity type. A compiled run declares none of them, which is the same refusal the register gives, stated before the request rather than after it - and so does a live run over a project whose [serve.tracked_entities] enabled is false, which is that same refusal reached from the project rather than from the invocation.

Classes

Functions:

register_documentation(resource_type, over)

What one register entry says it answers, and over which of the instance's tracked entity types.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capability.py
def register_documentation(resource_type: str, over: str) -> str:
    """What one register entry says it answers, and over which of the instance's tracked entity types."""
    return (
        f"One DHIS2 tracked entity per {resource_type}, read from the instance at request time, over the "
        f"tracked entity types this guide publishes as {resource_type}: {over}. Identity only - the tracked "
        "entity UID, the values of the attributes DHIS2 declares unique, and the rest of the attribute "
        f"values as extensions. Nothing a {resource_type} otherwise defines is filled in: DHIS2 states no "
        "mapping for those elements."
    )

build_security(posture, scope, *, issuer=None, forward_bearer=False)

State how this process decides who is calling, in every posture including the one that does not.

cors is false in all four because this facade sends no cross-origin headers at all: the capture UI it serves is same-origin with it, and a browser page from anywhere else is a deployment decision for whatever sits in front of this server.

issuer and forward_bearer are the jwt posture's and are ignored in the other three. The issuer is stated because a caller cannot get a token without knowing who to ask; forward_bearer is stated because it decides whether the register answers at all, and a client discovering that from a 501 would be a client this document had misled.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capability.py
def build_security(
    posture: ServeAuth, scope: ServeAuthScope, *, issuer: str | None = None, forward_bearer: bool = False
) -> CapabilityStatementSecurity:
    """State how this process decides who is calling, in every posture including the one that does not.

    `cors` is false in all four because this facade sends no cross-origin headers at all: the capture
    UI it serves is same-origin with it, and a browser page from anywhere else is a deployment
    decision for whatever sits in front of this server.

    `issuer` and `forward_bearer` are the `jwt` posture's and are ignored in the other three. The
    issuer is stated because a caller cannot get a token without knowing who to ask; `forward_bearer`
    is stated because it decides whether the register answers at all, and a client discovering that
    from a 501 would be a client this document had misled.
    """
    if posture is ServeAuth.NONE:
        return CapabilityStatementSecurity(cors=False, description=NO_AUTHENTICATION_DESCRIPTION)
    if posture is ServeAuth.JWT:
        return _jwt_security(scope, issuer=issuer, forward_bearer=forward_bearer)
    stated = TOKEN_AUTHENTICATION_DESCRIPTION if posture is ServeAuth.TOKEN else DHIS2_AUTHENTICATION_DESCRIPTION
    covered = _scope_description(posture, scope)
    return CapabilityStatementSecurity(
        cors=False,
        service=_security_services(posture),
        description=f"{stated} {covered}",
    )

build_server_capability(project, store_summary, settings, register_surface, server_version)

State what this process serves: the capture contract, the read types the store holds, and the register.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/capability.py
def build_server_capability(
    project: FhirProject,
    store_summary: StoreSummary,
    settings: ServeSettings,
    register_surface: RegisterSurface,
    server_version: str,
) -> CapabilityStatement:
    """State what this process serves: the capture contract, the read types the store holds, and the register."""
    canonical = project.config.ig.canonical
    names = FoundationNaming.from_naming(project.config.generate.naming)
    resources = [
        _response_resource(
            project,
            canonical,
            capture=settings.capture,
            record=settings.live and register_surface.serves_events(),
            data_set_responses=settings.live and settings.data_sets.responses,
        ),
        *(
            _read_resource(resource_type, project.config.generate.identifier_system_base, canonical, names)
            for resource_type in SERVED_READ_RESOURCE_TYPES
            if resource_type in store_summary.counts_by_type
        ),
        *_register_resources(settings, register_surface, project.config.ips.enabled),
    ]
    return CapabilityStatement(
        status="active",
        date=current_instant(),
        kind="instance",
        description=(
            f"{project.config.ig.title} served as a FHIR capture facade: {store_summary.total} resources "
            f"in the store, served under the {len(resources)} resource types this statement declares. "
            f"`GET /facade/spool` states how many responses "
            f"are stored, which is a number that changes while this server runs. Beside the FHIR surface "
            f"this process also answers `POST /facade/evaluate` (the same evaluation the declared `$evaluate` "
            f"operation answers, in this project's own JSON shape, with the line and column a parser "
            f"stopped on), "
            f"`GET /facade/terminology/validate-code` and `GET /facade/terminology/lookup` (this guide's own "
            f"vocabularies, not a terminology server), and `GET /cds-services` (CDS Hooks, one service)."
        ),
        instantiates=[f"{canonical}/CapabilityStatement/{names.capture_server_id}"],
        software=CapabilityStatementSoftware(name=SOFTWARE_NAME, version=server_version),
        implementation=CapabilityStatementImplementation(description=_implementation_description(settings)),
        fhirVersion="4.0.1",
        format=["json"],
        rest=[
            CapabilityStatementRest(
                mode="server",
                documentation=_REST_DOCUMENTATION if settings.capture else _VIEWER_REST_DOCUMENTATION,
                security=build_security(
                    settings.auth,
                    settings.auth_scope,
                    issuer=settings.jwt.issuer,
                    forward_bearer=settings.jwt.forward_bearer,
                ),
                resource=resources,
                operation=[
                    CapabilityStatementOperation(
                        name=EVALUATE_OPERATION_NAME,
                        definition=EVALUATE_OPERATION_DEFINITION,
                        documentation=EVALUATE_DOCUMENTATION,
                    )
                ],
            )
        ],
    )

Live store

The store built off a DHIS2 instance instead of a compiled IG, over the same JSON builders the generate targets write to disk.

live

The live store: the resources the facade serves, built off a DHIS2 instance instead of a compiled IG.

--live is the mode with no build step in front of it - point the server at a project and an instance, and it answers with the documents d2w fhir generate would have written and SUSHI would have compiled. One connected client reads the whole instance side of the build and the JSON builders turn that into resources: the store is a snapshot of the instance at startup, exactly as the compiled store is a snapshot of the last build, and no read of it ever touches DHIS2 again.

That one client stays open for the life of the process, because the register is answered from the instance per request rather than from the store - see dhis2w_fhir_serve.register. The caller owns it through open_live_client, so the store build and the register routes share one connection and one profile resolution.

What the builders here produce is the served read-set and nothing else. The definitional artifacts - StructureDefinitions, the extensions, the IG's kind #requirements CapabilityStatement - are authored as FSH and only exist as JSON once SUSHI has compiled them, and no FSH compiler runs in this process. That costs the read-set nothing: a capture server reads Questionnaire, CodeSystem, ValueSet, Location, and Organization (CAPTURE_SERVER_READ_RESOURCE_TYPES), every one of which comes out of a JSON builder here - the foundation terminology included.

The conformance resources join the store all the same, read off whatever SUSHI last compiled beside the project rather than built here (load_compiled_conformance_entries). A guide is one guide whichever way the process was started, and the profiles a served response claims have to resolve somewhere; a live run over a project that has also been compiled hosts them exactly as a compiled run does. A live run over a project that never has holds none, and the CapabilityStatement declares none - which is the honest answer, since there is nothing on disk to serve.

WHAT ONE MODE PUBLISHES, BOTH MODES PUBLISH. A guide is one guide whichever way the process was started, so every vocabulary a compiled build writes is built here too, from the same Python the other target reads:

  • The form-type, period-type, and program-rule-action pairs backing the D2FormType, D2Period, and D2ProgramRule bindings, and the organisation-unit level and whole-selection pairs. All five are declared inside FSH files a live run never compiles, so each has a JSON twin rendering the same Python vocabulary the template renders: a client resolving a served form's form-type code system gets the code system, not a 404.
  • The identifier namespaces the ConceptMaps target - the option, category-option, and category-option-combo UID and code systems - each enumerated as a complete CodeSystem beside the maps that name it. A NamingSystem states what a namespace is and answers no $validate-code, so a consumer validating a mapped identifier needs the enumeration whichever store it reads from.

All four ConceptMap families - option sets, categories, the attribute option combos an aggregate form is keyed by, and the resource type each tracked entity type is registered as - ride along with the terminology they map, so a live store serves the same reads, searches, and $translate answers over the maps that a compiled one does. The IG's own CapabilityStatement is still named by /metadata, which instantiates it by canonical - a URL derived from config, needing no artifact to state.

The example instances are the one thing a compiled store holds and a live one does not, and that is by design: an example is a teaching document a build writes into the guide, not a read a capture client resolves.

Classes

Functions:

open_live_client(project, settings) async

Open the DHIS2 client a live run reads through, named by the profile the project resolves.

The server holds this open for the whole process rather than closing it after the store is built: the register answers from the instance per request, so a live facade has one connection to DHIS2 for its lifetime and closes it when the lifespan unwinds.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/live.py
@asynccontextmanager
async def open_live_client(project: FhirProject, settings: ServeSettings) -> AsyncGenerator[Dhis2Client]:
    """Open the DHIS2 client a live run reads through, named by the profile the project resolves.

    The server holds this open for the whole process rather than closing it after the store is
    built: the register answers from the instance per request, so a live facade has one connection to
    DHIS2 for its lifetime and closes it when the lifespan unwinds.
    """
    generation = resolve_generation_profile(project, settings.profile)
    logger.info(
        "live store: reading %s as profile %s (from %s)",
        generation.profile.base_url,
        generation.name,
        generation.origin,
    )
    async with open_client(generation.profile) as client:
        yield client

build_live_store(project, settings, client) async

Build the store the facade serves from one DHIS2 instance, over the client the caller holds open.

The builders come in two shapes and both land in the same store. The questionnaires and the data dictionary are returned as R4 models, so they are dumped to their wire documents here. The option sets, the categories, the ConceptMaps of both, and the registry are returned as the serialised JSON artifacts the generate targets write to disk, so their documents are read back out of that exact text - what the facade serves live is then byte-identical to what the project would have committed, with no second serialisation path to drift from it.

The names and codes go through the same screening a generate run of this project would put them through, for the same reason the serialisation does: one project means one set of names, and a UID a compiled guide publishes as "Mortality under 5 years" is not one a live facade may serve as something else. _serving_gate is which screening that is.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/live.py
async def build_live_store(project: FhirProject, settings: ServeSettings, client: Dhis2Client) -> ResourceStore:
    """Build the store the facade serves from one DHIS2 instance, over the client the caller holds open.

    The builders come in two shapes and both land in the same store. The questionnaires and the
    data dictionary are returned as R4 models, so they are dumped to their wire documents here.
    The option sets, the categories, the ConceptMaps of both, and the registry are returned as the
    serialised JSON artifacts the generate targets write to disk, so their documents are read
    back out of that exact text - what the facade serves live is then byte-identical to what the
    project would have committed, with no second serialisation path to drift from it.

    The names and codes go through the same screening a generate run of this project would put them
    through, for the same reason the serialisation does: one project means one set of names, and a
    UID a compiled guide publishes as "Mortality under 5 years" is not one a live facade may serve
    as something else. `_serving_gate` is which screening that is.
    """
    config = project.config.generate
    canonical = project.config.ig.canonical
    ig_status = project.config.ig.status
    inputs = await fetch_live_ig_inputs(client, config, gate=_serving_gate(config))
    assignments = build_assignment_artifacts(
        inputs.sources,
        inputs.assignments,
        config,
        published=inputs.organisation_unit_stems,
        stem_plan=inputs.questionnaire_stems,
    )
    decomposition = build_category_decomposition(inputs.sources, inputs.categories, config, canonical)
    attribute_combos = build_attribute_combo_artifacts(
        inputs.sources, config, canonical, ig_status=ig_status, decomposition=decomposition
    )
    questionnaires = build_questionnaire_documents(
        inputs.sources,
        config,
        canonical,
        ig_status=ig_status,
        option_set_plan=inputs.option_set_plan,
        attribute_codes=inputs.attribute_codes,
        option_sets=inputs.option_sets,
        assignments=assignments.plan,
        attribute_combos=attribute_combos.plan,
    )
    data_dictionary = build_data_dictionary_documents(
        inputs.sources, config, canonical, ig_status=ig_status, decomposition=decomposition
    )
    foundation_terminology = build_foundation_terminology_documents(config, canonical, ig_status=ig_status)
    program_rule_actions = _program_rule_action_terminology(config, canonical, ig_status)
    organisation_unit_terminology = _organisation_unit_terminology(
        inputs.organisation_units, config, canonical, ig_status, inputs.organisation_unit_levels
    )
    json_builds: tuple[JsonBuild, ...] = (
        build_option_set_artifacts(
            inputs.option_sets, config, canonical, ig_status=ig_status, attribute_codes=inputs.attribute_codes
        ),
        JsonBuild(
            artifacts=build_option_set_concept_map_artifacts(inputs.option_sets, config, canonical, ig_status=ig_status)
        ),
        JsonBuild(
            artifacts=build_option_set_identifier_artifacts(inputs.option_sets, config, canonical, ig_status=ig_status)
        ),
        build_category_artifacts(
            inputs.categories, config, canonical, ig_status=ig_status, attribute_codes=inputs.attribute_codes
        ),
        JsonBuild(
            artifacts=build_category_concept_map_artifacts(inputs.categories, config, canonical, ig_status=ig_status)
        ),
        JsonBuild(
            artifacts=build_category_identifier_artifacts(inputs.categories, config, canonical, ig_status=ig_status)
        ),
        build_organisation_unit_instances(
            inputs.organisation_units,
            config,
            canonical,
            attribute_codes=inputs.attribute_codes,
            level_names=inputs.organisation_unit_levels,
        ),
        assignments,
        attribute_combos,
        JsonBuild(
            artifacts=build_attribute_combo_concept_map_artifacts(
                inputs.sources, config, canonical, ig_status=ig_status
            )
        ),
        JsonBuild(
            artifacts=build_attribute_combo_identifier_artifacts(inputs.sources, config, canonical, ig_status=ig_status)
        ),
    )
    documents: list[CodeSystem | ConceptMap | Questionnaire | ValueSet] = [
        *questionnaires.questionnaires,
        *data_dictionary.code_systems,
        *data_dictionary.value_sets,
        *data_dictionary.concept_maps,
        *foundation_terminology.code_systems,
        *foundation_terminology.value_sets,
        program_rule_actions.code_system,
        program_rule_actions.value_set,
        *[code_system for build in organisation_unit_terminology for code_system in build.code_systems],
        *[value_set for build in organisation_unit_terminology for value_set in build.value_sets],
    ]
    conformance = load_compiled_conformance_entries(project)
    entries = [
        *(_entry(_document(resource)) for resource in documents),
        *(_entry(json.loads(artifact.content)) for build in json_builds for artifact in build.artifacts),
        *conformance,
    ]
    for note in [*inputs.notes, *questionnaires.notes, *(note for build in json_builds for note in build.notes)]:
        logger.info("live store: %s", note.message)
    logger.info(
        "live store: hosting %d conformance resources from the compiled guide beside the project",
        len(conformance),
    )
    return ResourceStore(entries=tuple(entries))

Errors

Every failed interaction, and the OperationOutcome it answers with.

errors

The facade's error vocabulary: what a route raises, and the OperationOutcome each one answers with.

FHIR gives one error body for every failed interaction, so every handler here ends in the same place - an OperationOutcome served as application/fhir+json. The routes raise a ServeError naming what went wrong in FHIR's own terms; the handlers turn that into the status and issue code R4 pairs with it.

An unexpected exception is the one case the client learns nothing about: the outcome says the server failed, and the traceback goes to the log, because a stack trace on the wire tells a capture client nothing it can act on and tells an attacker plenty.

Attributes

IssueSeverity = Literal['fatal', 'error', 'warning', 'information'] module-attribute

The OperationOutcome.issue.severity values the facade uses.

IssueCode = Literal['invalid', 'not-found', 'not-supported', 'exception', 'processing', 'login', 'forbidden'] module-attribute

The OperationOutcome.issue.code values the facade uses.

login is R4's own code for "the user or system was not able to be authenticated", which is what a 401 off dhis2w_fhir_serve.auth means. It is a child of security in the issue-type hierarchy, and the child says the actual thing. forbidden is its sibling - "the user does not have the rights to perform this action" - and the facade uses it for one answer only: a 403 the DHIS2 instance gave a caller on a pass-through read, carried through as dhis2w_fhir_serve.passthrough describes.

Classes

ServeError

Bases: Exception

A failed interaction the facade can describe in FHIR's terms.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class ServeError(Exception):
    """A failed interaction the facade can describe in FHIR's terms."""

    status_code: int = 500
    issue_code: IssueCode = "exception"

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)
        self.diagnostics = diagnostics

    def response_headers(self) -> dict[str, str]:
        """The headers this refusal has to carry beyond the body, which for almost every one is none.

        A `dict[str, str]` because HTTP headers are the boundary, and the one refusal that overrides
        this is `dhis2w_fhir_serve.auth.UnauthenticatedError`: RFC 9110 requires a 401 to name the
        challenge a client should meet.
        """
        return {}
Methods:
response_headers()

The headers this refusal has to carry beyond the body, which for almost every one is none.

A dict[str, str] because HTTP headers are the boundary, and the one refusal that overrides this is dhis2w_fhir_serve.auth.UnauthenticatedError: RFC 9110 requires a 401 to name the challenge a client should meet.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def response_headers(self) -> dict[str, str]:
    """The headers this refusal has to carry beyond the body, which for almost every one is none.

    A `dict[str, str]` because HTTP headers are the boundary, and the one refusal that overrides
    this is `dhis2w_fhir_serve.auth.UnauthenticatedError`: RFC 9110 requires a 401 to name the
    challenge a client should meet.
    """
    return {}

NotFoundError

Bases: ServeError

The resource type is served, but nothing is stored under that id.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NotFoundError(ServeError):
    """The resource type is served, but nothing is stored under that id."""

    status_code = 404
    issue_code = "not-found"

    def __init__(self, resource_type: str, resource_id: str) -> None:
        super().__init__(f"no {resource_type} with id `{resource_id}` is served here")
        self.resource_type = resource_type
        self.resource_id = resource_id

NotServedError

Bases: ServeError

The facade serves a fixed set of resource types, and this is not one of them.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NotServedError(ServeError):
    """The facade serves a fixed set of resource types, and this is not one of them."""

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(f"this server does not serve the resource type `{resource_type}`")
        self.resource_type = resource_type

NotServedFromCompiledIgError

Bases: ServeError

The resource type is answered from the DHIS2 instance, and this process serves a compiled guide instead.

Same status and issue code as NotServedError, because that is what it is from the client's side - this server does not support that interaction - with the reason stated so the operator reads it as a way the process was started rather than as a missing feature.

The capture UI renders this sentence verbatim on the tracked entities page, so it names the whole command rather than a bare flag: a reader who has never typed d2w fhir serve still has something they can run.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NotServedFromCompiledIgError(ServeError):
    """The resource type is answered from the DHIS2 instance, and this process serves a compiled guide instead.

    Same status and issue code as `NotServedError`, because that is what it is from the client's
    side - this server does not support that interaction - with the reason stated so the operator
    reads it as a way the process was started rather than as a missing feature.

    The capture UI renders this sentence verbatim on the tracked entities page, so it names the
    whole command rather than a bare flag: a reader who has never typed `d2w fhir serve` still has
    something they can run.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"`{resource_type}` is answered from the DHIS2 instance this facade runs against. This facade "
            "serves a compiled implementation guide, so it holds no register to search. "
            "Run `d2w fhir serve --live` to search one."
        )
        self.resource_type = resource_type

RegisterDisabledError

Bases: ServeError

The project serves no tracked entity at all: [serve.tracked_entities] enabled is false.

Same status and issue code as NotServedFromCompiledIgError, and for the same reason - from the client's side this server does not support the interaction - with the config key named so the operator reads it as a decision this project wrote down rather than as a missing feature.

The capture UI renders this sentence verbatim too, so it names the file the decision lives in and the key that reverses it - a --live run refused here would be refused again, and pointing at the process rather than at the project would send the reader the wrong way.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class RegisterDisabledError(ServeError):
    """The project serves no tracked entity at all: `[serve.tracked_entities] enabled` is false.

    Same status and issue code as `NotServedFromCompiledIgError`, and for the same reason - from
    the client's side this server does not support the interaction - with the config key named so
    the operator reads it as a decision this project wrote down rather than as a missing feature.

    The capture UI renders this sentence verbatim too, so it names the file the decision lives in
    and the key that reverses it - a `--live` run refused here would be refused again, and pointing
    at the process rather than at the project would send the reader the wrong way.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"`{resource_type}` is not served here: this project's fhir.toml turns the register off, with "
            "`[serve.tracked_entities] enabled` set to false. Set that key to true and serve again to "
            "search or list the register."
        )
        self.resource_type = resource_type

RegisterListingDisabledError

Bases: ServeError

The register may be searched here but not listed: [serve.tracked_entities] listing is false.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class RegisterListingDisabledError(ServeError):
    """The register may be searched here but not listed: `[serve.tracked_entities] listing` is false."""

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this facade serves no `{resource_type}` listing; name an `identifier` to search for one, "
            "or set `[serve.tracked_entities] listing = true` in fhir.toml and serve again"
        )
        self.resource_type = resource_type

RecordDisabledError

Bases: ServeError

This project publishes who its subjects are and not what was recorded about them.

[serve.tracked_entities] events is false, so the register answers and the record does not. Same status and issue code as every other "this server does not serve that here", with the key named so an operator reads it as a decision this project wrote down rather than as a missing feature.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class RecordDisabledError(ServeError):
    """This project publishes who its subjects are and not what was recorded about them.

    `[serve.tracked_entities] events` is false, so the register answers and the record does not. Same
    status and issue code as every other "this server does not serve that here", with the key named so
    an operator reads it as a decision this project wrote down rather than as a missing feature.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this facade serves no `{resource_type}`: this project publishes who its tracked entities "
            "are and not what was recorded about them; set `[serve.tracked_entities] events = true` in "
            "fhir.toml and serve again"
        )
        self.resource_type = resource_type

DataSetResponsesDisabledError

Bases: ServeError

This project publishes its data set forms and not the values the instance holds for them.

[serve.data_sets] responses is false, so the forms are served and what was reported against them is not. Same status and issue code as every other "this server does not serve that here", with the key named so an operator reads it as a decision this project wrote down rather than as a missing feature.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class DataSetResponsesDisabledError(ServeError):
    """This project publishes its data set forms and not the values the instance holds for them.

    `[serve.data_sets] responses` is false, so the forms are served and what was reported against
    them is not. Same status and issue code as every other "this server does not serve that here",
    with the key named so an operator reads it as a decision this project wrote down rather than as
    a missing feature.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this facade serves no `{resource_type}`: this project publishes its data set forms and not "
            "the values the DHIS2 instance holds for them; set `[serve.data_sets] responses = true` in "
            "fhir.toml and serve again"
        )
        self.resource_type = resource_type

MissingSearchParameterError

Bases: ServeError

A read this server bounds by a parameter was asked without it.

Refused rather than answered widely, which is the whole point: a data set read missing its organisation unit or its period is a read of every organisation unit for every period the data set collects, and a client that asked about one clinic last month would take that answer for it. The refusal names every parameter the read is bounded by, so one round trip states the whole requirement rather than one missing name at a time.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class MissingSearchParameterError(ServeError):
    """A read this server bounds by a parameter was asked without it.

    Refused rather than answered widely, which is the whole point: a data set read missing its
    organisation unit or its period is a read of every organisation unit for every period the data
    set collects, and a client that asked about one clinic last month would take that answer for it.
    The refusal names every parameter the read is bounded by, so one round trip states the whole
    requirement rather than one missing name at a time.
    """

    status_code = 400
    issue_code = "invalid"

    def __init__(self, resource_type: str, parameter: str, requirement: str, why: str) -> None:
        """Carry the refusal naming what was missing, what the read needs, and what it costs to omit it."""
        super().__init__(f"`{parameter}` is required here and this request named none: {requirement}, because {why}")
        self.resource_type = resource_type
        self.parameter = parameter
        self.requirement = requirement
Methods:
__init__(resource_type, parameter, requirement, why)

Carry the refusal naming what was missing, what the read needs, and what it costs to omit it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self, resource_type: str, parameter: str, requirement: str, why: str) -> None:
    """Carry the refusal naming what was missing, what the read needs, and what it costs to omit it."""
    super().__init__(f"`{parameter}` is required here and this request named none: {requirement}, because {why}")
    self.resource_type = resource_type
    self.parameter = parameter
    self.requirement = requirement

TooManySearchValuesError

Bases: ServeError

One parameter was repeated more times than this server answers a single read over.

The count and the limit are both named, because the client can act on neither without the other: a read this server will not answer is a read the client has to split, and how many pieces that takes is what these two numbers say.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class TooManySearchValuesError(ServeError):
    """One parameter was repeated more times than this server answers a single read over.

    The count and the limit are both named, because the client can act on neither without the other:
    a read this server will not answer is a read the client has to split, and how many pieces that
    takes is what these two numbers say.
    """

    status_code = 400
    issue_code = "invalid"

    def __init__(self, resource_type: str, parameter: str, given: int, limit: int, key: str) -> None:
        """Carry the refusal naming the parameter, the count it carried, the limit, and the key that sets it."""
        super().__init__(
            f"this request names {given} `{parameter}` values and this server answers at most {limit} in one "
            f"read: ask for {limit} or fewer, or raise `{key}` in fhir.toml and serve again. Every value named "
            "is read whole, so the count is what bounds what one request costs."
        )
        self.resource_type = resource_type
        self.parameter = parameter
        self.given = given
        self.limit = limit
Methods:
__init__(resource_type, parameter, given, limit, key)

Carry the refusal naming the parameter, the count it carried, the limit, and the key that sets it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self, resource_type: str, parameter: str, given: int, limit: int, key: str) -> None:
    """Carry the refusal naming the parameter, the count it carried, the limit, and the key that sets it."""
    super().__init__(
        f"this request names {given} `{parameter}` values and this server answers at most {limit} in one "
        f"read: ask for {limit} or fewer, or raise `{key}` in fhir.toml and serve again. Every value named "
        "is read whole, so the count is what bounds what one request costs."
    )
    self.resource_type = resource_type
    self.parameter = parameter
    self.given = given
    self.limit = limit

SummaryDisabledError

Bases: ServeError

This project publishes no patient summary: [ips] enabled is false.

Same status and issue code as every other "this server does not serve that here", with the key named so an operator reads it as a decision this project wrote down rather than as a missing feature. A summary is a clinical document about a person, and docs/fhir/design/ips.md R7 puts it behind a key for that reason - offering one is a posture a deployment states rather than one it inherits.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class SummaryDisabledError(ServeError):
    """This project publishes no patient summary: `[ips] enabled` is false.

    Same status and issue code as every other "this server does not serve that here", with the key
    named so an operator reads it as a decision this project wrote down rather than as a missing
    feature. A summary is a clinical document about a person, and `docs/fhir/design/ips.md` R7 puts
    it behind a key for that reason - offering one is a posture a deployment states rather than one
    it inherits.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this server assembles no `{resource_type}` summary: this project sets `[ips] enabled` to "
            "false, so it publishes who its subjects are and what was recorded about them and no "
            "summary over either; set it true in fhir.toml and serve again to answer `$summary`"
        )
        self.resource_type = resource_type

SummaryNotSupportedForSubjectError

Bases: ServeError

$summary was asked of a register whose subjects are not people.

The IPS defines $summary on Patient and nowhere else, and this register serves nine resource types over whatever tracked entity types a project maps onto them. A summary of a cold-chain fridge is not a narrower version of a patient summary - it is a different document nobody has defined - so the refusal names the resources this server does answer it on rather than pretending the operation half-applies.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class SummaryNotSupportedForSubjectError(ServeError):
    """`$summary` was asked of a register whose subjects are not people.

    The IPS defines `$summary` on `Patient` and nowhere else, and this register serves nine resource
    types over whatever tracked entity types a project maps onto them. A summary of a cold-chain
    fridge is not a narrower version of a patient summary - it is a different document nobody has
    defined - so the refusal names the resources this server does answer it on rather than pretending
    the operation half-applies.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str, person_resource_types: tuple[str, ...]) -> None:
        named = ", ".join(f"`{person}`" for person in person_resource_types)
        super().__init__(
            f"`$summary` is a patient summary and `{resource_type}` names no person: this server answers "
            f"it on {named} alone. Whatever is served under `{resource_type}` is served at its own "
            "address as usual; a summary of it is a document nobody has defined."
        )
        self.resource_type = resource_type

ProjectionNotConfiguredError

Bases: ServeError

A search was told to read the materialized projection and this process holds none.

Loud rather than silent, and for the reason PassThroughUnavailableError is loud: the silent alternative is answering the lookup from the DHIS2 instance instead, which would be a different search than the one fhir.toml states, with a different cost, and no cursor to read it by. fhir.toml refuses the pair before the socket opens, so this is what a runtime assembled by hand meets rather than what an operator meets.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class ProjectionNotConfiguredError(ServeError):
    """A search was told to read the materialized projection and this process holds none.

    Loud rather than silent, and for the reason `PassThroughUnavailableError` is loud: the silent
    alternative is answering the lookup from the DHIS2 instance instead, which would be a different
    search than the one `fhir.toml` states, with a different cost, and no cursor to read it by.
    `fhir.toml` refuses the pair before the socket opens, so this is what a runtime assembled by hand
    meets rather than what an operator meets.
    """

    def __init__(self) -> None:
        """Carry the refusal naming both keys, since the fix is the two of them agreeing."""
        super().__init__(
            "this server answers a register search from the materialized projection - `[serve.search] "
            'backend = "projection"` - and holds none to read: state `[serve.projection] store = '
            '"sqlite"` and fill it with `d2w fhir sync`'
        )
Methods:
__init__()

Carry the refusal naming both keys, since the fix is the two of them agreeing.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self) -> None:
    """Carry the refusal naming both keys, since the fix is the two of them agreeing."""
    super().__init__(
        "this server answers a register search from the materialized projection - `[serve.search] "
        'backend = "projection"` - and holds none to read: state `[serve.projection] store = '
        '"sqlite"` and fill it with `d2w fhir sync`'
    )

ProjectionEmptyError

Bases: ServeError

The projection is configured and nothing has ever been synced into it.

404 and not-supported, the same pair every other "this server does not answer that here" carries, because from the client's side that is what it is. The distinction that matters is in the diagnostic: an empty projection is not a register with nobody in it, and answering an empty searchset would tell a client the instance holds no people when nobody has looked yet.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class ProjectionEmptyError(ServeError):
    """The projection is configured and nothing has ever been synced into it.

    404 and `not-supported`, the same pair every other "this server does not answer that here"
    carries, because from the client's side that is what it is. The distinction that matters is in
    the diagnostic: an empty projection is not a register with nobody in it, and answering an empty
    searchset would tell a client the instance holds no people when nobody has looked yet.
    """

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        """Carry the refusal naming the resource, the projection, and the command that fills it."""
        super().__init__(
            f"`{resource_type}` is answered from this project's materialized projection, and nothing has "
            "been synced into it yet - so this server has read nothing about the register rather than "
            "read that it is empty. Run `d2w fhir sync` and ask again."
        )
        self.resource_type = resource_type
Methods:
__init__(resource_type)

Carry the refusal naming the resource, the projection, and the command that fills it.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self, resource_type: str) -> None:
    """Carry the refusal naming the resource, the projection, and the command that fills it."""
    super().__init__(
        f"`{resource_type}` is answered from this project's materialized projection, and nothing has "
        "been synced into it yet - so this server has read nothing about the register rather than "
        "read that it is empty. Run `d2w fhir sync` and ask again."
    )
    self.resource_type = resource_type

NoPublishedSubjectTypeError

Bases: ServeError

The facade runs live, but the guide publishes no registration form, so it knows of no type to search.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NoPublishedSubjectTypeError(ServeError):
    """The facade runs live, but the guide publishes no registration form, so it knows of no type to search."""

    status_code = 404
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this project publishes no registration form, so no tracked entity type is served here and "
            f"`{resource_type}` cannot be searched; generate a tracker program's registration form first."
        )
        self.resource_type = resource_type

UpstreamError

Bases: ServeError

The DHIS2 instance behind this facade refused or failed the read a request depends on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class UpstreamError(ServeError):
    """The DHIS2 instance behind this facade refused or failed the read a request depends on."""

    status_code = 502
    issue_code = "exception"

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)

BadSearchError

Bases: ServeError

The search parameters cannot be read as a query.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class BadSearchError(ServeError):
    """The search parameters cannot be read as a query."""

    status_code = 400
    issue_code = "invalid"

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)

UnsupportedSearchParameterError

Bases: ServeError

The search names a parameter this server does not answer that resource type on.

Refusing is the whole point. A parameter the facade cannot apply, ignored, is answered with everything the endpoint holds - and a client that asked for the people called Smith reads that result set as the people called Smith. A 400 naming the parameter that is answered is the one reply that cannot be misread.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class UnsupportedSearchParameterError(ServeError):
    """The search names a parameter this server does not answer that resource type on.

    Refusing is the whole point. A parameter the facade cannot apply, ignored, is answered with
    everything the endpoint holds - and a client that asked for the people called Smith reads that
    result set as the people called Smith. A 400 naming the parameter that is answered is the one
    reply that cannot be misread.
    """

    status_code = 400
    issue_code = "invalid"

    def __init__(self, resource_type: str, parameter: str, supported_parameters: tuple[str, ...]) -> None:
        """Carry the refusal naming what was asked for and what this server answers instead."""
        named = ", ".join(f"`{supported}`" for supported in supported_parameters)
        answers = f"{named} is the one it supports" if len(supported_parameters) == 1 else f"it answers {named}"
        super().__init__(f"`{parameter}` is not a search parameter this server answers `{resource_type}` on: {answers}")
        self.resource_type = resource_type
        self.parameter = parameter
        self.supported_parameters = supported_parameters
Methods:
__init__(resource_type, parameter, supported_parameters)

Carry the refusal naming what was asked for and what this server answers instead.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self, resource_type: str, parameter: str, supported_parameters: tuple[str, ...]) -> None:
    """Carry the refusal naming what was asked for and what this server answers instead."""
    named = ", ".join(f"`{supported}`" for supported in supported_parameters)
    answers = f"{named} is the one it supports" if len(supported_parameters) == 1 else f"it answers {named}"
    super().__init__(f"`{parameter}` is not a search parameter this server answers `{resource_type}` on: {answers}")
    self.resource_type = resource_type
    self.parameter = parameter
    self.supported_parameters = supported_parameters

UnknownFilterAttributeError

Bases: ServeError

The value filter names a tracked entity attribute this register does not filter on.

Refused rather than answered empty, and that is the difference between this and a _tag naming a type the resource is not served over. A tag names a value that may or may not be held; this names the FIELD, and a field this register has no column for is a query nobody could ever satisfy - a client that read an empty searchset back would take it as "no women are registered here". So the refusal states the attributes this register does filter on, which is the same set /metadata and /facade/uiconfig declare ahead of the request.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class UnknownFilterAttributeError(ServeError):
    """The value filter names a tracked entity attribute this register does not filter on.

    Refused rather than answered empty, and that is the difference between this and a `_tag` naming a
    type the resource is not served over. A tag names a value that may or may not be held; this names
    the FIELD, and a field this register has no column for is a query nobody could ever satisfy - a
    client that read an empty searchset back would take it as "no women are registered here". So the
    refusal states the attributes this register does filter on, which is the same set `/metadata` and
    `/facade/uiconfig` declare ahead of the request.
    """

    status_code = 400
    issue_code = "invalid"

    def __init__(self, resource_type: str, parameter: str, attribute_uid: str, declared: tuple[str, ...]) -> None:
        """Carry the refusal naming the attribute asked for and the ones this register answers on."""
        named = ", ".join(f"`{uid}`" for uid in declared)
        answers = f"it filters on {named}" if declared else "it filters on none"
        super().__init__(
            f"`{parameter}` names the tracked entity attribute `{attribute_uid}`, which `{resource_type}` "
            f"is not filtered by here: {answers}"
        )
        self.resource_type = resource_type
        self.parameter = parameter
        self.attribute_uid = attribute_uid
        self.declared = declared
Methods:
__init__(resource_type, parameter, attribute_uid, declared)

Carry the refusal naming the attribute asked for and the ones this register answers on.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def __init__(self, resource_type: str, parameter: str, attribute_uid: str, declared: tuple[str, ...]) -> None:
    """Carry the refusal naming the attribute asked for and the ones this register answers on."""
    named = ", ".join(f"`{uid}`" for uid in declared)
    answers = f"it filters on {named}" if declared else "it filters on none"
    super().__init__(
        f"`{parameter}` names the tracked entity attribute `{attribute_uid}`, which `{resource_type}` "
        f"is not filtered by here: {answers}"
    )
    self.resource_type = resource_type
    self.parameter = parameter
    self.attribute_uid = attribute_uid
    self.declared = declared

BadOperationError

Bases: ServeError

The parameters an operation was invoked with cannot be read as what the operation declares.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class BadOperationError(ServeError):
    """The parameters an operation was invoked with cannot be read as what the operation declares."""

    status_code = 400
    issue_code = "invalid"

    def __init__(self, diagnostics: str) -> None:
        super().__init__(diagnostics)

NotAnEndpointError

Bases: ServeError

Nothing is served at that path, whatever method it was asked for.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NotAnEndpointError(ServeError):
    """Nothing is served at that path, whatever method it was asked for."""

    status_code = 404
    issue_code = "not-found"

    def __init__(self, path: str) -> None:
        super().__init__(f"`{path}` is not an endpoint this server serves")
        self.path = path

CaptureDisabledError

Bases: ServeError

This server publishes its guide and receives nothing: [serve] capture is false.

405 rather than 404, because the path is served - a client may read and search the receipts this project already holds at the very address it may not post to - and the create interaction is the one thing gone. The config key is named for the reason RegisterDisabledError names its own: an operator reading it should see a decision this project wrote down, not a missing feature.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class CaptureDisabledError(ServeError):
    """This server publishes its guide and receives nothing: `[serve] capture` is false.

    405 rather than 404, because the path is served - a client may read and search the receipts this
    project already holds at the very address it may not post to - and the create interaction is the
    one thing gone. The config key is named for the reason `RegisterDisabledError` names its own: an
    operator reading it should see a decision this project wrote down, not a missing feature.
    """

    status_code = 405
    issue_code = "not-supported"

    def __init__(self, resource_type: str) -> None:
        super().__init__(
            f"this server receives no {resource_type}: this project sets `[serve] capture` to false, so it "
            "serves its guide and stores nothing new; set it true in fhir.toml and serve again to capture"
        )
        self.resource_type = resource_type

MethodNotAllowedError

Bases: ServeError

The path is served, but not for that HTTP method.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class MethodNotAllowedError(ServeError):
    """The path is served, but not for that HTTP method."""

    status_code = 405
    issue_code = "not-supported"

    def __init__(self, method: str, path: str) -> None:
        super().__init__(f"`{method}` is not supported on `{path}`")
        self.method = method
        self.path = path

BatchNotSupportedError

Bases: ServeError

The service base was posted to, which in FHIR is a batch or a transaction this facade does not run.

POST [base] is the one interaction the root path has, so the refusal names it rather than stating a bare method mismatch: a client sending a Bundle there is asking for batch processing, and this facade takes one QuestionnaireResponse per request instead.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class BatchNotSupportedError(ServeError):
    """The service base was posted to, which in FHIR is a batch or a transaction this facade does not run.

    `POST [base]` is the one interaction the root path has, so the refusal names it rather than
    stating a bare method mismatch: a client sending a Bundle there is asking for batch processing,
    and this facade takes one QuestionnaireResponse per request instead.
    """

    status_code = 405
    issue_code = "not-supported"

    def __init__(self, method: str) -> None:
        super().__init__(
            f"`{method} /` is not served here: this server runs no batch and no transaction. Post one "
            "QuestionnaireResponse per request to `/QuestionnaireResponse`."
        )
        self.method = method

NotAcceptableError

Bases: ServeError

The request accepts no format this server answers in.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class NotAcceptableError(ServeError):
    """The request accepts no format this server answers in."""

    status_code = 406
    issue_code = "not-supported"

    def __init__(self, accept: str) -> None:
        super().__init__(
            f"`{accept}` accepts no JSON, and this server answers `{FHIR_JSON_MEDIA_TYPE}` only; "
            "ask for that, for `application/json`, or for `*/*`"
        )
        self.accept = accept

UnsupportedFormatError

Bases: ServeError

The request's _format names a format this server does not answer in.

_format overrides Accept, so a value this server cannot serve is refused even where the header would have admitted JSON: the client stated the format it wants the answer in, and handing it JSON instead would answer a question it did not ask.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class UnsupportedFormatError(ServeError):
    """The request's `_format` names a format this server does not answer in.

    `_format` overrides `Accept`, so a value this server cannot serve is refused even where the
    header would have admitted JSON: the client stated the format it wants the answer in, and
    handing it JSON instead would answer a question it did not ask.
    """

    status_code = 406
    issue_code = "not-supported"

    def __init__(self, stated_format: str) -> None:
        super().__init__(
            f"`_format={stated_format}` names a format this server does not serve, and this server answers "
            f"`{FHIR_JSON_MEDIA_TYPE}` only; ask for `_format=json`, for `_format=application/json`, or for "
            "`_format=application/fhir+json`"
        )
        self.stated_format = stated_format

UnsupportedMediaTypeError

Bases: ServeError

The request body is declared in a media type this server does not read.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
class UnsupportedMediaTypeError(ServeError):
    """The request body is declared in a media type this server does not read."""

    status_code = 415
    issue_code = "not-supported"

    def __init__(self, media_type: str) -> None:
        super().__init__(
            f"`{media_type}` is not a media type this server reads; send the body as `{FHIR_JSON_MEDIA_TYPE}`"
        )
        self.media_type = media_type

Functions:

outcome(status_code, severity, code, diagnostics, expression=None, headers=None)

Build the OperationOutcome response one failed interaction answers with.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def outcome(
    status_code: int,
    severity: IssueSeverity,
    code: IssueCode,
    diagnostics: str,
    expression: tuple[str, ...] | None = None,
    headers: dict[str, str] | None = None,
) -> JSONResponse:
    """Build the OperationOutcome response one failed interaction answers with."""
    body = OperationOutcome(
        issue=[
            OperationOutcomeIssue(
                severity=severity,
                code=code,
                diagnostics=diagnostics,
                expression=list(expression) if expression else None,
            )
        ]
    )
    return JSONResponse(
        status_code=status_code,
        content=body.model_dump(mode="json", exclude_none=True, by_alias=True),
        media_type=FHIR_JSON_MEDIA_TYPE,
        headers=headers,
    )

register_error_handlers(app)

Answer every failure - raised, routed, or unexpected - with an OperationOutcome.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/errors.py
def register_error_handlers(app: FastAPI) -> None:
    """Answer every failure - raised, routed, or unexpected - with an OperationOutcome."""
    app.add_exception_handler(ServeError, _handle_serve_error)
    app.add_exception_handler(RequestValidationError, _handle_validation_error)
    app.add_exception_handler(StarletteHTTPException, _handle_http_exception)
    app.add_exception_handler(Exception, _handle_unexpected_error)

Request logging

One log line per interaction, and the plain configuration the server process runs under.

log

Request logging: one line per interaction, and the plain configuration the server process runs under.

The line is the facade's whole access log - method, path, status, duration - because a FHIR endpoint's useful signal is which resource a client asked for and whether it got it. A request that ends in an unhandled exception is logged at the same shape before the exception continues to the error handler, so a 500 is never a silent gap in the log.

Classes

RequestLogMiddleware

Bases: BaseHTTPMiddleware

Write one log line per request, whether it answered or raised.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/log.py
class RequestLogMiddleware(BaseHTTPMiddleware):
    """Write one log line per request, whether it answered or raised."""

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        """Time the request, log its outcome, and let the response or the exception through."""
        started = time.perf_counter()
        try:
            response = await call_next(request)
        except Exception:
            _log_request(request, status_code=500, started=started)
            raise
        _log_request(request, status_code=response.status_code, started=started)
        return response
Methods:
dispatch(request, call_next) async

Time the request, log its outcome, and let the response or the exception through.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/log.py
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
    """Time the request, log its outcome, and let the response or the exception through."""
    started = time.perf_counter()
    try:
        response = await call_next(request)
    except Exception:
        _log_request(request, status_code=500, started=started)
        raise
    _log_request(request, status_code=response.status_code, started=started)
    return response

Functions:

configure_logging(level=logging.INFO)

Send this package's log to stderr under a plain formatter, leaving the root logger alone.

Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/log.py
def configure_logging(level: int = logging.INFO) -> None:
    """Send this package's log to stderr under a plain formatter, leaving the root logger alone."""
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter(_LOG_FORMAT))
    logger.handlers = [handler]
    logger.setLevel(level)
    logger.propagate = False

Package surface

The names below re-export from dhis2w_fhir_serve itself.

dhis2w_fhir_serve

FHIR facade over a generated IG project: serves its resources and receives QuestionnaireResponse captures.

Each module owns its schemas; this module is the one stable import surface over them, so from dhis2w_fhir_serve import ResourceStore keeps working however the internals are arranged.

What is deliberately NOT here is the capture UI. dhis2w_fhir_serve.ui and the /facade/uiconfig document exist so the built React bundle can work, they are reached by running the server with settings.ui, and create_app is the only thing that mounts them. UiBundleMissingError is the one exception: create_app raises it while building, so a caller of create_app has to be able to catch it by name.