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 servegets 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
Parametersinput, and say an evaluation's answer as theParametersa 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/spoolpages 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:
- The settings.
ServeSettings.resolveapplies the flag-over-[serve]precedence, resolves the DHIS2 profile, and refuses a project that has never been built. ConstructingServeSettingsdirectly is supported too, and a facade built that way differs fromd2w fhir serveon purpose rather than by accident. - The runtime.
open_serve_runtimeloads 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. Underauth = "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 ownAuthorizationover; see credential pass-through.attach_serve_runtimeputs all three things the handlers read onto the application, and nothing serves a request before it has been called. - The routers.
serve_routersstates the mount requirements as data: the FHIR routers carryDepends(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/healthmounts first or it is gone - and every router gets the HEAD sweep, or a liveness probe askingHEAD /metadatareads a live facade as down.ServeRouters.guardedis the fourth requirement, and the one an application usually answers itself: see bring your own authentication. - The error handlers. Without
register_error_handlers, every typed refusal the facade raises -RegisterDisabledError,NotServedError,CaptureDisabledError- is a 500 with noOperationOutcomein 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.
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.
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.
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.
The full facade. Every line above, in one call.
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
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
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
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
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
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
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
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
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
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
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
server_version()
¶
The installed version of this package, as the app and its CapabilityStatement report it.
facade_provenance()
¶
What a pass-through read names itself as to the instance: this software, and its 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
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
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
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
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
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
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
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | |
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
current_monotonic()
¶
The reading the cache measures its own entries against - monotonic, so a clock change is not an expiry.
credential_key(header_value)
¶
The cache key for one Authorization value - a hash, so no plaintext credential is held.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
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
by_type_and_id(resource_type, resource_id)
¶
The resource a GET /{type}/{id} read resolves to, or None.
by_canonical(canonical_url)
¶
The resource a canonical url resolves to, whatever its type, or None.
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
concept_maps()
¶
Every ConceptMap the store holds, as the R4 models $translate reads its mappings off.
types_present()
¶
summary()
¶
Resource counts per type.
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
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
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
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
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
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
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
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
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
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
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
token()
¶
This cursor as the page parameter carries it.
Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/spool.py
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
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
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 | |
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
directory_for(lifecycle)
¶
Where receipts in one lifecycle state are read from and written to.
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
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
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
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
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
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
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
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
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
count()
¶
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
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
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
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
requested_cursor(stated)
¶
Which page was asked for - the first one when the request names none.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | |
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
item_link_ids = ()
class-attribute
instance-attribute
¶
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
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
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
asked_link_ids(index, answers)
¶
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
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:
- concept code - what the contract asks for, and the only tier a strict server accepts.
- option UID - a client that sent the DHIS2 UID against a code-mode CodeSystem.
- 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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/trackedEntitiesrequires - 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
#choicequestion 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_CScannot answer either: it publishes every attribute the project's forms ask anywhere, without saying whose. D2TET_CMmaps 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 publishedsubjectTypecannot disagree. A published type the map says nothing about is aPatient, which is the same default the map itself was built from.D2TEA_CSpublishes one concept per tracked entity attribute the forms ask, carrying the DHIS2 code, auniqueboolean, and asearchableboolean. 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
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
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.
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
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
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
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | |
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
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
serves_tracked_entities()
¶
True when the guide published a tracked entity type, which is what a DHIS2 search requires.
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
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
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
attribute_for_system(system)
¶
The attribute whose values are carried under one identifier system, or None.
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
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
program_name(program_uid)
¶
The name this guide publishes one program under, or None when the program is outside its selection.
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
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
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 thePatientevery 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
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
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
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
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
serves_listing()
¶
True when a search naming no identifier is answered with a page rather than a refusal.
serves_events()
¶
True when one entity's own record is answered here, as well as its identity.
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
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
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
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
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
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:
- 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. - 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. - 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, withoutprogram=, and inspected here. - 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 itsfieldsexplicitly - includingenrollments[...]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. - A page states how many pages there are only when it is asked to.
pageandpageSizecome back on every request;totalandpageCountcome back only undertotalPages=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
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
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
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
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
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
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
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
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
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
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
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
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
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
is_tracked_entity_uid(value)
¶
Whether a value could be a DHIS2 UID at all - eleven alphanumerics starting with a letter.
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
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:
resourceTypeis what the published map takes the entity's tracked entity type onto.idis 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 aD2TEA_CSconcept 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.tagstates 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 inidentifier[]- 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 theD2TrackedEntityAttributeValueextension: 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
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
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
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
token()
¶
This cursor as the page parameter carries it.
Source code in packages/dhis2w-fhir-serve/src/dhis2w_fhir_serve/register/listing.py
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
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
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
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
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
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
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
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-entityis the tracked entity UID itself and is answered by reading that one entity, not by filtering - a UID is not an attribute and nofilter=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_entitysays whether this caller may have them, per person, per request. That isdocs/fhir/design/projection.mdR9 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
outcomeentry 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
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.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Methods:¶
read(resource_type, resource_id)
async
¶
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.
write(batch)
async
¶
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.
watermarks()
async
¶
How far each tracker collection has been read, which is what the next poll asks from.
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
Methods:¶
index(entries)
async
¶
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.
forget(tracked_entity_uids)
async
¶
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
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.
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
forget(tracked_entity_uids)
async
¶
Drop nothing: an entity DHIS2 no longer holds is one this backend stops finding by itself.
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
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
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
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
¶
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
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
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
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
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
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
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
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
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.
- The watermark advances only when the rows it describes are durable.
writeputs 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). - A write is idempotent by resource id. A sync re-polls from
watermark - overlapand 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). - A tombstone removes the row.
removeddeletes 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.mdsection 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
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
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
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
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
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
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
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
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
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
close()
async
¶
Dispose of the connection, which is what the serve lifespan does when the process unwinds.
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
dhis2backend putsfilter=<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 whatdocs/fhir/design/projection.mdsection 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
dhis2backend 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
Methods:¶
__init__(store)
¶
Read through the projection store that owns the file - see this module's docstring on why.
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
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
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
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
updatedAfterandincludeDeleted=true, apply creates, updates, and tombstones, and advance the watermark. Measured idle cost: one request, 56 bytes. - Full rebuild.
--rebuilddrops to empty and refills. Per D3 that is routine rather than a recovery step, and it is how afhir.tomlmapping 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
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
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
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
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
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.
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
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
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
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
as_of_headers(cursor)
¶
The one header every projection-served response carries, whatever shape its body is.
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
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
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
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
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:
- The events come off the tracked entity, never off a program.
/api/tracker/eventsdemands aprogramunconditionally on 2.43 and answers a Tomcat HTML page when it is missing (BUGS.md 91), and its singularenrollment=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. - 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". - The default projection carries none of this. The tracked entity endpoint omits the enrollments
entirely unless
fieldsnames 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. - 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
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
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
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
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
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
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
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
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
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
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | |
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).
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
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
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
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
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
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
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
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
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.
trueis a dose with no dose number;falseis not a dose at all and produces nothing. DHIS2 says the vaccine was not given and states no reason, and R4 requires astatusReasonon anot-doneimmunization - 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
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
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
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
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
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
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
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
parse_store_search(params)
¶
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
parse_response_search(params)
¶
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
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
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
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
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
base_url(request)
¶
The service base every fullUrl and self link is built from, without its trailing slash.
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
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
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
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
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
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
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
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
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
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
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
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
Methods:¶
of_period(period)
classmethod
¶
The window one reporting period covers.
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
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
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
Functions:¶
draw_seed()
¶
Draw the seed a $generate call that named none is answered from.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
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
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
value_set_urls()
¶
Every value set url this surface answers membership for, sorted.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | |
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.