FHIR IG generation (dhis2w_fhir)¶
dhis2w_fhir is the package behind d2w fhir: the fhir.toml
document, the emitters that turn DHIS2 metadata into an Implementation Guide - FSH for the
definitional artifacts, pre-built R4 JSON for the organisation-unit registry, the
option-set terminology, and the category terminology - and the DHIS2 period grammar they
share. It mounts onto the CLI
through the dhis2w.plugins.v1 entry point, and every component symbol
re-exports from the top-level package, so
from dhis2w_fhir import GenerateConfig, parse_period keeps working however
the components are arranged internally. The R4 resource models are the one exception: they
live in dhis2w_fhir.r4, which is FHIR's own vocabulary rather than part of the plugin
surface.
When to reach for it¶
- Read or write a project's
fhir.tomlfrom Python (load_project,load_fhir_config,find_project_fhir_config,write_fhir_config). - Parse a DHIS2 ISO period into its type and date range, or walk backwards from a date
(
parse_period,recent_periods,PERIOD_TYPE_DEFINITIONS). - Carry DHIS2 attribute values onto a generated resource (
AttributeValueIn,AttributeCodeIndex,resolve_attribute_code_index,attribute_value_extensions). - Build FSH artifacts without the CLI (
build_foundation_artifacts,build_questionnaire_artifacts,build_page_artifacts) and sync them to disk (sync_artifacts). - Build the pre-built R4 documents - the registry (
dhis2w_fhir.r4.Organization,dhis2w_fhir.r4.Location), the option-set terminology (build_option_set_artifacts), and the category terminology (build_category_artifacts), the last two ondhis2w_fhir.r4.CodeSystemanddhis2w_fhir.r4.ValueSet- and sync them to disk (sync_json_artifacts), one owned directory per source. - Map an option set's generated concept codes back to the DHIS2 option UID and
option code (
build_option_set_concept_mapsfor thedhis2w_fhir.r4.ConceptMapmodels,build_option_set_concept_map_artifactsfor the JSON files that land inCONCEPT_MAP_DIRECTORY). - Generate one target, or the whole guide, without the command
(
generate_foundation,generate_option_sets,generate_categories,generate_questionnaires,generate_examples,generate_organisation_units,generate_pages,generate_full). Each returns aGenerateReport, and each raisesBuildAbortingCodeErrororBuildAbortingNameErrorrather than writing a guide the IG publisher will die on hours later. - Scaffold a project, or re-render one that already exists (
init_project,refresh_project,read_project_scaffold_state,ProjectScaffoldState). - Produce a validation report rather than only rendering one (
validate_codes, withresolve_validation_context,resolve_validation_scope, andresolve_code_sourcefor the three things a run has to decide first).build_aborting_codeandbuild_aborting_nameare the two predicates the report's error grade and the generate refusal share, so a caller can ask the same question either command asks;display_coderenders a DHIS2 code for human eyes the way the report does. - Ask the same question of the files a build publishes, with no instance behind it
(
check_publishable_artifacts, returning anArtifactCheckReportofArtifactFindings). It reads a project's compiled resources, pre-built JSON, and FSH sources through those same two predicates, which is whatd2w fhir check-artifactsandmake buildrun. - Run the conformance chain in process, or grade one phase of it on its own (
run_doctor,DoctorOptions,DoctorReport,render_doctor_markdown,phase_evidence,resolve_doctor_profile, and the gradersgrade,grade_capture,grade_forward,grade_oracleoverDoctorFinding,CaptureOutcome, andFamilyOutcome). Readrun_doctor's own docstring first: it writes a workspace, shells out to a compiler, and posts a corpus, which the graders do not. - Assemble one person's International Patient Summary out of a projected subject and the
doses somebody already read (
build_patient_summary,AssembledSummary,RecordedDose,summary_caveat,REQUIRED_SECTIONS,IpsSection), and publish the section mapping behind it as a ConceptMap (build_section_concept_map,build_section_concept_map_artifacts). The assembly opens no connection and reads no store, so a command is as free to call it as a served facade is. - Record why a spooled response was refused, and read the record back (
record_refusal,read_refusal_record,ForwardRefusalRecord,RefusalReason,SPOOL_RELATIVE_PATH).ForwardRefusalRecordis the declared type ofSpooledReceipt.refusal, so a caller reading receipts holds instances of it either way. - Talk to a running facade from Python without composing a request (
FacadeClient, withsubmit_responseanswering aCaptureReceipt,generate,read_response,read,searchover aResourceQuery,resolvefor a canonical,capability, andevaluateanswering anEvaluationOutcome). Credentials areBearerToken,UsernamePassword, orPersonalAccessToken; a refusal raisesFacadeErrorcarrying theOperationOutcomethe facade stated its reason in.
Every capability above that reads DHIS2 takes the connection as an argument:
client= on validate_codes, run_doctor, and each generate target, with the Profile
form kept as the convenience wrapper the commands use. A handed-in client is used as it
stands and left open, so an application already holding an authenticated connection makes
one connection rather than one per call.
Worked example — parse a period, then walk backwards¶
import datetime
from dhis2w_fhir import parse_period, recent_periods
parse_period("2024BiW2")
# PeriodValue(iso='2024BiW2', period_type='BiWeekly',
# start_date=date(2024, 1, 15), end_date=date(2024, 1, 28))
recent_periods("Monthly", 3, datetime.date(2026, 8, 2))
# ['202607', '202606', '202605']
Reference¶
Periods¶
period
¶
DHIS2 reporting periods: the ISO period grammar, its period-type catalogue, the parser, and its inverse.
Classes¶
PeriodTypeDefinition
¶
Bases: BaseModel
One DHIS2 period type as terminology: its name, its ISO format, and a display phrase.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/schemas.py
PeriodValue
¶
Bases: BaseModel
One DHIS2 reporting period: its ISO identifier, its period type, and the dates it covers.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/schemas.py
Functions:¶
parse_period(iso)
¶
Parse a DHIS2 ISO period string into its period type, start date, and end date.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/parser.py
recent_periods(period_type, count, today)
¶
The count most recent completed ISO periods of period_type, newest first.
A period counts as completed when its end date falls strictly before today. An
unregistered period type, or a non-positive count, yields an empty list rather than
raising - the caller is discovering data, not validating configuration.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/period/recent.py
Project configuration¶
config
¶
The fhir.toml document: its models plus discovery, load, and save.
A FHIR IG project is any directory holding a fhir.toml (scaffolded by
d2w fhir init). Discovery walks up from the working directory, mirroring
how .dhis2/profiles.toml is found.
The document composes the per-component selection tables, so this module depends on the components and never the other way round - an emitter receives its config as a parameter.
Classes¶
NoFhirProjectError
¶
UnknownFhirConfigKeyError
¶
MalformedFhirConfigError
¶
Bases: CliUserError
Raised when fhir.toml is not valid TOML, naming the file and where the parser stopped.
IgConfig
¶
Bases: BaseModel
SUSHI IG identity - the [ig] table of fhir.toml.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
NamingConfig
¶
Bases: BaseModel
The identity source and the FSH naming tokens - the [generate.naming] table of fhir.toml.
source picks the identity stem every artifact of an object derives from: the FHIR
resource id, the canonical URL, the file name, and the FSH name all follow one resolved
segment. "id" (the default) takes the DHIS2 id verbatim; "code-or-id" takes the
object's code when it is usable as a stem and unique in the run, falling back to the id
with a note; "code" requires such a code on every selected object and refuses the run
otherwise.
Artifact names merge the prefix and kind tokens and underscore the rest
(D2 + OS + _BirthType + _CS); ids join the kebab of each non-empty token
(d2-os-birth-type-cs). prefix, option_set, category, attribute_option_combo,
data_set, program, and program_stage may be empty to drop them;
organisation_unit must stay non-empty or the org-unit artifact names would degenerate
to bare _CS/_Level_CS. tracked_entity_type names the person-only registration form a
tracked entity type publishes. attribute_option_combo names the vocabulary a data
set's non-default category combo publishes, and it takes a token of its own rather than the
COC the data dictionary uses: D2COC_CS is the disaggregation vocabulary a
question's cells are coded from, while D2AOC_*_VS is the vocabulary a response's
attribute option combo is drawn from, and the two are bound in different places. Future
group / group-set artifacts follow the same scheme (OUG, OUGS).
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
HostileNamePosture
¶
Bases: StrEnum
What a generate run does with a DHIS2 name the IG publisher's own build cannot survive.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
GenerateConfig
¶
Bases: BaseModel
Generation behaviour - the [generate] table of fhir.toml.
The four data-definition tables select the questionnaire form kinds: data_sets picks
aggregate data sets, event_programs picks programs without registration, tracker_programs
picks programs with registration (one Questionnaire per program stage plus the program's own
registration form), and tracked_entity_forms picks the tracked entity types that publish a
person-only registration form - a form that creates a person and enrols them in nothing.
Empty, tracked_entity_forms publishes one form per type the selected tracker programs
track; the other three publish everything of their kind the instance holds.
hostile_names is what the run does with a DHIS2 name carrying <, which the IG publisher
writes into a page it strict-parses and then dies on. Unset, the run asks on a terminal and
refuses without one, so a script is never left hanging on a question; "refuse" answers it
with today's refusal and "substitute" publishes the rewritten wording. d2w fhir generate
--substitute-hostile-names and --refuse-hostile-names answer it for one run.
timezone is the IANA zone the instance's zone-less timestamps are wall-clock readings in
(BUGS.md #62). Naming it turns every emitted dateTime into the numeric offset that zone
was on at that very instant, DST included; leaving it unset keeps the UTC reading.
tracked_entity_types maps a DHIS2 tracked entity type UID onto the FHIR resource type its
registrations are about. A DHIS2 tracked entity is not always a person - a project tracks
households, buildings, herds, and equipment as readily as patients - so the type says what it
is and every form of every program tracking it follows. A type named here is not selected by
it: selection stays with the three data-definition tables, and a type this table never
mentions is a Patient, which is what keeps a person-tracking project's config empty.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
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 | |
BasemapSource
¶
Bases: BaseModel
One named raster tile source the capture UI's map offers as a layer.
name is what the map's layer control calls it and is the deployment's own word - this
project never renames a source it was pointed at. url is the {z}/{x}/{y} template the
tiles are fetched from, and it is the one thing in the whole UI that reaches an origin other
than the server the page came from.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
DataSetsConfig
¶
Bases: BaseModel
What a live run answers about the instance's aggregate data - the [serve.data_sets] table.
The register's sibling, and the same posture: every default offers everything, and the table exists for the deployment that wants less. It says what this facade will tell a client about the values the instance holds for a data set, which is a decision a project makes once rather than one per invocation, so no flag overrides it.
responses is the whole surface: false and GET /facade/data-sets/{uid}/responses answers the
not-supported outcome naming this key, while the guide's own forms, the receipts, and the
register are served exactly as they were. There is no second enabled key beside it, because
there is no aggregate register for one to take away - a data set's values are the only thing
this table is about.
page_size is what one page carries when the client names no _count, and page_size_limit is
the largest _count honoured: a client asking for more is served the limit rather than refused.
data_sets means "the ones the guide publishes" when empty, which is what keeps this table
absent from a project that publishes what it serves. Naming UIDs restricts the surface to them,
and a data set outside the list is answered exactly as one the guide publishes no form for.
period_limit is the most periods one read may name. Every read is answered whole, so the
periods a request names are what bounds its cost; a request naming more is refused with the
limit and the count it gave.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
TrackedEntitiesConfig
¶
Bases: BaseModel
The register a live run serves - the [serve.tracked_entities] table of fhir.toml.
Tracked entities are the one thing this facade answers from the DHIS2 instance rather than from what it published, so what it will say about them is stated here rather than inferred from the guide. The table is register-wide: it says the same thing about every FHIR resource the published map takes a tracked entity type onto, because whether this process answers about the instance's subjects at all is one decision rather than one per resource type.
enabled is the whole register: false and every register route answers the not-supported outcome
and /metadata declares none of its resource types, in a live process exactly as in a compiled
one. listing is the no-parameter search alone - false leaves identifier search untouched and
refuses only the request that means "everybody", which is the posture for an instance whose
register is not something a capture client may page through.
events is one entity's own record - the events of its enrollments, each served as the
QuestionnaireResponse the guide publishes for its program stage. False and the record is refused
while identity is still served, which is the posture for a deployment that publishes who its
subjects are and not what was recorded about them.
page_size is what one page carries when the client names no _count, and page_size_limit
is the largest _count honoured: a client asking for more is served the limit rather than
refused, which is what FHIR says a server may do with a _count it will not meet.
tracked_entity_types and search_attributes both mean "the ones the guide publishes" when
empty, which is what keeps this table absent from a project that publishes what it serves.
Naming types restricts search and listing alike to them. Naming attributes defines the search
keys outright - a named attribute is a key whether or not DHIS2 declares it unique or searchable,
because the operator naming it has said it names a subject here.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
SearchBackend
¶
Bases: StrEnum
What answers a register search - the [serve.search] backend key.
Two values ship. "index" is the name reserved for the OpenSearch backend of step 6 in
docs/fhir/design/projection.md, and it is deliberately not a member here for the reason
ServeAuth reserves oauth2 in its own docstring: a value that parses and then has nothing to
run on is worse than a value the file refuses by name, naming the key it refused.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
SearchConfig
¶
Bases: BaseModel
How a register search is answered - the [serve.search] table of fhir.toml.
A register search reaches the instance through one seam, NameSearchIndex, and this table says
which backend sits behind it. backend = "dhis2" is the instance itself: the search a live run
has always run, with the authorization properties it has always had - the matches a search
discloses are identifiers, and the record behind one is read back under the caller's own
credentials, so DHIS2 decides per match per caller what may be seen.
backend = "projection" moves the finding half into the store [serve.projection] names and
leaves the disclosing half exactly where it is. A search then costs one indexed query rather
than one tracker query per key per tracked entity type, it answers a name the exact-match filter
could never answer, and every answer states the cursor it is as of. What it does not do is hand
over a record the instance has not authorized: each match is read back live under the caller's
own credentials, which is the R9 posture of docs/fhir/design/projection.md section 6 in full.
The table has no command-line flag. Which searches this server answers is its contract - it is
what /metadata declares - rather than a property of one invocation, which is the rule
[serve.tracked_entities] follows for the same reason.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ProjectionBackend
¶
Bases: StrEnum
Which durable store holds this project's materialized projection - [serve.projection] store.
"postgres" is the name reserved for the document store of step 7 in
docs/fhir/design/projection.md, and it is deliberately not a member here for the reason
SearchBackend reserves "index": a value that parses and then has nothing to run on is worse
than a value the file refuses by name.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ProjectionConfig
¶
Bases: BaseModel
The materialized projection this project holds - the [serve.projection] table of fhir.toml.
A projection is a durable copy of the mapped scope of a DHIS2 instance, held as the FHIR
resources this project's map publishes, filled by d2w fhir sync and written by nothing else.
It is derived, it is rebuildable from zero, and every answer served out of it states the instant
it is as of. docs/fhir/design/projection.md section 4 is the doctrine in seven rules, and the
first of them is the one this table exists under: DHIS2 stays the record for everything DHIS2
can hold, and this holds a copy.
store is which backend holds it, and "none" - the default - is no projection at all. That is
not a degraded mode: a facade reading the instance per request is the product, it needs no
operator, and nothing in this table may make it harder to run
(docs/fhir/design/projection.md R11).
path is where a SQLite projection's one file lives, relative to the project root unless it is
absolute - the same rule [serve] spool_dir follows, because the two are the same kind of
directory work. Deleting the file is a supported operation: the next d2w fhir sync fills it
from zero, which is what "rebuildable" means when it is not frightening.
overlap_seconds is how far back before its own watermark an incremental run re-reads. A sync
that polled from exactly its watermark would drop the rows written in the instant it was reading,
so it re-reads a window and relies on a write being idempotent by resource id.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ServeAuth
¶
Bases: StrEnum
How the served facade decides who is calling it - the [serve] auth posture.
Four postures ship, and they are a ladder rather than a menu: none serves every caller,
token takes a secret this deployment issued, dhis2 takes the caller's own DHIS2 credentials,
and jwt takes a token an external OpenID Connect issuer minted, validated here against that
issuer's JWKS. Climbing a rung is editing [serve] auth and the table beside it.
oauth2 is the name reserved for an authorization server this facade runs itself, and is
deliberately not a value here: DHIS2 2.43.1's own authorization server 500s for any client the
API creates (BUGS.md 96), so a project could state it and nothing would answer. A posture that
parses and then refuses is worse than one that is not offered, so the reservation lives in this
docstring and in docs/fhir/301-serving.md rather than in the enum. A deployment that wants
tokens from an authorization server today states jwt and names the one it already runs.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ServeJwtConfig
¶
Bases: BaseModel
Which issuer the jwt posture trusts, and what one of its tokens means - the [serve.jwt] table.
issuer is the OpenID Connect issuer identifier, the value its tokens carry as iss. The
facade reads {issuer}/.well-known/openid-configuration once while it starts, takes the
jwks_uri from it, and verifies every token against the keys published there. It is the one
key the posture cannot run without, and a jwt run that names none is refused before the socket
opens rather than at the first caller.
audience is checked only when it is stated. An issuer that mints tokens for several audiences
is minting tokens this facade should not accept on another audience's behalf, so a deployment
sharing an issuer with other services states the value it was registered under. Left out, a
token this issuer signed is a token this facade takes.
username_claim names the claim whose value becomes the request identity - what a receipt
records as its submitter. preferred_username is the OpenID Connect claim for exactly that, and
it is the default; a deployment whose issuer puts the DHIS2 username somewhere else names that
claim instead. A token that carries no such claim is refused: an identity nobody stated is not
one to invent.
forward_bearer is whether a register read carries the caller's token on to DHIS2. It is false
by default and it is the honest default: DHIS2 accepts a foreign issuer's JWT only when it was
configured to trust the same issuer (oidc.jwt.token.authentication.enabled), and a facade that
quietly read the register as its own profile instead would answer every caller with that
profile's rights. So under forward_bearer = false the live register is not answered at all,
and the refusal says what would make it answerable.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
Attributes¶
issuer = None
class-attribute
instance-attribute
¶
The OIDC issuer identifier, or None when this project's table states none.
audience = None
class-attribute
instance-attribute
¶
The aud every accepted token must carry, or None to accept whatever this issuer minted.
username_claim = 'preferred_username'
class-attribute
instance-attribute
¶
The claim whose value names the caller, and becomes the identity a receipt records.
forward_bearer = False
class-attribute
instance-attribute
¶
Whether a register read carries the caller's own token to DHIS2 - see the note above.
ServeAuthScope
¶
Bases: StrEnum
How much of the served surface one [serve] auth posture covers.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ServeConfig
¶
Bases: BaseModel
How d2w fhir serve runs this project - the [serve] table of fhir.toml.
Where a project is served from is a property of the project, not of the invocation: a
developer whose DHIS2 stack already owns 8080 states another port once here and every
make serve and bare d2w fhir serve in that project honours it. A command-line flag still
wins over the table, and the table wins over these defaults.
ui serves the capture UI at / alongside the FHIR routes. A project whose whole workflow is
people filling in forms states it once here and gets the UI from every make serve.
basemaps names the raster tile layers the capture UI's organisation-unit map offers, in the
order it offers them; the first is the one the map opens with. The layer control always carries
a None entry beside them, so drawing the boundaries on a plain canvas is a click rather than a
config change. An empty list is therefore the air-gapped posture in full - the only layer on
offer is None, and the page reaches no origin but this server. It is the one part of this
table that makes the browser talk to anybody else, which is why it is stated rather than
inferred.
capture is whether this server receives submissions at all. True - the default - mounts the
create route, declares create on QuestionnaireResponse in /metadata, and lets the capture
screens submit. False is the viewer posture: the guide is published, read, searched, and
$generated against, and nothing is received. $generate stays because it is a read of a
published form that happens to answer with a draft, and writes nothing here.
THE RECEIPTS STAY READABLE WITH CAPTURE OFF. read and search-type on QuestionnaireResponse
are untouched by this dial, and so is GET /facade/spool. The receipts a project already holds are as
true as they were, and a server that stopped answering for them would make every id it handed
out at capture time expire on the day somebody edited one line of this file. Only create goes.
spool_dir is where the receipt tree lives - received/, forwarded/, rejected/, and the
malformed/ holding pen beside them. A relative path is resolved against the project root; an
absolute one is taken as written. It is a [serve] key because the spool belongs to the serve
surface - this server is what writes it - and d2w fhir forward reads the same key rather than
carrying a spool location of its own, so the process that writes a receipt and the process that
drains it can never disagree about where it is.
[serve.tracked_entities] is the register: whether the instance's tracked entities are served at
all, whether they can be listed rather than only searched for, and how a listing is paged. It is
one of the two parts of this table that say what a live run will tell a client about the instance
behind it.
[serve.data_sets] is the other: whether a live run answers what the instance holds for a data
set, which data sets it answers for, how a page is sized, and how many periods one read may name.
The register says what this facade will tell a client about the instance's subjects, and this
says what it will tell them about its aggregate values.
[serve.search] is what answers a register search - the instance itself, or the materialized
projection beside it. It says how a lookup is answered where [serve.tracked_entities] says
what may be looked up.
[serve.projection] is the materialized projection: which store holds a durable copy of the
mapped scope of the instance, where that store lives, and how far back an incremental sync
re-reads. store = "none" - the default - is no projection at all, which is the posture every
facade started before this table existed runs in and will keep running in.
auth is who the facade serves, and auth_scope is how much of it the posture covers. The key
is ServeAuth | None rather than ServeAuth because absence is a third state this table has to
be able to tell apart: a project that never wrote the key is served on loopback with no
authentication, and refused on any other interface, so that binding the world is a sentence
somebody wrote rather than a default nobody read. auth = "none" written out is that sentence.
[serve.jwt] is what auth = "jwt" runs on: the issuer whose tokens this facade takes, the
audience it checks when one is stated, the claim that names the caller, and whether a register
read carries the caller's token on to DHIS2. It is always present as a table of defaults, so a
jwt run that names no issuer is refused by the key's name rather than by a missing section.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 | |
Attributes¶
auth = None
class-attribute
instance-attribute
¶
The posture, or None when this project's table states none - see the note above on absence.
jwt = Field(default_factory=ServeJwtConfig)
class-attribute
instance-attribute
¶
What the jwt posture runs on - the [serve.jwt] table, defaults when the project writes none.
OverwritePosture
¶
Bases: StrEnum
What a drain does with an aggregate value a forwarded receipt of this spool already sent.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
CorrectionPosture
¶
Bases: StrEnum
Whether this deployment accepts a submission that amends one it already forwarded.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
WithdrawalPosture
¶
Bases: StrEnum
Whether this deployment retracts from DHIS2 what one of its forwarded receipts landed.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
ForwardConfig
¶
Bases: BaseModel
How d2w fhir forward drains this project's spool - the [forward] table of fhir.toml.
live is what a project with no compiled guide forwards through. A drain reads the published
Questionnaires and terminology to translate a receipt against, and a project that has run SUSHI
has them on disk. A project captured through d2w fhir serve --live has never built anything,
so the same documents are built off the instance instead - one full metadata read per drain,
where a compiled guide costs a directory listing.
Left on, that read happens only when there is no compiled guide to read instead; a project that builds its IG never pays it. Turned off, a drain against a project with no compiled guide is refused and says which two commands produce one - which is the posture for a deployment that wants its forwards reading a reviewed, published guide and nothing else.
import is the posture every drain of this project runs in when the command line says nothing.
False - the default - is a dry run: every payload still goes to the real endpoint under that
endpoint's own validate-only mode, so DHIS2 decides the answer while nothing is written and no
receipt moves. True makes the bare d2w fhir forward of this project commit, which is what a
project whose drains are routine states once rather than remembering --import every time. The
field is import_responses in Python because import is a Python keyword; the key in the file
is import, and the file accepts no other spelling of it.
register_completeness is the second write an aggregate response asks for. An aggregate
submission reporting itself completed marks its data set complete for the very tuple its
values landed under, once DHIS2 has taken them. True - the default - honours that; false leaves
the values imported and registers nothing, which is the posture for a deployment where
completeness is somebody else's decision to record.
overwrites is what a drain does with an aggregate value a forwarded receipt of this spool
already sent, arriving in a response that declares no correction. "allow" - the default -
posts it and names it: DHIS2 keeps the newest number for a cell, and the toolkit follows the
platform it writes into rather than inventing a stricter one. "refuse" posts no payload
holding such a value; the response stays in the queue with a record naming every covered value
and the receipt that sent it, which is the posture for a deployment where forwarded data changes
only through a declared correction. The dial reaches aggregate values alone - a tracker event
carries its own DHIS2 identity, so it collides rather than overwriting.
corrections and withdrawals are the other half of the same question, and they govern a
marked submission where overwrites governs an unmarked one. A correction says which receipt
it corrects; an overwrite says nothing and is simply a second capture of the same tuple. Both
default to "off", because a deployment that publishes forms and forwards them is not thereby a
deployment that lets a submitter reach back into what DHIS2 already holds. corrections =
"amend" accepts a submission that names the receipt it amends and lands on that receipt's own
DHIS2 identity; withdrawals = "retract" is what d2w fhir withdraw requires before it deletes
anything. Withdrawal is terminal - DHIS2 burns the UID it deletes, so the withdrawn receipt can
never be forwarded again - which is why the capability is stated rather than assumed.
A flag on the command line wins over all six keys for one run, and either key wins over these
defaults - the same order [serve] states for its own dials.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
IpsConfig
¶
Bases: BaseModel
What this instance says about a person beyond their identifiers - the [ips] tables of fhir.toml.
[ips.identity] nominates the tracked entity attribute carrying a person's name, birth date, and
sex, and maps that sex attribute's values onto R4's administrative-gender codes. DHIS2 holds no
field that means any of those, so the nomination is the instance's own statement or there is
nothing to publish - docs/fhir/design/ips.md section 4 is the argument, and section 9's phase 1
is what the nomination reaches: the register's own Patient.
[ips.sections] nominates which recorded values belong in which section of a patient summary,
and [ips.sections.immunizations] is the one section phase 2 maps. Section 5 is the argument:
DHIS2 marks no data element as an immunisation, so the section content of a summary is stated or
it is absent.
enabled is the dial the whole summary surface hangs off, and it is false by default. A patient
summary is a clinical document about a person, and publishing one is a decision a deployment
makes rather than a default it inherits: d2w fhir serve answers $summary on a register whose
subjects are people only where this key says so, and refuses it by name where it does not
(R7 in section 8, "Gated and additive"). The register, the record, and everything else this
facade serves are untouched either way - a summary is a new read over reads that already exist.
The tables are always present as tables of defaults, for the reason [serve.jwt] is: a project
that nominates nothing is refused by the key's name rather than by a missing section, and its
register answers exactly what it answered before they existed.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
FhirProjectConfig
¶
Bases: BaseModel
The full parsed fhir.toml document.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
FhirProject
¶
Bases: BaseModel
A discovered FHIR IG project: parsed config plus where it lives on disk.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
Attributes¶
project_root
property
¶
Directory containing fhir.toml.
ig_directory
property
¶
The SUSHI IG directory (<project_root>/ig).
fsh_directory
property
¶
The FSH source directory (<project_root>/ig/input/fsh).
resources_directory
property
¶
The predefined-resource directory (<project_root>/ig/input/resources), loaded without a FSH compile.
Functions:¶
basemaps_from_options(values)
¶
Read repeated --basemap values as the layers a run offers, or refuse the ones that say nothing.
Each value is either Name=https://.../{z}/{x}/{y}.png or a bare template, whose host becomes
the layer's name - the honest word for a source this project was handed and knows nothing else
about. The split is on the first = and only when what precedes it is a plain word: a template
carrying ?api_key=... is one url, not a name and a url.
The single value none serves no layers, which is the command line's way of saying what
basemaps = [] says in the table. Naming it beside a real layer is a contradiction rather than
a shorthand, so it is refused instead of guessed at.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
find_project_fhir_config(start=None)
¶
Walk up from start (defaulting to $PWD) looking for fhir.toml.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
load_fhir_config(path)
¶
Parse and validate a fhir.toml file, refusing any key the document does not declare.
Every table declares its full key set, so a misspelled option is a refusal rather than a line that sets nothing: the key is named, placed in its table, and matched against the names that table accepts, and every unknown key in the file is reported in one pass. A refusal about a value rather than a name (a wrong type, a value outside its range) keeps pydantic's own report.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
write_fhir_config(path, config)
¶
Write a fhir.toml with default permissions - it is committed project config, not a credential store.
Written under the keys the file declares rather than the Python field names, so what is written
is what load_fhir_config reads back: [forward] import is import_responses in Python only
because the file's own name for it is a keyword there.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
load_project(start=None)
¶
Discover and load the nearest FHIR project, raising NoFhirProjectError when there is none.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/config.py
The patient summary¶
The [ips] tables and the document they reach. dhis2w_fhir.ips holds the two
nominations an instance makes about a person - which tracked entity attribute carries
their name, birth date, and sex, and which recorded values are doses - because DHIS2
states neither and a guess would be indistinguishable from a fact.
dhis2w_fhir.summary assembles those into the IPS document Bundle, as a pure function
of what a caller already read, and states in the document itself what it is and is not.
The section mapping is published beside the vocabularies it maps, as D2Section_CM, so a
consumer audits the assignment without ever holding the project's fhir.toml -
Terminology and ConceptMaps
covers the shape, and build_section_concept_map and
build_section_concept_map_artifacts are the builders behind it.
ips
¶
The [ips] tables: which attribute carries a person's identity, and which stage data is a dose.
docs/fhir/design/ips.md sections 4 and 5 are the argument in full, and they are one argument made
twice. DHIS2 has no name field, no sex field, and no date-of-birth field, and it marks no data
element as an immunisation, a problem, or an allergy. Which of an instance's tracked entity
attributes mean those demographic things, and which of its data elements belong in which section of
a patient summary, are decisions every instance makes for itself, usually differently. So both are
nominated or both are nothing, and this module holds the two nominations and the reading of them.
[ips.identity] fills Patient.name, Patient.birthDate, and Patient.gender on the register
projection, which is what section 9's phase 1 asked of it - before any summary document exists.
[ips.sections] says which recorded values a summary's clinical sections carry, and phase 2 maps
exactly one section: Immunizations. A section with no table is a section this project maps
nothing into, and the document states that rather than inventing content for it
(dhis2w_fhir.summary).
Only UIDs, never names. Attribute names are not unique in DHIS2 and change without notice, and
the guide already publishes the names it reads off the instance as D2TEA_CS.
Honest failure per person, not per instance (section 4, "Honest failure per person"). An
instance-wide nomination is a statement about the attribute, not a promise about every row: a person
the instance holds no birth date for keeps the required element and states its absence on the
data-absent-reason extension, which is the IG's own worked example. A person whose birth date is a
string this server cannot read as a date states the same absence under error rather than
unknown, because "nobody recorded one" and "what was recorded is not a date" are different
answers and a summary that flattened them would be less true than the instance is.
name and gender state no absence, because neither is a required element on the resource the
register serves: a HumanName carrying nothing but a data-absent extension satisfies no reader and
no invariant, and Patient.gender is 0..1. What the instance holds is never lost either way - the
raw attribute value rides the D2TrackedEntityAttributeValue extension exactly as it always has, so
a nomination adds a reading of a value and removes nothing.
Classes¶
IdentityNominations
¶
Bases: BaseModel
Which tracked entity attribute carries which demographic fact - the [ips.identity] table.
Three attributes are nominated by UID and a fourth key states what a sex value means.
administrative_gender maps the value DHIS2 stores against the sex attribute - the option's
DHIS2 code on an option-set-bound attribute - onto one of R4's four administrative-gender
codes. It is a map rather than a rename because the binding on Patient.gender is required, and
it is the smallest possible instance of the clinical-vocabulary source docs/fhir/design/ips.md
section 3 says does not exist yet: four codes rather than forty thousand.
name publishes as Patient.name[0].text and nothing else. A single text name satisfies the IPS
invariant ips-pat-1, which asks for family, given, or text, and an instance whose
given and family names sit in two attributes states the one it wants read rather than having this
project guess which half is which.
An empty table nominates nothing, which is what every project that never wrote it states: the register serves the identity it always served, which is none.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
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 | |
Attributes¶
name = None
class-attribute
instance-attribute
¶
The tracked entity attribute whose value is published as Patient.name[0].text.
birth_date = None
class-attribute
instance-attribute
¶
The tracked entity attribute whose value is published as Patient.birthDate.
sex = None
class-attribute
instance-attribute
¶
The tracked entity attribute whose value administrative_gender reads as Patient.gender.
administrative_gender = Field(default_factory=dict)
class-attribute
instance-attribute
¶
One DHIS2 value of the sex attribute per key, mapped onto male, female, other, or unknown.
Methods:¶
nominates_anything()
¶
Whether this table nominates a single attribute, which is what every caller asks first.
nominated_attribute_uids()
¶
Every attribute this table nominates, once each, in the order the keys are declared.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
ImmunizationsMapping
¶
Bases: BaseModel
Which recorded values are doses - the [ips.sections.immunizations] table.
docs/fhir/design/ips.md section 6 puts the Immunizations row at WITH A MAPPING and says what
the mapping has to state: dose events in an immunisation program stage, with the event's own date
as the occurrence. This table states exactly that, in two lists.
program_stages names the stages whose events record doses. dose_data_elements names the data
elements inside them that each record a dose of one vaccine - which is the shape a DHIS2
immunisation form actually has: MCH BCG dose, MCH Measles dose, MCH Penta dose, one element
per vaccine, the value saying that a dose was given or which dose of the series it was. So the
data element is the vaccine and the value is the dose, and Immunization.vaccineCode carries
the data element's own DHIS2 coding. The IPS binds vaccineCode preferably rather than
requiredly, so publishing a DHIS2 coding there violates no profile - which is what lets this
section carry real doses while an international vaccine vocabulary is still missing (section 5).
Both lists are required together. A stage with no data element nominated records nothing this
reads, and a data element with no stage names values on events nobody said were doses; either
alone maps nothing, which is the failure mode [ips.identity] refuses the same way.
An empty table maps nothing, which is what every project that never wrote it states.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
Attributes¶
program_stages = ()
class-attribute
instance-attribute
¶
The program stages whose events carry doses, by UID.
dose_data_elements = ()
class-attribute
instance-attribute
¶
The data elements inside those stages that each record a dose of one vaccine, by UID.
Methods:¶
maps_anything()
¶
Whether this table maps a single dose, which is what every caller asks first.
records_dose(program_stage_uid, data_element_uid)
¶
Whether one value of one stage's event is a dose this table nominated.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
SectionMappings
¶
Bases: BaseModel
Which recorded values belong in which section of a summary - the [ips.sections] tables.
One sub-table per IPS section this project maps, and immunizations is the only one phase 2
ships: docs/fhir/design/ips.md section 9 says why it goes first, and section 6 says why every
other section needs its own nomination rather than a general rule. A section named here that this
version does not map is refused by name, so a project writing [ips.sections.problems] today is
told the key is not one rather than left with a table that quietly does nothing.
An absent table maps no section at all. The summary is still served - the owner's call, and
dhis2w_fhir.summary states the caveat that goes with it - with its three required sections
carrying an empty reason and its recommended sections omitted.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
ServedIdentity
¶
Bases: BaseModel
The demographic elements one nomination fills on one person, and the absences it states.
The field names are the FHIR element names on purpose: a caller carries this straight onto the resource it is serving, so what this decides and what a client reads cannot drift apart.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
NominatedValueTypeIssue
¶
Bases: BaseModel
One nominated attribute whose published DHIS2 value type is not one the FHIR element accepts.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
Methods:¶
message()
¶
The refusal a run states, naming the key, the attribute, and the value type it found.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
Functions:¶
nominated_value_type_issues(nominations, value_types)
¶
Check every nomination against the value type D2TEA_CS publishes for it, in key order.
docs/fhir/design/ips.md section 4, "Value-shape validation": a nomination whose value type
cannot fill the element it was nominated for refuses the run, in the manner
[generate.tracked_entity_types] refuses a resource type that is not one.
An attribute the guide publishes nothing about raises no issue. The guide's silence is not
evidence of a wrong type - it means the attribute is outside this project's selection - and
[serve.tracked_entities] search_attributes already settles that an operator's nomination of an
attribute outstates what the vocabulary happens to carry about it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
served_identity(values, nominations)
¶
Read one person's nominated attribute values as the demographic elements they were nominated for.
values is that person's attribute values keyed by attribute UID. A nomination the person holds
no value for, and a value this server cannot read, are both per-person facts rather than
instance-wide ones - see this module's docstring for which of them states what.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/ips.py
summary
¶
One person's summary, assembled: the IPS document this toolchain can honestly build about them.
WHAT AN IPS IS, AND WHAT THIS BUILDS. One FHIR document: a Bundle whose first entry is a
Composition and whose remaining entries are the resources that Composition's sections point at.
Bundle-uv-ips fixes type to document, requires identifier and timestamp, and carries the
invariant bdl-ips-1 - an IPS document has no Composition besides the first.
Composition-uv-ips pins type to the LOINC pattern 60591-5, constrains subject to one
Patient, and sets section to 3..* with title, code, and text required on every section
present. This module builds exactly that, and docs/fhir/design/ips.md is the argument behind
every choice in it.
IT IS A LIBRARY AND NOT A ROUTE. Everything below is a pure function of what somebody already
read: the person as the register projects them, and the doses as the record surface projected them.
dhis2w_fhir_serve.routes.summary is one caller and d2w fhir is free to be another - nothing here
opens a connection, reads a store, or knows what a request is.
THREE REQUIRED SECTIONS, AND WHAT THEY SAY WHEN NOTHING IS MAPPED. Problems, Allergies and
Intolerances, and Medication Summary are the sections the IPS puts a SHALL:populate obligation on
the Creator actor for, and this project maps none of them (docs/fhir/design/ips.md section 6:
Allergies is HONESTLY EMPTY, the other two are WITH A MAPPING nobody has written yet). So each
carries Composition.section.emptyReason, which is what the invariant ips-comp-1 accepts in place
of an entry, with the unavailable code out of R4's own list-empty-reason. Nothing is invented to
fill them: an unmapped stage does not become a free-text Observation and an unmapped element does not
get swept into Results on the grounds that it was numeric.
THE ONE MAPPED SECTION IS IMMUNIZATIONS. [ips.sections.immunizations] says which program
stages record doses and which of their data elements each record a dose of one vaccine, and every
entry here traces to a line somebody wrote in that table. A project that maps it and a person who
has no dose recorded are different facts and read differently: the section is present with an empty
reason for the person, and absent entirely for the project.
THE CAVEAT IS PART OF THE DOCUMENT. The IG says in as many words that a system that can never
populate the three obligated sections "can produce valid IPS Bundle instances, although it cannot
comply with the Creator (IPS) actor obligations" - two claims, and this toolchain has to be able to
make each separately (R5). So the document says so itself, in Composition.text: R4's own
human-readable rendering of a resource, which every reader of a document sees and which survives
being saved to a file. summary_caveat is the same sentence as text, for a caller that states it
beside the response as well - the two-place idiom dhis2w_fhir_serve.projection.serving argues for
the projection's as-of instant, and for the same reason: one fact, stated where resources are read
and stated again where responses are.
DETERMINISTIC (R4 in section 8). Two assemblies of an unchanged record differ in
Bundle.timestamp and Composition.date and in nothing else: the sections are in a fixed order,
the doses are in the record's own order, and every id is derived from a DHIS2 identifier through
uuid5 rather than minted per call.
NO PROFILE IS CLAIMED. meta.profile names none of the IPS StructureDefinitions, because live
serve mode publishes no StructureDefinitions and resolves none (R8): the IG is a vocabulary this
document conforms to, not a dependency the generated country guide takes on.
Classes¶
IpsSection
¶
Bases: BaseModel
One section of the summary: its LOINC code, its title, and the display that code carries.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
RecordedDose
¶
Bases: BaseModel
One dose one recorded event holds: which vaccine, when, and which dose of the series.
Read off the record the facade already serves rather than off DHIS2 a second way: the caller
projects one tracked entity's events through the very machinery
GET /facade/tracked-entities/{uid}/events answers with, and hands the doses in it here. display is
the data element's name as the guide publishes it, and dose_number is the value DHIS2 stored
where that value names a dose - Dose 2, IPT 1 - and nothing where the value is a plain
statement that the dose was given.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
AssembledSummary
¶
Bases: BaseModel
One assembled summary: the document, and the honest statement that goes beside it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
Attributes¶
caveat
instance-attribute
¶
What this document is and is not, in one sentence - the same words Composition.text carries.
creator_conformant = False
class-attribute
instance-attribute
¶
Whether the document claims the Creator (IPS) actor's obligations, which today it never does.
The three obligated sections are unmapped in every project this toolchain serves, so the answer is false and the caveat says so. It is a field rather than a constant because R5 asks this project to state validity and conformance as two separate claims, and a caller reading one should not have to infer the other from a sentence.
Functions:¶
summary_caveat(*, mapped_sections, dose_count)
¶
The one sentence a summary states about itself, in the document and beside the response.
Two situations and two sentences, because they are different facts and a reader has to be able to tell them apart: a summary with a mapped section carrying real content, and a summary whose clinical sections are all empty. Both are valid IPS documents and neither is conformance with the Creator (IPS) actor, which is what the closing clause states in both.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
build_patient_summary(subject, doses, *, vaccine_code_system, assembled_at, author_display, immunizations_mapped=False, unpublished_stage_uids=())
¶
Assemble one person's summary out of what the register and the record already say about them.
subject is the very resource GET /Patient/{uid} answers with, carried into the document
unchanged, so the person a summary is about and the person the register serves can never
disagree. doses is what the record projected; vaccine_code_system is the DHIS2 data-element
identifier namespace their vaccine codes are stated under - the same namespace the published
section map maps out of, so the document and the map name one thing one way.
immunizations_mapped is the project's own word rather than something inferred from doses
being empty: a mapped section with no dose is a statement about this person, and an unmapped one
is a statement about the project, and the summary has to make them differently.
unpublished_stage_uids names the mapped stages this guide publishes no form for, which the
section states in its own narrative. A guide narrower than its mapping and a person who was
never vaccinated produce the same count of doses and are not the same fact.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/summary.py
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 | |
DHIS2 attribute values¶
The projection every generated resource carries its DHIS2 attribute values on, plus
the uid -> code index a generate run resolves once against /api/attributes and
every emitter joins against. DHIS2 sends an attribute value as an attribute UID and a
string, so the code is a lookup rather than part of the value.
attributes
¶
DHIS2 attribute values shared by every component: the projection plus the emit-time code index.
Classes¶
AttributeValueIn
¶
Bases: BaseModel
One DHIS2 attribute value: the attribute it belongs to, and the value the instance holds.
DHIS2 sends {"attribute": {"id": "ihn1wb9eho8"}, "value": "KE03"} and nothing else - no
code, no name, no value type - so the projection carries the UID and the value alone and
the attribute's code is joined from AttributeCodeIndex at emit time. The value is a
string whatever the attribute's declared DHIS2 value type; a GeoJSON-valued attribute
arrives as the serialised document.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
AttributeCodeIndex
¶
Bases: BaseModel
What one generate run knows about the instance's attributes: their codes, and which are unique.
An attribute DHIS2 left without a code is absent from the mapping rather than present with
an empty one: most instances code few of their attributes, and some code none of them, so
every consumer reads through code_for and decides what a missing code means for it.
unique_uids holds the attributes DHIS2 declares unique. A unique attribute value is a
business identifier - a national registry number on a facility, a payer code on a data set -
so it is emitted as an Identifier on the resource rather than as an annotation extension,
and every consumer decides which of the two it writes by asking is_unique.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/attributes.py
Generate notes¶
What a generate target has to say about a run, as a model rather than a sentence. A
GenerateNote carries the kind of decision it records (GenerateNoteCategory) beside
the human text, and echoes_validate says whether the kind only restates a finding
d2w fhir validate reports on the instance - which is
what lets a bare run count those apart from what generation itself found.
notes
¶
Human-facing note formatting shared by every emitter and the service layer.
Classes¶
GenerateNoteCategory
¶
Bases: StrEnum
The kind of decision one generate note records, which is what makes a note reasonable about.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
GenerateNote
¶
Bases: BaseModel
One note a generate target raised: the human text, plus the kind of decision it records.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
Functions:¶
pluralize(count, noun)
¶
Render a count with its noun, singular at exactly one (1 error, 0 errors).
verb_for_count(count, singular, plural)
¶
The verb form a count takes, so a counted sentence agrees with itself at one and at many.
aggregate_note(message, subjects, sample_size=5)
¶
One loud note for many subjects: message, a capped sample, and the remainder count.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
generate_note(category, message)
¶
Wrap one already-written message in the note kind it belongs to.
aggregate_generate_note(category, message, subjects, sample_size=5)
¶
One aggregate note for many subjects, carried as the note kind it belongs to.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/notes.py
FHIR R4 resource schemas¶
dhis2w_fhir.r4 is the capture-facing surface over the R4 resource models, which
dhis2w_fhir_engine owns and defines at
dhis2w_fhir_engine.r4.resources. The
engine is the FHIR foundation package, so one Patient and one Bundle serve the
generator, the evaluator, and the server alike; this module re-exports that family
under the path capture code imports it from, and every name below is the engine's own
class object rather than a copy of it. import dhis2w_fhir.r4 keeps working exactly as
written, and a model built through it is an instance of the engine's class.
That family covers the models every pre-built JSON document is serialised from -
Organization and Location for the registry, CodeSystem and ValueSet for the
option-set and category terminology, ConceptMap for both families' mappings back to
DHIS2. Beside them are the resources a summary document is assembled out of -
Composition and its flat CompositionSection, Patient, Condition,
AllergyIntolerance, and Observation, with Bundle carrying the identifier
and timestamp a document requires; see
examples/fhir/client/ips_document.py
and the IPS working paper.
Every one is frozen, alias-aware, and closed to unknown keys, so
Model.model_validate(payload).model_dump_json(exclude_none=True, by_alias=True)
reproduces the input document key for key.
The R4 primitive checks under dhis2w_fhir.r4.primitives stay this package's own, and
arrive under the same dhis2w_fhir.r4 name.
schemas
¶
The capture-facing surface over the FHIR R4 resource models, which dhis2w_fhir_engine owns.
dhis2w_fhir_engine.r4.resources is where the models are defined: the engine is the FHIR foundation
package, so one definition of Patient, Bundle, or QuestionnaireResponse serves the evaluator,
the generator, and the server alike. This module re-exports that family under dhis2w_fhir.r4, which
is the import path capture code reads them from - one import surface per package, so a caller working
on DHIS2 capture never has to know which package below it declared the class.
Nothing is redefined here and nothing is renamed: every name below is the engine's own class object.
Classes¶
AllergyIntolerance
¶
Bases: DomainResource
A FHIR R4 AllergyIntolerance - one allergy a summary names, or an assertion that none is known.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Attachment
¶
Bases: Element
Attached content - the base64 GeoJSON boundary a Location extension carries.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
BackboneElement
¶
Bases: Element
BackboneElement - an element defined inside a resource rather than as a reusable datatype.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Bundle
¶
Bases: Resource
A FHIR R4 Bundle - the container a search answers with, and the one a document is.
identifier and timestamp are optional on the base resource and required on a document: a
document bundle states which document it is and the instant it was assembled.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
BundleEntry
¶
Bases: BackboneElement
Bundle.entry - one resource in a bundle, at the URL it is served from.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
BundleEntrySearch
¶
Bases: BackboneElement
Bundle.entry.search - why an entry is in a search result set.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
BundleLink
¶
Bases: BackboneElement
Bundle.link - one relation of a search result set, such as self.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatement
¶
Bases: DomainResource
A FHIR R4 CapabilityStatement - what a DHIS2 capture server accepts and serves.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementImplementation
¶
Bases: BackboneElement
CapabilityStatement.implementation - the specific installation the statement describes.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementInteraction
¶
Bases: BackboneElement
CapabilityStatement.rest.resource.interaction - one RESTful interaction supported on a resource type.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementOperation
¶
Bases: BackboneElement
CapabilityStatement.rest.operation and .rest.resource.operation - one operation the endpoint answers.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementResource
¶
Bases: BackboneElement
CapabilityStatement.rest.resource - one resource type the endpoint serves, and how.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementRest
¶
Bases: BackboneElement
CapabilityStatement.rest - the RESTful behaviour of one end of the conversation.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementSearchParam
¶
Bases: BackboneElement
CapabilityStatement.rest.resource.searchParam - one search parameter supported on a resource type.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementSecurity
¶
Bases: BackboneElement
CapabilityStatement.rest.security - how the endpoint decides who is calling it.
service draws on R4's restful-security-service value set, which is extensible: a scheme the
value set has no code for is stated as CodeableConcept.text, which is what the binding is for.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CapabilityStatementSoftware
¶
Bases: BackboneElement
CapabilityStatement.software - the software the described endpoint runs.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeableConcept
¶
Bases: Element
A concept expressed as one or more codings, optionally with free text.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeSystem
¶
Bases: DomainResource
A FHIR R4 CodeSystem as generated from one DHIS2 option set, one concept per option.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeSystemConcept
¶
Bases: BackboneElement
CodeSystem.concept - one DHIS2 option, keyed by its option UID.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeSystemConceptDesignation
¶
Bases: BackboneElement
CodeSystem.concept.designation - the translation of a concept display into one locale.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeSystemConceptProperty
¶
Bases: BackboneElement
CodeSystem.concept.property - one declared property carried by a single concept.
The value[x] choices are the ones the generated code systems declare a property type for:
#string and #code on the DHIS2 code, domain, value type, and parent properties,
#boolean on the uniqueness flag, #integer on the organisation-unit hierarchy level, and
Coding on the category axes a category option combo concept decomposes over.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CodeSystemProperty
¶
Bases: BackboneElement
CodeSystem.property - the declaration of a property the concepts in the code system may carry.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Coding
¶
Bases: Element
One code drawn from a code system, with the display the system gives it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Composition
¶
Bases: DomainResource
A FHIR R4 Composition - the first entry of a document bundle, and the index of everything in it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
CompositionSection
¶
Bases: BackboneElement
Composition.section - one section of a document: what it is, what is in it, or why nothing is.
entry names the resources the section carries and emptyReason says why it carries none, and a
section states one or the other. No nested section: the International Patient Summary pins
section.section to 0..0, so the section model a summary uses is flat.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ConceptMap
¶
Bases: DomainResource
A FHIR R4 ConceptMap taking one option set's concept codes back to the DHIS2 identifiers they stand for.
identifier is a single Identifier rather than a list: R4 gives ConceptMap 0..1 where it
gives CodeSystem and ValueSet 0..*.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ConceptMapGroup
¶
Bases: BackboneElement
ConceptMap.group - the mappings from one source system into one target system.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ConceptMapGroupElement
¶
Bases: BackboneElement
ConceptMap.group.element - one source concept and every target it maps onto.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ConceptMapGroupElementTarget
¶
Bases: BackboneElement
ConceptMap.group.element.target - the DHIS2 identifier one concept maps onto, and how closely.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Condition
¶
Bases: DomainResource
A FHIR R4 Condition - one problem a summary's Problems section names, or an assertion of none.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ContactPoint
¶
Bases: Element
A telecom contact point - the phone number or email address of an organisation unit.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
DomainResource
¶
Bases: Resource
DomainResource - a resource carrying narrative and extensions; every resource emitted here is one.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Element
¶
Bases: FhirBase
Element - the R4 root for datatypes, and the _x sibling a primitive's extensions hang from.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Extension
¶
Bases: Element
One extension: either a nested set of extensions or a single value[x] choice.
Only the choices this package emits or reads are modelled. valueDecimal is typed as
int | float rather than float so a whole number survives the round trip: float
would coerce the wire value 2896 to 2896.0 and change the document.
valueString_element carries the _valueString sibling the way name_element carries
_name: a DHIS2 string an extension holds - a date label, a description - is translated in
the instance, and its translations ride the standard R4 translation extension on the primitive.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
FhirBase
¶
Bases: BaseModel
Pydantic carrier for every schema here - frozen, alias-aware, closed to unknown keys. Not a FHIR type.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
HumanName
¶
Bases: Element
A person's name; the generated contacts and a nominated DHIS2 attribute both carry it in text.
text alone satisfies the IPS invariant ips-pat-1, which asks for family, given, or
text. family and given are here for a name a document was handed already split; nothing in
this project splits one, because which half of a person's name an attribute holds is a fact
DHIS2 does not state.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Identifier
¶
Bases: Element
A business identifier: the DHIS2 UID or code under its identifier system.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Immunization
¶
Bases: DomainResource
A FHIR R4 Immunization - one dose a summary's Immunizations section names.
The four elements the International Patient Summary asks a Creator to populate where they are
known are status, vaccineCode, patient, and occurrence[x], and each of them is a fact
DHIS2 states about a recorded dose. vaccineCode's binding in the IPS is preferred rather
than required, so a dose recorded against a DHIS2 data element is published with the DHIS2
coding and violates no profile - which is what lets this section carry real content while an
international vaccine vocabulary is still missing.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ImmunizationProtocolApplied
¶
Bases: BackboneElement
Immunization.protocolApplied - which dose of a series one administration was.
doseNumberString rather than doseNumberPositiveInt because a DHIS2 option code naming a dose
is a code and not always a number: Dose 0 and IPT 1 are both dose numbers an instance states,
and reading either as an integer would either fail or invent one.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
JsonResource
¶
Bases: FhirBase
The one open model here: a wire document carried verbatim, keyed only by its resourceType.
A Bundle entry, a compiled-store body, and an operation parameter carrying a whole resource hold
whatever resource the document happens to be, so modelling their contents would mean naming every
resource type in advance. This is the typed wrapper the house style asks for over a genuinely
dynamic wire shape: extra="allow" keeps every key the document carried, and resourceType is
the one fact that is always there.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ListEntry
¶
Bases: BackboneElement
List.entry - one resource the list names, as a reference.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Location
¶
Bases: DomainResource
A FHIR R4 Location as generated from the physical place of one DHIS2 organisation unit.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
LocationPosition
¶
Bases: BackboneElement
Location.position - the WGS84 point of an organisation unit.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Meta
¶
Bases: Element
Resource.meta - the profiles a generated instance claims conformance to, and the tags classifying it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Narrative
¶
Bases: Element
DomainResource.text - the human-readable XHTML rendering of a resource.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Observation
¶
Bases: DomainResource
A FHIR R4 Observation - one recorded value, coded as whatever code system stated the question.
value[x] here is the string and the concept: a DHIS2 data value arrives as the string DHIS2
sent, and a Quantity would need a unit and a unit system nobody has stated for a DHIS2 data
element. dataAbsentReason is the element R4 gives an observation made with no value.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
OperationOutcome
¶
Bases: DomainResource
A FHIR R4 OperationOutcome - the error body every failed interaction answers with.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
OperationOutcomeIssue
¶
Bases: BackboneElement
OperationOutcome.issue - one thing that went wrong, at the severity and issue type R4 names for it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Organization
¶
Bases: DomainResource
A FHIR R4 Organization as generated from one DHIS2 organisation unit.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
OrganizationContact
¶
Bases: BackboneElement
Organization.contact - a contact party for the organisation unit.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Parameters
¶
Bases: Resource
A FHIR R4 Parameters - the body an operation answers with; a Resource, so it carries no narrative.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ParametersParameter
¶
Bases: BackboneElement
Parameters.parameter - one named input or output of an operation, valued or nested in part.
resource is how a parameter carries a whole resource rather than a datatype - an operation
handed a document to work over, and an operation answering an OperationOutcome about one of its
own parts. It is JsonResource for the reason BundleEntry.resource is: which resource type it
holds is the caller's to decide, one request at a time.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Patient
¶
Bases: DomainResource
A FHIR R4 Patient - the person a summary document is about, with the demographics somebody stated.
RegisteredEntity is the register's projection of a tracked entity and carries identity alone;
this is the fuller resource a document assembles, so it names the elements a nomination fills.
birthDate_element carries the _birthDate sibling the way name_element carries _name: a
person the instance holds no birth date for keeps the required element and states its absence on
the data-absent-reason extension, which is what DATA_ABSENT_REASON_EXTENSION_URL is for.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Period
¶
Bases: Element
A time range with inclusive bounds - the reporting period a captured data value set covers.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Questionnaire
¶
Bases: DomainResource
A FHIR R4 Questionnaire as generated from one DHIS2 data set, event program, or program stage.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
QuestionnaireItem
¶
Bases: BackboneElement
Questionnaire.item - one question, or a group nesting the questions of a section or a disaggregation.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Attributes¶
readOnly = None
class-attribute
instance-attribute
¶
True when DHIS2 owns the value - a generated tracked entity attribute, minted by the instance on import.
enableWhen = None
class-attribute
instance-attribute
¶
The conditions under which the form asks this item; absent on an item the form always asks.
enableBehavior = None
class-attribute
instance-attribute
¶
How several conditions combine. R4 requires it past one condition and admits it at one.
QuestionnaireItemEnableWhen
¶
Bases: BackboneElement
Questionnaire.item.enableWhen - one condition under which the form asks the item carrying it.
The answer[x] choices are the ones a DHIS2 program rule condition compiles into: a coded
answer, a tick, a number, and the three temporal primitives. operator is the R4 code, and
exists states its sense on answerBoolean rather than on any of the others.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
QuestionnaireResponse
¶
Bases: DomainResource
A FHIR R4 QuestionnaireResponse - one captured DHIS2 data value set, event, or tracker event.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
QuestionnaireResponseAnswer
¶
Bases: BackboneElement
QuestionnaireResponse.item.answer - one captured value on the value[x] element its type asks for.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
QuestionnaireResponseItem
¶
Bases: BackboneElement
QuestionnaireResponse.item - one answered question, or a group mirroring the questionnaire's tree.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Reference
¶
Bases: Element
A reference to another resource - a literal Organization/mOsABqg3Cqw, or a business identifier.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
RegisteredEntity
¶
Bases: DomainResource
One DHIS2 tracked entity as the FHIR resource its type is published as - identity only, no domain claims.
resourceType is a plain string rather than a literal because the resource a tracked entity is
served as is what the published D2TET_CM maps its type onto: a Patient for the people, a
Specimen for the samples, whatever a project states. The elements are the four every R4 resource
in that map carries and DHIS2 states without interpretation: identifier for the tracked entity
UID and the values of the attributes DHIS2 declares unique, meta.tag for the tracked entity
type, and extension for every other attribute value the entity holds.
The three demographic elements below are filled from a nomination and from nothing else. DHIS2
has no name field, no sex field, and no date-of-birth field, so which attribute means which of
them is stated in [ips.identity] per instance or it is not stated at all; a project that
nominates nothing serves the four elements above and no others. They sit at the end of the model
rather than in R4's own element order so that a served resource is byte-identical to what this
register answered before the nomination existed. birthDate_element carries the _birthDate
sibling the way Patient does, because a nominated birth date the instance holds no readable
value for states its absence there - see dhis2w_fhir.ips and
dhis2w_fhir_serve.register.projection.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Resource
¶
Bases: FhirBase
Resource - the R4 root for resources, a sibling of Element rather than a subtype of it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ResourceList
¶
Bases: DomainResource
A FHIR R4 List, which is what carries a DHIS2 organisation-unit assignment.
R4 binds Group.member.entity to Patient, Practitioner, PractitionerRole, Device,
Medication, Substance, and Group, so a Location cannot be a Group member; List.entry.item
is Reference(Resource) and takes one. Named ResourceList because List is a built-in.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ValueSet
¶
Bases: DomainResource
A FHIR R4 ValueSet as generated from one DHIS2 option set, composing the whole matching CodeSystem.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ValueSetCompose
¶
Bases: BackboneElement
ValueSet.compose - the content logic that builds the expansion of the value set.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
ValueSetInclude
¶
Bases: BackboneElement
ValueSet.compose.include - one code system the value set draws its codes from.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Functions:¶
json_resource(resource)
¶
Carry a typed resource as a JsonResource, exactly as the emitter would have written it.
Source code in packages/dhis2w-fhir-engine/src/dhis2w_fhir_engine/r4/resources.py
Conversion: QuestionnaireResponse to DHIS2¶
The inverse of the emitters, and the reference implementation
docs/fhir/design/conversion.md holds the later published
StructureMaps against. A caller assembles a ConversionContext once from the compiled IG
artifacts - the served Questionnaires, the option-set CodeSystems and their ConceptMaps, the
ValueSets binding the two, and the published Locations - and then translates each captured
response into the DHIS2 import payload its form kind reports: a DataValueSet for a data set,
a TrackerEvent for an event program or a tracker program stage. A response the translator
cannot read whole answers with typed refusals naming the link id and the reason, never with a
partial payload.
A result carries exactly one payload and its target_kind names which, so ConversionResult.payload
reads the document off the kind and payload_of narrows it to one wire shape. The batch form is
ConversionReport.payloads_of, whose four named properties - data_value_sets, events,
tracked_entities, enrollments - are the order a drain posts them in.
conversion.artifacts is where a project's own files become those models: it reads the
compiled ig/fsh-generated/resources merged with the predefined ig/input/resources tree -
the same two trees d2w fhir serve serves - and build_project_context assembles the context
from them plus the project's [generate] naming, identifier base, and timezone.
Every public name below is importable from dhis2w_fhir itself, which is the package's one
stable import surface; the dhis2w_fhir.conversion path answers the same objects.
schemas
¶
Conversion schemas: the translation context, the outcome taxonomy, and the typed DHIS2 payloads.
Every type the QR -> DHIS2 translator reads or writes lives here. The context side
(ConversionNaming, OptionTable, QuestionSpec, FormSpec, ConversionContext) is what a
caller assembles once from the compiled IG artifacts; the outcome side (ConversionNote,
ConversionRefusal, ConversionResult, ConversionReport) is what one translated response
answers with.
The DHIS2 payloads themselves are the generated OpenAPI models: DataValueSet / DataValue for
the aggregate envelope, TrackerEvent / TrackerDataValue for both event kinds, and
TrackerTrackedEntity / TrackerEnrollment / TrackerAttribute for the registration one, and
TrackerEnrollment on its own for the registration that enrols a person the instance already holds.
Nothing is hand-rolled - those schemas carry every field the import endpoints read, and every wire
value they carry is a string, so a lexical decimal and a DHIS2 option code survive untouched.
Attributes¶
CONCEPT_CODE_TIER = 'concept-code'
module-attribute
¶
The spelling the contract asks for: the concept code the served CodeSystem publishes.
OPTION_UID_TIER = 'option-uid'
module-attribute
¶
The first lenient fall-back: the DHIS2 UID of the option, sent against a code-mode CodeSystem.
OPTION_CODE_TIER = 'option-code'
module-attribute
¶
The second lenient fall-back: the DHIS2 code of the option, sent against an id-mode CodeSystem.
Classes¶
ConversionTargetKind
¶
Bases: StrEnum
Which DHIS2 import payload one translated response becomes.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
CodedAnswerMode
¶
Bases: StrEnum
How exactly a coded answer has to name its concept before the translator accepts it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
WireValueKind
¶
Bases: StrEnum
How one question's answers are serialised onto the DHIS2 wire.
Derived once, at context build, from the question's R4 item type, whether it repeats, whether it binds terminology, and - when the caller supplied one - the DHIS2 value type behind it. Every serialisation branch dispatches on this and on nothing else.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionNoteCategory
¶
Bases: StrEnum
The kind of interpretation one conversion note records, which is what makes a note reasonable about.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionRefusalCategory
¶
Bases: StrEnum
The kind of refusal one untranslatable response raised, named so a caller can route it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
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 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 | |
ConversionNote
¶
Bases: BaseModel
One thing the translator had to interpret, recorded rather than left silent.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionRefusal
¶
Bases: BaseModel
One reason a response was not translated, naming the element it stumbled on.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionNaming
¶
Bases: BaseModel
The extension urls and identifier systems one project's responses and forms are written in.
Every name is derived from fhir.toml - the IG canonical for the extensions the response
profiles pin, and [generate] identifier_system_base for the DHIS2 identifier systems a
response names its tracked entity, its enrollment, its organisation unit, and its form under.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
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 | |
Attributes¶
enrolled_at_url
instance-attribute
¶
Canonical of the extension a registration response dates the enrollment it mints from.
incident_at_url
instance-attribute
¶
Canonical of the extension a registration response dates the incident that enrollment follows.
entity_level_url
instance-attribute
¶
Canonical of the item extension a registration question states which DHIS2 level it is imported at on.
subject_exists_url
instance-attribute
¶
Canonical of the extension a registration response states that its subject is already held on.
program_rule_url
instance-attribute
¶
Canonical of the extension a form publishes the DHIS2 program rules it does not itself express on.
attribute_option_combos_url
instance-attribute
¶
Canonical of the Questionnaire extension a form declares its attribute-option-combo ValueSet on.
attribute_option_combo_url
instance-attribute
¶
Canonical of the QuestionnaireResponse extension one response names its attribute option combo on.
tracked_entity_type_system
instance-attribute
¶
The DHIS2 identifier system a registration form names the type it enrols a person as under.
option_code_system
instance-attribute
¶
The DHIS2 identifier namespace an option-set ConceptMap carries its option codes onto.
option_uid_system
instance-attribute
¶
The DHIS2 identifier namespace an option-set ConceptMap carries its option UIDs onto.
attribute_option_combo_uid_system
instance-attribute
¶
The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo UIDs onto.
attribute_option_combo_code_system
instance-attribute
¶
The DHIS2 identifier namespace an attribute-combo ConceptMap carries its option-combo codes onto.
Methods:¶
from_config(config, canonical)
classmethod
¶
Derive every conversion name from the IG canonical plus the [generate] naming tokens and base.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
target_identifier_system(form_kind)
¶
The DHIS2 identifier system one form kind's Questionnaire names the object it was generated from under.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
OptionEntry
¶
Bases: BaseModel
One DHIS2 option of a served CodeSystem, in every spelling a coded answer may name it by.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
concept_code
instance-attribute
¶
The code the served CodeSystem publishes the option under - what the contract asks for.
option_code = None
class-attribute
instance-attribute
¶
The DHIS2 option code, when the artifacts carry it; None when only the UID is recoverable.
wire_value
property
¶
What a DHIS2 data value stores for this option: its code, falling back to its UID.
ResolvedOption
¶
Bases: BaseModel
One option a received code resolved to, and which spelling of it matched.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
OptionLookup
¶
Bases: BaseModel
The outcome of resolving one received code against a served option table.
Exactly one of the three states holds: option set is a resolution, ambiguous_option_uids
non-empty is a code the table carries more than once at one tier, and both empty is a code
the table does not carry at all.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
OptionTable
¶
Bases: BaseModel
Every option of one served CodeSystem, resolvable by each of the three spellings it can arrive as.
The table is the inverse of what the terminology target emitted. Under
concept_code_source = "id" the concept code is the option UID and the dhis2-code property
carries the DHIS2 code; under "code" the concept code is the DHIS2 code and the dhis2-id
property carries the UID. Both spellings identify the same option, which is why a lenient
resolution accepts either and notes what it did.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
QuestionSpec
¶
Bases: BaseModel
One answerable question of a served form, as the translator writes its DHIS2 data value.
data_element_uid and category_option_combo_uid come straight from the link-id grammar:
a plain question's link id is the data element UID, and a disaggregated aggregate cell's is
<dataElement>.<categoryOptionCombo> - the very key a DHIS2 data value carries. A tracker
registration form asks tracked entity attributes rather than data elements, and its link ids
are attribute UIDs, so data_element_uid is the attribute a TrackerAttribute is keyed by.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
option_system = None
class-attribute
instance-attribute
¶
Canonical of the CodeSystem a coded answer resolves against, or None when the binding is open.
value_type = None
class-attribute
instance-attribute
¶
The DHIS2 value type behind the question, when the caller supplied one; the compiled IG publishes none.
entity_level = None
class-attribute
instance-attribute
¶
Which DHIS2 level a registration answer is imported at, off the question's D2EntityLevel extension.
True is a tracked entity attribute of the program's tracked entity type, whose value is stated on the tracked entity; False is an attribute only the program asks, whose value is stated on the enrollment the registration creates. None is a form that states no level - every other form kind, and a registration form published before the guide carried the extension - and the translator then writes the answer on the tracked entity.
required = False
class-attribute
instance-attribute
¶
Whether the form makes the question mandatory - Questionnaire.item.required.
On a registration form this is the program's own join saying so: the emitter writes
required from programTrackedEntityAttributes.mandatory, which is the grain DHIS2 answers
E1018 on. That makes it the one fact that decides whether an entity-level answer may ride
the enrollment of a person the instance already holds.
FormSpec
¶
Bases: BaseModel
One served Questionnaire flattened into what a response answering it translates through.
attribute_option_combo_value_set is the vocabulary the form's D2AttributeOptionCombos
extension declares, and its presence is what makes the response-side extension required: a
data set on the default category combo declares none, and its values are keyed under the one
attribute option combo it has.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
tracked_entity_type_uid = None
class-attribute
instance-attribute
¶
The DHIS2 type a registration form enrols a person as, off the form's $DHIS2-TET identifier.
attribute_option_combo_value_set = None
class-attribute
instance-attribute
¶
Canonical of the ValueSet the form declares its responses key their values from, or None.
attribute_option_combo_system = None
class-attribute
instance-attribute
¶
Canonical of the CodeSystem behind that ValueSet, or None when the context does not carry it.
ConversionContext
¶
Bases: BaseModel
Everything the QR -> DHIS2 translator reads besides the response itself.
Assembled once from the compiled IG artifacts - the served Questionnaires, the option-set CodeSystems (and their ConceptMaps, where a code-mode guide needs them), the ValueSets that bind the two, and the published Locations - plus the two dials a run chooses: how exactly a coded answer has to name its concept, and which IANA zone DHIS2's zone-less timestamps are wall-clock readings in.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
forms = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Every served form, keyed by the questionnaire canonical a response names it under.
option_tables = Field(default_factory=dict)
class-attribute
instance-attribute
¶
Every served option terminology, keyed by its CodeSystem canonical.
organisation_unit_uids_by_location_id = Field(default_factory=dict)
class-attribute
instance-attribute
¶
The DHIS2 UID behind every published Location id - a code stem under code-or-id naming.
resolves_organisation_units
property
¶
Whether the context was given a Location table to resolve organisation-unit references through.
Methods:¶
form_for(reference)
¶
The served form one reference names: the questionnaire canonical, or the bare form id.
forms is keyed by the canonical a response carries, and a canonical ends in the form's
id - so a caller holding the id alone (the segment a UI routes on) resolves here too,
rather than building a second index. None for nothing named and for nothing served under
the name, which are the same answer to a translator: no form to read the response against.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionResult
¶
Bases: BaseModel
What one QuestionnaireResponse translated into: a payload, or the reasons there is none.
Exactly one of data_value_set, event, tracked_entity, and enrollment is set when
refusals is empty, and target_kind names which. A refused response carries none of them: a
response the translator cannot read whole produces a named refusal rather than a partial payload
that would import the half of itself that happened to parse.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
tracked_entity = None
class-attribute
instance-attribute
¶
The person and the enrollment a registration response creates, carried whole for one /api/tracker post.
enrollment = None
class-attribute
instance-attribute
¶
The enrollment alone a registration whose subject the instance already holds creates.
Set instead of tracked_entity, and posted as a top-level enrollments array: an enrollment
that rides inside a trackedEntities wrapper rewrites the person's owning organisation unit
(BUGS.md 73), and a person this response did not create is not this response's to move.
completeness = None
class-attribute
instance-attribute
¶
The completeness the aggregate response claims, set only where its status is completed.
Carried beside the data value set rather than inside it, because it is a second write to a second
resource and it only happens once DHIS2 has taken the values. A response reporting itself
in-progress translates its values and carries nothing here.
is_refused
property
¶
Whether the response produced a refusal instead of a payload.
payload
property
¶
The payload target_kind names, or None when the response was refused.
The four payload fields are mutually exclusive - the invariant the class docstring states -
and the target kind is what says which one carries the document, so this is the one place
that invariant is written down rather than re-derived at every call site. A registration
chooses between two kinds for that reason: TRACKER for the person this response mints,
with the enrollment nested inside it, and TRACKER_ENROLLMENT for the enrollment alone on a
person the instance already holds. The typed fields stay for the caller wanting one shape.
Methods:¶
payload_of(wire_shape)
¶
This result's payload when its target kind produced the wire shape asked for, else None.
The narrowing accessor beside payload: a caller that only acts on aggregate envelopes asks
for DataValueSet and reads a typed answer, rather than testing a field that a tracker
result leaves unset for reasons of its own.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
ConversionReport
¶
Bases: BaseModel
The outcome of translating a batch of spooled responses, in the order they were drained.
The order here is the spool's, not the posting order: a caller draining into DHIS2 posts
FORWARD_TARGET_ORDER so a registration lands before the stage events of the same drain.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
Attributes¶
translated
property
¶
Every response that produced a payload.
refused
property
¶
Every response that produced a refusal.
data_value_sets
property
¶
Every aggregate envelope the batch produced, ready to post to /api/dataValueSets.
events
property
¶
Every event the batch produced, ready to post to /api/tracker.
tracked_entities
property
¶
Every registration the batch produced, ready to post to /api/tracker before its events.
enrollments
property
¶
Every enrollment-only payload the batch produced, for the people the instance already holds.
Methods:¶
payloads_of(wire_shape)
¶
Every payload of one wire shape the batch produced, in the order the responses were drained.
The four properties above are this method under the names a caller posts them by; a refused response contributes nothing, because a refusal is the absence of a payload.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/schemas.py
context
¶
Assembling the translation context from the compiled IG artifacts a project publishes.
The translator reads one thing besides the response: a ConversionContext. This module builds it
from the very documents d2w fhir serve holds - the served Questionnaires, the option-set
CodeSystems, the ValueSets binding a question to its terminology, the ConceptMaps taking a
concept code back to its DHIS2 option code, and the published Locations whose ids are identity
stems rather than DHIS2 UIDs.
Two things the compiled IG deliberately does not publish are threaded in by the caller. The DHIS2
value type behind a question is one: a Questionnaire states an R4 item type, and BOOLEAN and
TRUE_ONLY share #boolean, so a caller holding the instance's metadata passes
value_types_by_data_element and the translator writes TRUE_ONLY values the way DHIS2 stores
them. Without it a boolean question is read as BOOLEAN and the response carries a note saying so.
The other is the project timezone, which is what a zoned R4 timestamp is read back through
(BUGS.md #62).
A tracker registration form's questions are the program's tracked entity attributes, and they are
read through the very same walk: an attribute has the DHIS2 value types a data element has, binds
option sets the same way, and lands on the same QuestionSpec. value_types_by_data_element is
keyed by whichever object the link ids name, so a caller wanting TRUE_ONLY right on a
registration form passes the attributes' value types in the same table.
One thing a registration question does carry that a data element's cannot: the D2EntityLevel
extension, which says whether DHIS2 imports its answer onto the tracked entity or onto the
enrollment. It is read off the item here, so the translator splits the answers by what the guide
published rather than by anything the caller has to supply.
Classes¶
ConversionContextError
¶
Bases: ValueError
Raised when a compiled artifact cannot be read into the context the translator needs.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
Functions:¶
build_conversion_context(naming, questionnaires, *, code_systems=(), value_sets=(), concept_maps=(), locations=(), value_types_by_data_element=None, coded_answer_mode=CodedAnswerMode.LENIENT, timezone=None)
¶
Read one project's compiled artifacts into the context every response is translated through.
questionnaires decides which responses can be translated at all - a canonical absent here is
a refusal, not a guess. value_sets binds a #choice question to the CodeSystem behind its
answerValueSet, and code_systems (refined by concept_maps where a code-mode guide needs
the DHIS2 option code back) is what a coded answer resolves against. locations is what turns
a Location/<id> reference into the DHIS2 organisation unit UID it stands for, which under
code-or-id naming is not the id itself.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
build_form_spec(questionnaire, naming, code_system_urls_by_value_set, value_types_by_data_element)
¶
Flatten one served Questionnaire into the form spec a response answering it translates through.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
build_option_table(code_system, naming, concept_maps=())
¶
Read one served CodeSystem into the option table a coded answer resolves against.
The concept properties carry the complementary DHIS2 identifier both ways round, so the table
is complete under concept_code_source = "id" from the CodeSystem alone. Under "code" an
option whose DHIS2 code was not a usable concept code took the UID instead, and only the set's
ConceptMap still knows the code - which is why the maps refine the table where they are given.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/context.py
artifacts
¶
Reading one project's published IG into the translation context a forward run drains through.
build_conversion_context takes plain R4 models; this module is where a project's guide becomes them.
collect_artifacts is the one place documents are sorted into the five resource types the translator
reads - Questionnaire, CodeSystem, ValueSet, ConceptMap, and Location - and everything else the guide
publishes is passed over, because nothing in the QR -> DHIS2 direction consults it.
Two ways a guide reaches it. load_compiled_artifacts reads the two trees d2w fhir serve serves as
one: ig/fsh-generated/resources, which SUSHI compiled from the emitted FSH, and ig/input/resources,
the predefined registry, terminology, and ConceptMap tree the generate targets wrote as JSON. A project
that has never run SUSHI has neither, and dhis2w_fhir.service.fetch_live_artifacts builds the same
documents off the instance instead - the same builders d2w fhir serve --live answers reads from - so
a capture UI that needed no build step drains through a forward that needs none either.
How an unreadable document is handled depends on what it is. A Questionnaire the R4 models cannot
read fails the collection naming its source: a form quietly skipped turns every response answering it
into an unknown-form refusal, which reads as a problem with the data rather than with the guide. The
other four cost only what they carry - a coded answer resolved unchecked, an organisation-unit
reference read as a UID - so an unreadable one is left out and named on unreadable_resources, which
the caller reports. A guide is free to hand-write terminology this package has no model for, and one
such document is not worth refusing a whole spool over.
Classes¶
CompiledIgMissingError
¶
Bases: LookupError
Raised when a project has no compiled IG to translate captured responses against.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
CompiledArtifactReadError
¶
Bases: ValueError
Raised when a published resource the translator reads cannot be parsed as its R4 model.
CompiledArtifacts
¶
Bases: BaseModel
Everything one published guide holds that the QR -> DHIS2 translator reads.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
SourcedDocument
¶
Bases: BaseModel
One wire document paired with whatever names it in a diagnostic - a file path, or a builder.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
BoundQuestionUids
¶
Bases: BaseModel
The DHIS2 objects one published guide asks questions from, split by the endpoint that types them.
Both halves feed the same value_types_by_data_element table the context takes, because a link
id names one object whichever kind of form carries it - the split exists only because a data
element's value type is read from /api/dataElements and an attribute's from
/api/trackedEntityAttributes.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
ProgramRuleNames
¶
Bases: BaseModel
The DHIS2 program rules a published guide names, keyed by UID, for reading a refusal back.
DHIS2 refuses a tracker import a rule rejected with E1300 and names the rule by UID alone. The
guide published that UID beside the rule's name, so the drain reads the refusal as the name an
administrator knows the rule by, while the UID stays on the response's own report.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
Functions:¶
load_compiled_artifacts(project)
¶
Read one project's compiled IG plus its predefined resource tree into the artifacts the translator reads.
Compiled resources are read first and the predefined tree second, which is the order the facade loads them in, so a project reading its own guide two ways sees one collection.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
collect_artifacts(documents)
¶
Sort wire documents into the five resource types the QR -> DHIS2 translator reads.
One collection point for both ways a project's guide reaches the translator: read off disk from
a compiled build, or built in memory from the instance a --live run points at. Anything
outside TRANSLATED_RESOURCE_TYPES is passed over, because nothing in the response direction
consults it.
How an unreadable document is handled depends on what it is. A Questionnaire the R4 models
cannot read fails the collection naming its source: a form quietly skipped turns every response
answering it into an unknown-form refusal, which reads as a problem with the data rather than
with the guide. The other four cost only what they carry - a coded answer resolved unchecked, an
organisation-unit reference read as a UID - so an unreadable one is left out and named on
unreadable_resources, which the caller reports.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
bound_question_uids(artifacts, naming)
¶
Every DHIS2 object the published forms ask a question from, sorted, read through the link-id grammar.
The forms are flattened with the very call the context builds them with, so the UIDs asked of the
instance are exactly the ones a translated answer will be keyed by - a disaggregated cell's
<dataElement>.<categoryOptionCombo> link id contributing its data element once. A tracker
registration form's link ids are tracked entity attribute UIDs, so they are collected apart:
they are the same question grammar keyed to a different DHIS2 object, and a different endpoint
types them.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
program_rule_names(artifacts, naming)
¶
Every program rule the published forms carry, read off their D2ProgramRule extensions.
Only the rules a form could not express are published this way, which is exactly the set a refusal can name: a rule the form did express refuses nothing the form admits.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
build_project_context(project, artifacts, *, value_types_by_data_element=None, coded_answer_mode=CodedAnswerMode.LENIENT)
¶
Assemble the translation context one project's captured responses are read through.
Every name comes from fhir.toml - the IG canonical the extensions are pinned to and the
[generate] naming tokens and identifier base - and the project timezone is what a zoned R4
timestamp is read back through into the wall clock DHIS2 stores (BUGS.md #62).
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/artifacts.py
values
¶
Serialising one question's answers into the DHIS2 wire value, exhaustively per value type.
This is the inverse of what the examples emitter wrote forward, and it is written as one branch
per WireValueKind so the two directions can be read side by side:
TRUE_ONLY `"true"`, or no data value at all - DHIS2 never stores `"false"` here.
BOOLEAN `"true"` / `"false"`.
INTEGER family the integer verbatim; the form's own `minValue` / `maxValue` are DHIS2's to enforce.
NUMBER family the lexical decimal the R4 primitive carries - a whole number stays whole.
MULTI_TEXT every selected option's DHIS2 code, joined by the separator DHIS2 splits on.
option-coded the resolved option's DHIS2 code, falling back to its UID.
DATE / TIME the R4 primitive verbatim; both are already what DHIS2 stores.
DATETIME the zone-less wall clock behind the zoned R4 instant (BUGS.md #62).
URL / text the string verbatim, which is where `COORDINATE` lands too.
ORGANISATION_UNIT the DHIS2 UID behind the answered Location reference.
attachment refused - DHIS2's file-resource wire is a separate upload, not a data value.
Nothing here reaches for a default. A question the translator cannot serialise answers with a typed refusal naming the link id, so a caller never posts a payload with a value quietly missing from the middle of it.
Attributes¶
Classes¶
WireValue
¶
Bases: BaseModel
One DHIS2 data value the translator produced, and what producing it had to interpret or refuse.
value is None on two very different outcomes, which refusals separates: a TRUE_ONLY
question answered false writes no data value because that is how DHIS2 spells false, while
a refused answer writes none because the response is not translatable at all.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
WallClockReading
¶
Bases: BaseModel
One R4 timestamp read back to the zone-less wall clock DHIS2 stores, and what reading it took.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
Attributes¶
derived = False
class-attribute
instance-attribute
¶
Whether an offset was present and read back through the project's zone.
unzoned = False
class-attribute
instance-attribute
¶
Whether no offset was present, so the timestamp was taken as already being the wall clock.
moment = None
class-attribute
instance-attribute
¶
The same wall clock as a zone-less datetime, which is what TrackerEvent.occurredAt takes.
OrganisationUnitResolution
¶
Bases: BaseModel
The DHIS2 organisation unit one Location/<id> reference names, and what resolving it took.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
Functions:¶
answer_wire_value(question, answers, context)
¶
Serialise every answer to one question into the single data value DHIS2 stores for it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
resolve_option(table, code, mode)
¶
Resolve one received code to the option it names, as strictly as the run's dial asks.
Strict accepts the concept code the served CodeSystem publishes and nothing else. Lenient then tries the DHIS2 option UID and the DHIS2 option code, because the generated CodeSystem carries every option under both spellings and a client that sent the other one still named exactly one option. Two matches inside one tier is an ambiguity leniency cannot paper over.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
wall_clock_reading(value, timezone)
¶
Read one R4 timestamp back to the zone-less wall clock DHIS2 stores - the inverse of zoned_date_time.
DHIS2 serves and accepts occurredAt and DATETIME data values as zone-less local timestamps
under fields its OpenAPI types as Instant (BUGS.md #62), so the offset an R4 dateTime
requires is exactly what has to come back off. timezone is the IANA zone those wall-clock
readings are taken in, which the project states as [generate] timezone; naming none reads
the clock in UTC, which is the same guess the forward direction makes.
A date-only value carries no clock and passes through, and so does a timestamp already written
without an offset - the caller is told which through unzoned rather than left to assume.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
decimal_wire_value(value)
¶
Write one R4 decimal answer as the DHIS2 wire value it is, without a float round trip.
A whole number stays whole - the R4 model types valueDecimal as int | float for exactly
that reason, so 2896 never becomes 2896.0. A fractional number is written as the shortest
decimal that reads back as the same number, and never in exponent notation, which DHIS2's
numeric value types do not accept.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
resolve_organisation_unit(reference, context)
¶
Resolve a Location/<id> reference to the DHIS2 organisation unit UID the published Location identifies.
A Location id is an identity stem, and under naming.source = "code" that stem is the unit's
DHIS2 code rather than its UID - so the resolution goes through the org-unit identifier the
registry wrote and never assumes the id is the UID. A context given no Location table has
nothing to go through and falls back to reading the id as a UID, which it says out loud.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
wall_clock_notes(reading, context, *, link_id)
¶
The note one wall-clock reading is worth, so a stripped offset is never silent.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/values.py
payloads
¶
The five payload translators: one per DHIS2 form kind, each writing the import shape DHIS2 reads.
aggregate -> a `/api/dataValueSets` envelope: data set, ISO period, organisation unit, the
attribute option combo the whole report is filed under where the form declares
a vocabulary for one, and one data value per answered cell, each carrying its
category option combo.
event -> one `/api/tracker` event of an event program: the UID derived from the
receipt's own logical id, program, organisation unit, occurrence, status,
and one data value per answered question.
tracker -> one `/api/tracker` tracked entity: its client-minted UID, the tracked entity
type the form names, the organisation unit that owns it, one attribute per
answered entity-level question, and the single enrollment the response
creates - minted UID, program, organisation unit, enrolment date, incident
date where one was stated, `ACTIVE` status, and one attribute per answered
program-only question. A response stating `D2SubjectExists` names a person
the instance already holds, and produces that enrollment alone - naming the
existing tracked entity, with no tracked entity beside it to rewrite.
tracker-event -> the same event as `event`, plus the program stage it belongs to, the tracked
entity it was captured for, and the enrollment it sits on.
Every DHIS2 object a tracker payload creates is named before it is posted. A registration reads its tracked entity and enrollment UIDs off the response, where the client that filled the form minted them; both event kinds derive theirs from the receipt's own logical id, so one receipt always names one event. A dry run and the import behind it therefore report the same UID, and forwarding a receipt twice is refused by the instance as an object it already holds rather than filing a second copy of one visit.
Every fact a payload carries is read out of the response through an identifier or an extension,
never out of a URL. A Questionnaire's canonical segment is an identity stem, and a Location's id
is one too, so a data set UID comes off the form's .../id/data-set identifier and an
organisation unit UID off the registry's .../id/org-unit slice - which under code-or-id naming
are not what the ids spell.
A response the translator cannot read whole produces refusals and no payload at all. There is no partial envelope: a data value set missing the third of its forty cells would import as a complete report of a period, which is worse than not importing.
Classes¶
TranslatedAnswer
¶
Bases: BaseModel
One answered question and the DHIS2 wire value it produced.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
TranslatedAnswers
¶
Bases: BaseModel
Every data value one response's item tree produced, in the order the response carries them.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
AggregateTranslation
¶
Bases: _Outcome
One aggregate response translated: its data value set, or the reasons it produced none.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
EventTranslation
¶
Bases: _Outcome
One event or tracker-event response translated: its event, or the reasons it produced none.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
RegistrationTranslation
¶
Bases: _Outcome
One registration response translated: what it creates in DHIS2, or the reasons it produced none.
tracked_entity and enrollment are alternatives, and target_kind says which one the
response produced: a registration creating the person it enrols carries the tracked entity with
the enrollment nested inside it, and one whose subject the instance already holds carries the
enrollment alone.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
Functions:¶
translate_aggregate_response(response, form, context)
¶
Translate one aggregate response into the /api/dataValueSets envelope DHIS2 imports it as.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
receipt_event_uid(response_id)
¶
The DHIS2 UID one receipt's event is imported under, derived from the receipt's own logical id.
The same receipt names the same event UID - on the dry run and on the import that follows it,
on this machine and on the next. That property is the whole point: an event travels to
/api/tracker under importStrategy=CREATE, so forwarding a receipt DHIS2 already holds the
event of is refused as an object that exists, rather than filing a second copy of one visit.
It is the identity a registration already has - the tracked entity UID its subject identifier
carries - given to the one payload kind that carried none, and it is what lets a dry run's
diagnostics be read against the objects the import then creates, because both name this UID.
The material is <response id>:event:0, hashed with SHA-256 - never Python's per-process
salted hash - and shaped into [A-Za-z][A-Za-z0-9]{10} by the same drawer the synthesis
path mints tracked entity and enrollment UIDs with. The trailing ordinal is the discriminator
a receipt naming more than one event would move; a receipt reports exactly one.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translate_event_response(response, form, context)
¶
Translate one event-program response into the single /api/tracker event DHIS2 imports it as.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translate_tracker_event_response(response, form, context)
¶
Translate one tracker program stage response into the enrolled /api/tracker event it reports.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translate_tracker_registration_response(response, form, context)
¶
Translate one registration response into the /api/tracker tracked entity and enrollment it creates.
Both DHIS2 identities are the client's: the tracked entity UID the subject identifier carries
and the enrollment UID the D2TrackerEnrollment extension carries are minted by whoever filled
the form, and they travel to DHIS2 as sent. That is what lets the stage events of the same
enrollment be captured before either identity exists, and it is why a registration is posted
before them.
The attributes are the form's own answers, serialised through the value-type machinery a data element's answers go through - a tracked entity attribute has the same DHIS2 value types, binds option sets the same way, and its coded answers resolve against the published ValueSets on the same strict/lenient dial.
DHIS2 imports those answers at two levels, and the form says which is which: a question whose
D2EntityLevel extension is true asks a tracked entity attribute of the program's tracked
entity type, and its value is stated on the tracked entity; a question stating false asks an
attribute only the program collects, and its value is stated on the enrollment. A question
stating no level at all - a guide compiled before the extension was published - is written on
the tracked entity, which is where every registration answer went before the split.
A response carrying D2SubjectExists as true states that its subject identifier names a person
the instance already holds, and what it creates is then the enrollment alone - a top-level
enrollments array naming that existing tracked entity, posted under the same plain
importStrategy=CREATE every other payload goes under. No trackedEntities wrapper goes round
it: an enrollment that rides inside one has to be posted CREATE_AND_UPDATE, which silently
rewrites the person's owning organisation unit (BUGS.md 73), and the person is not this
response's to move. The program's own attributes ride the enrollment, because DHIS2 answers
E1018 to a mandatory program attribute that arrives on nothing.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translate_tracked_entity_response(response, form, context)
¶
Translate one person-only response into the /api/tracker tracked entity it creates.
The registration translator without its enrollment half. DHIS2 accepts a bare trackedEntities
import under plain CREATE, so the payload is one tracked entity carrying the type off
$DHIS2-TET, the organisation unit it is owned at, and its answers - every one of them on the
entity, because the form asks only attributes the type itself collects and there is no
enrollment for an answer to land on.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translate_answers(response, form, context)
¶
Walk one response's item tree and serialise every answered question, in document order.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/payloads.py
translator
¶
The entry point: one QuestionnaireResponse in, one typed DHIS2 payload or a named refusal out.
Translation is three questions asked in order, and the first that cannot be answered ends the response: which DHIS2 form kind the submission claims to be, which served form it answers, and whether the two agree. Only then does the form kind's payload translator run.
The batch form drains a spool the same way one response at a time, so a ConversionReport is one
result per submission in the order they were drained - a refusal never stops the responses behind
it, and a caller posts report.tracked_entities, report.enrollments, report.data_value_sets,
and report.events while routing report.refused back to whoever sent them. People first, then
the payloads that create an enrollment: a registration naming an existing person answers against
one a person-only response of the same drain creates, and a stage event answers against an
enrollment the registration is what creates.
Classes¶
Functions:¶
translate_response(response, context)
¶
Translate one captured response into the DHIS2 import payload its form kind reports as.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/translator.py
translate_responses(responses, context)
¶
Translate a batch of captured responses, one result each, in the order they were drained.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/conversion/translator.py
The conformance runner¶
What d2w fhir doctor concluded about one instance, as
models rather than as terminal output. A DoctorReport carries one DoctorPhaseResult per
phase - the outcome, the one line it is read by, the reason a phase that did not run gives,
and every DoctorFinding it raised with the field path a mismatch was found at. The graders
are the pure half of the runner: grade_forward, grade_capture, grade_oracle, and
grade_drift turn a phase's own report into the verdict it is recorded under, over
ForwardReport, CaptureOutcome, FamilyOutcome, and DriftReport, and
render_doctor_markdown turns the whole run into the report file a handover is read from.
run_doctor is the other half, and what it does to the machine is part of its contract: it
mints a workspace directory (or writes into the one DoctorOptions.workspace names) and
removes a minted one unless keep is set, runs sushi or docker run where a compiler is
on the machine, writes compiled resources under ig/fsh-generated/resources, runs an ASGI
application in process, and posts a synthetic corpus at the instance under validate-only
mode. A caller that cannot afford that wants the graders rather than the run.
doctor
¶
The instance conformance runner behind d2w fhir doctor: run the whole toolchain, report what breaks.
Doctor scaffolds a throwaway project against the ambient DHIS2 profile, generates the IG source from that instance, compiles it when a compiler is on the machine, serves it in process, captures a synthetic corpus through the served endpoint, drains that corpus at the instance under validate-only mode, lets the instance judge the served resources object by object, and closes by asking whether the guide already on disk still describes the instance it was generated from. Every phase reports one typed result, and a failure never stops a phase that does not depend on it.
The direction of judgement is the whole point of the oracle phase: the DHIS2 instance is the authority and the served output is what is on trial, never the reverse. A mismatch is a fact about this toolchain against this instance, stated with the field path it was found on.
No MCP tool, the way d2w profile has none. A run writes a project tree, shells out to a compiler,
posts a corpus through an in-process server, and reads a whole instance - a write-heavy
orchestration with no read-only shape an MCP tool could honestly advertise.
run_doctor is a library call all the same, and what it does to the machine is part of its
contract rather than a surprise: it mints a workspace directory (or writes into the one
DoctorOptions.workspace names) and removes a minted one unless keep is set, runs sushi or
docker run when a compiler is on the machine, writes compiled resources under
ig/fsh-generated/resources, runs an ASGI application in process, and posts a synthetic corpus at
the instance under validate-only mode. A caller that cannot afford any of that wants the phases it
grades rather than the run: grade, grade_capture, grade_forward, grade_oracle, and grade_drift are
pure over DoctorFinding, CaptureOutcome, FamilyOutcome, ForwardReport, and DriftReport.
The drift phase is the one phase whose subject is not the throwaway project. It reads the published
guide the working directory sits in, so resolve_published_project runs before anything is written
and returns nothing for a working directory inside the workspace: a project this run generated
seconds ago can only agree with the instance, and a phase that always passes says nothing.
Classes¶
DoctorPhase
¶
Bases: StrEnum
The phases of one doctor run, in the order they execute.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
DoctorOutcome
¶
Bases: StrEnum
What one phase concluded, which is what the verdict and the exit code are read off.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
DoctorFinding
¶
Bases: BaseModel
One thing a phase found: what object, what is wrong with it, and where on it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
DoctorPhaseResult
¶
Bases: BaseModel
The outcome of one phase: the verdict, the one line it is read by, and what it found.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
DoctorOptions
¶
Bases: BaseModel
The dials one doctor run is invoked with.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
Attributes¶
workspace = None
class-attribute
instance-attribute
¶
Where the throwaway project is written. None mints a temporary directory.
keep = False
class-attribute
instance-attribute
¶
Keep a minted temporary workspace instead of removing it when the run ends.
all_targets = False
class-attribute
instance-attribute
¶
Scaffold empty selection tables, which selects every data set and every program.
live = False
class-attribute
instance-attribute
¶
Run the oracle phase, where the instance judges the served resources object by object.
samples = DEFAULT_ORACLE_SAMPLES
class-attribute
instance-attribute
¶
How many resources per family the oracle deep-compares against the instance.
DoctorReport
¶
Bases: BaseModel
Everything one doctor run concluded: what it ran against, where it ran, and what each phase found.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
Attributes¶
version_tree = None
class-attribute
instance-attribute
¶
The v41 / v42 / v43 plugin tree the client bound after detecting the instance's version.
failed_phases
property
¶
Every phase that concluded the toolchain is broken against this instance.
findings
property
¶
Every finding of the run, in phase order.
counts_by_outcome
property
¶
How many phases reached each outcome - the shape the verdict line is written from.
verdict_line
property
¶
The whole run in one line: the verdict word, then how many phases reached each outcome.
PhaseOutcome
¶
Bases: BaseModel
One phase's verdict before it is recorded: what it concluded, the line it says so on, its findings.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
CaptureOutcome
¶
Bases: BaseModel
What one form contributed to the capture phase: whether it generated, whether it was accepted, and why not.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
FamilyOutcome
¶
Bases: BaseModel
What one oracle family concluded: the line it is summarised by, and every mismatch it found.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
Functions:¶
grade(phase, evidence, findings)
¶
Grade one phase off what it found: an error breaks it, a warning degrades it, nothing passes it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
generate_findings(notes)
¶
Every note one generate run raised, as the findings the generate phase reports.
A note is a decision the emitters took against this instance's metadata - a form reshaped, a code fallen back to a UID, several tracked entity types left to the default resource - so the phase reports all of them and none of them breaks the run. The projection lives here rather than inline so a note's wording is what a test of the finding reads.
One note is one finding, however many targets raised it. Several targets read the same source
notes - a form-structure note reaches the questionnaires, the examples, and the pages alike -
and a table stating one fact about the instance three times says nothing the first row did not,
while pushing other findings behind the cap that keeps it readable. GenerateNote is frozen,
so this dedupes by value and keeps the order the targets raised them in.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
grade_capture(captured)
¶
Grade the capture phase: an endpoint refusing what it generated is broken, a form it cannot fill is not.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
grade_forward(report)
¶
Grade the forward phase: a DHIS2 rejection is broken, a translator refusal is a note.
Rejections roll up by cause rather than by response, because DHIS2 names one row per broken rule and two hundred responses breaking the same rule are one thing to fix. What a dry run could not check is left out of the findings entirely: an event whose enrollment this very run would have created is unverifiable rather than refused, and an import is what settles it.
A drain that stopped early is an error of its own. It says the instance stopped answering part-way through, which is a fact about the instance this run is judging rather than about any payload, and a phase that passed on the strength of the half it managed would be reporting the quiet failure as a clean bill.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
grade_oracle(families)
¶
Grade the oracle phase off what the instance said about each family it judged.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
drift_findings(report)
¶
Every object that moved since the guide was published, as the findings the drift phase reports.
Drift is warning-class throughout, which is the line doctor already draws: a failure means the
toolchain does not work against this instance, and a guide describing the instance as it stood
last month still serves, still captures, and still forwards. It is out of date, not broken, and
the exit code says so - only a fail exits 1.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
grade_drift(report)
¶
Grade the drift phase: a moved object is a note about the guide, never a break in the toolchain.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
resolve_published_project(workspace)
¶
The published project this run checks for drift: the one the working directory sits in, or none.
Doctor's own workspace is not it. The run scaffolds and generates that project seconds before the
drift phase reads it, so it can only ever agree with the instance - and a phase that always
passes says nothing. A published guide is one somebody generated and compiled at some point in
the past, which is exactly what a fhir.toml found by walking up from the working directory is.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
resolve_doctor_profile()
¶
The instance doctor runs against: d2w -p, then DHIS2_PROFILE, then a nearby project, then the default.
The same resolution d2w fhir validate uses, and for the same reason: doctor's subject is an
instance rather than a project, and it scaffolds the project it works in, so a fhir.toml in
the working directory is a source of the profile name and nothing else.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
run_doctor(generation, options, *, reporter=None, client=None)
async
¶
Run every phase against the instance the profile names and report what each one concluded.
client is a connection the caller already holds open. Handed one, the connect phase reads the
instance version off it and opens nothing, and the client is left open when the run ends - its
lifetime belongs to whoever entered it. With none, the profile opens one for the run and closes
it on every exit path.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
render_doctor_markdown(report)
¶
Render one run as the markdown report a handover is read from, phase table first, findings after.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
phase_evidence(phase)
¶
The one line a phase is read by: its evidence, with the reason folded in when it did not run.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/doctor.py
Drift between a published guide and the instance¶
Whether the guide already on disk still describes the instance it was generated from.
detect_drift reads the instance for everything one project publishes and names every
organisation unit, option, question, and program stage that moved inside that project's own
selection scope; read_published_guide is its offline half, projecting the published
artifacts onto the objects the comparison is about. The three comparators -
compare_organisation_units, compare_option_set, and compare_form - are pure over a
PublishedGuide side and an instance side, so a caller can grade a snapshot it holds without
opening a connection. Every finding is a DriftFinding naming what moved, which direction it
moved in, and what each side says about it.
drift
¶
What the instance holds that the published guide does not: the drift report behind doctor's last phase.
A guide is a photograph. d2w fhir generate reads the instance once, the compiler turns that reading
into artifacts, and from then on the artifacts say what the instance said on the day they were
written. DHIS2 keeps moving: a chiefdom is split, an option is added to a set, a question is dropped
from a stage. Nothing in the published guide knows, and nothing in the toolchain said so until this
module.
Drift is measured inside the project's own selection scope and nowhere else. An organisation unit
outside [generate.organisation_units] is not drift - the project never asked for it - and neither
is a data set the selection tables leave out. The scope is read from the same fhir.toml the
publication was generated under, so the question is always "does this guide still describe the part
of the instance it claims to describe", never "has the instance changed anywhere".
The five classes¶
| Class | Published side | Instance side |
|---|---|---|
| Organisation units | the Location of every unit in the registry |
the hierarchy under root, to max_level |
| Options | the concepts of a published option-set CodeSystem |
that option set's options |
| Tracked entity attributes | a tracker or tracked-entity form's questions |
the program's or type's attributes |
| Data elements | an aggregate, event, or tracker-event form's questions |
the data set's or stage's elements |
| Program stages | the tracker-event forms a tracker program publishes |
that program's stages |
Each class reports in both directions and on renames alike: something the instance gained, something
it lost, and something whose name changed under an identity that did not. A rename matters because
the published display is what a reader of the guide sees - D2TEA_CS carries every attribute's name,
a CodeSystem concept carries every option's - so a guide naming an object what the instance no
longer calls it is wrong in the way documentation is wrong.
Tracked entity types are out of scope. d2w fhir validate already names every type the project
never typed, under unmapped-tracked-entity-type, and one fact reported twice in two vocabularies is
worse than one report; the drift phase points at that checklist rather than repeating it.
What is compared, and what is deliberately not¶
Identity is the DHIS2 UID throughout, because that is what survives a rename and what a consumer
joins on. Names are compared through flatten_whitespace, and a published name also matches the
wording substitute_build_aborting_text produces: a project generated with hostile_names =
"substitute" publishes "Fixed, under 1y" for an instance that says "Fixed, <1y", and reporting that
as a rename would be reporting the toolchain's own rewrite back at the reader.
Codes are not compared. A code change is a real event, but it is one the identifier slices already
carry and one d2w fhir validate grades for FHIR-safety, so the drift report stays about the objects
a form asks and the names a reader reads.
The remedy is the same sentence for every finding, which is why it is stated once on the phase rather
than once per row: regenerate, then compile. That is the documented lifecycle - d2w fhir generate
re-reads the instance, make sushi turns the new source into artifacts - and nothing about a drifted
object needs a different answer.
Classes¶
DriftSubject
¶
Bases: StrEnum
The kinds of object this report covers, each spelled as a reader of the guide would say it.
The first five are what the report is about: the objects a guide carries inside its artifacts. The rest name the DHIS2 object one whole artifact was generated from, for the one case where the artifact outlived it - a selected option set or form the instance no longer holds. Naming that object as an option or a data element would be naming the wrong thing.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
DriftKind
¶
Bases: StrEnum
Which direction one object drifted in.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
DriftFinding
¶
Bases: BaseModel
One drifted object: what it is, where the guide carries it, and what each side says about it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
Attributes¶
holder
instance-attribute
¶
The published artifact the object belongs to, in the words the guide names that artifact by.
name
property
¶
The one name to call this object by: the instance's when it holds it, the guide's otherwise.
title
property
¶
The object as a finding names it: what it is, what it is called, and its DHIS2 UID.
detail
property
¶
What each side says about this object, as the one line the finding is read by.
PublishedObject
¶
Bases: BaseModel
One DHIS2 object as the published guide carries it: the UID it is keyed by, the name it shows.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
PublishedOptionSet
¶
Bases: BaseModel
One option-set CodeSystem the guide publishes, and the options its concepts stand for.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
PublishedForm
¶
Bases: BaseModel
One Questionnaire the guide publishes: which DHIS2 object it is keyed to, and what it asks.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
Attributes¶
kind
instance-attribute
¶
The DHIS2 form kind off the form's own code: aggregate, event, tracker, tracker-event, tracked-entity.
asks_attributes
property
¶
Whether this form's questions are tracked entity attributes rather than data elements.
holder
property
¶
How a finding names the form the drifted question belongs to.
PublishedGuide
¶
Bases: BaseModel
Everything on disk this report reads: the registry, the published option sets, the published forms.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
InstanceObject
¶
Bases: BaseModel
One DHIS2 object as the instance states it today.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
InstanceOption
¶
Bases: BaseModel
One option as the instance states it today, under both the identities a concept can be keyed by.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
InstanceOptionSet
¶
Bases: BaseModel
One option set as the instance states it today.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
InstanceForm
¶
Bases: BaseModel
What the instance says a published form's DHIS2 object collects today.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
DriftReport
¶
Bases: BaseModel
Everything one drift pass concluded: what it read, what it read it against, and what moved.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
Functions:¶
registry_scope_line(config)
¶
The slice of the hierarchy the registry claims to publish, in the words the evidence states it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
read_published_guide(project)
¶
Read what one project publishes off disk, through the reader the served store and check-artifacts use.
load_compiled_artifacts is the single reader of the two published trees - ig/fsh-generated
for what the compiler wrote, ig/input/resources for the registry and terminology the emitters
wrote straight to JSON - so a drift pass, a served store, and a conversion context can never
disagree about what a project publishes. It raises CompiledIgMissingError on a project that was
generated but never compiled, which is a fact about the project rather than about the instance.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
detect_drift(client, project)
async
¶
Read the instance for everything one published guide claims, and report every object that moved.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
compare_organisation_units(published, instance)
¶
Judge the published registry against the hierarchy slice the project selected.
Both sides are already narrowed to the selection, so every difference is drift rather than a unit the project never asked for: a unit the instance gained inside the scope, a unit it lost, and a unit whose name changed under a UID that did not.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
compare_option_set(published, instance)
¶
Judge one published CodeSystem's concepts against the options the instance's set holds today.
A concept is keyed by the option UID under concept_code_source = "id" and by the option's DHIS2
code under "code", so the instance side is indexed under both and the comparison holds whichever
mode the guide was published in.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
compare_form(published, instance, published_stage_uids)
¶
Judge one published form's questions against what the instance says its DHIS2 object collects.
A tracker registration form answers for its program's attributes and a tracked-entity form for
its type's; every other kind answers for data elements. A tracker program is also asked for its
stages, because a stage the program gained publishes no form and so asks none of its questions.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/drift.py
The capture spool¶
Where d2w fhir serve writes a receipt and where d2w fhir forward moves it next. The
layout is duplicated rather than imported - dhis2w-fhir is a dependency of
dhis2w-fhir-serve, so the arrow only points one way and the forwarder reads the files
directly under the same conventions.
spool
¶
The capture spool the forwarder drains: where a received response is read from, and where it moves next.
d2w fhir serve writes every accepted QuestionnaireResponse to <spool root>/received/<id>.json as a
receipt envelope - the root being .serve/responses inside the project unless [serve] spool_dir names
another - and ls on that directory is the pending count. This module is the read side of the same
convention plus the four states a receipt can end in:
`received/` captured, not yet forwarded - the queue.
`forwarded/` translated, posted, and accepted by DHIS2.
`rejected/` translated and posted, and DHIS2 refused it.
`withdrawn/` it landed, and `d2w fhir withdraw` retracted it from DHIS2 afterwards.
Three of those states carry an <id>.report.json sidecar holding what DHIS2 answered. A rejection needs
one to say why it was refused; an acceptance needs one because "DHIS2 took it" is not the whole answer
either - the import counts are what say how much of it landed, and a receipt filed with nothing beside
it leaves that unanswerable from the spool alone.
withdrawn/ is the one state a receipt reaches without being posted again. Its report is what DHIS2
answered the delete, and the report of the import that landed it stays behind in forwarded/: that
document is still true of that import, and the two answer different questions. Withdrawal is terminal
by DHIS2's rule rather than by ours - the UID a tracker delete burns is refused under every import
strategy afterwards - so nothing moves a receipt back out of withdrawn/.
A fourth directory, malformed/, is a holding pen rather than a state: a file that does not read as a
receipt is moved there with its reason beside it, and the read that found it carries on. One unreadable
byte on disk must not cost the other two hundred receipts their drain, and a file skipped without being
named is a submission that has silently disappeared - so the policy is move it, name it, and continue.
A directory that cannot be read at all is a different failure and is still raised.
A conversion-refused response never moves, unless what refused it can never be fixed: entered-in-error
is a withdrawal, which this toolchain does not build, so that one is filed to rejected/ rather than
retried by every drain forever. Every other refusal has a fix in the guide or in the data, so leaving it
in received/ makes the next d2w fhir forward a retry with no bookkeeping at all. What a committing
drain does leave behind is an <id>.refusal.json beside the receipt - when the drain saw it, how many
drains have refused it, and why - so a listing can tell a receipt drains keep refusing from one no drain
has touched. The same sidecar carries the other refusal a drain makes and leaves queued: a response
whose aggregate values a forwarded receipt already sent, under [forward] overwrites = "refuse". The
marker is about the queue, so the move that finally drains the receipt deletes it: the import report
supersedes it.
The layout is duplicated rather than imported: dhis2w-fhir is a dependency of dhis2w-fhir-serve, so
the arrow only points one way and the forwarder reads the files directly under the same conventions.
Moves are os.replace within one filesystem, so a response is in exactly one state at every instant.
WHERE the tree sits is the one thing both sides share code for. [serve] spool_dir moves it, and the
server that writes a receipt and the drain that files it have to land on the same directory to the
character - so resolve_spool_root here is what both of them resolve the key through, and the
duplicated names above are the layout under that root rather than a second answer to where it is.
One drain at a time. A .drain.lock in the spool root carries an exclusive flock for the length of
a run and the draining process id inside it, because two drains over one spool would post the same
receipt twice and race each other's renames.
Classes¶
SpoolReadError
¶
Bases: LookupError
Raised when a spool directory cannot be read at all - a permission, a device, a broken mount.
Not what an unreadable file raises: one bad file is quarantined and named, because losing a whole drain to it would be the larger failure. This is the directory itself refusing.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
SpoolLockedError
¶
Bases: LookupError
Raised when another drain of the same spool holds the lock, naming the process that holds it.
SpoolState
¶
Bases: StrEnum
Which of the spool's four directories a receipt currently sits in.
Three of the four are the drain's: a receipt is captured, and DHIS2 either takes the payload or
refuses it. The fourth is an operator's, and it is the only one that is reached without posting
the receipt again - d2w fhir withdraw retracts from DHIS2 what a forwarded receipt landed, and
files the receipt here to say the submission stands withdrawn rather than unsent.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
SpoolLayout
¶
Bases: BaseModel
One project's spool on disk: the project it belongs to, and the root its receipts live under.
Carried rather than recomputed, because every move in a drain is a rename between two
directories of this layout and a second opinion about where the root is would file a receipt
where nothing looks for it. project_root rides along because a report names a spool file
relative to the project, which stays true of a root outside it - the path is simply absolute there.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Attributes¶
malformed_directory
property
¶
The holding pen for files that do not read as receipts, which is no receipt's state.
lock_path
property
¶
The lockfile one drain holds for its whole run.
Methods:¶
resolve(project_root, spool_dir=SPOOL_RELATIVE_PATH)
classmethod
¶
The layout one project's [serve] spool_dir names, defaulting to the tree inside the project.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
directory_for(state)
¶
QuarantinedFile
¶
Bases: BaseModel
One file moved to malformed/ because it does not read as a receipt, and what stopped it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
SpooledResponse
¶
Bases: BaseModel
One receipt off the spool: the response to translate, plus where its file currently sits.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Attributes¶
submitted_by = None
class-attribute
instance-attribute
¶
The DHIS2 username the facade validated the submission under, or None when it validated none.
Facade-side provenance. It says who handed this receipt to d2w fhir serve, and says nothing
about the identity the values reach DHIS2 under - a drain posts as the forwarding profile, and
storedBy on the instance is DHIS2's own stamp of that profile.
layout
instance-attribute
¶
The spool this receipt came off, which is the one that files it - see SpoolLayout.
path
instance-attribute
¶
The file this receipt was read from, which is what the lifecycle moves.
project_root
property
¶
The project this receipt belongs to, which is what a report names its path relative to.
response
instance-attribute
¶
The captured response as a model, parsed from the verbatim resource the receipt carries.
SpoolReading
¶
Bases: BaseModel
What one read of received/ found: the receipts to drain, and the files it moved aside.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
RefusalReason
¶
Bases: BaseModel
One reason a drain refused a spooled response, as the refusal record states it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
ForwardRefusalRecord
¶
Bases: BaseModel
What the last committing drain wrote beside a receipt it refused to send.
The receipt itself never moves and is never rewritten - see the module docstring - so this sidecar is the whole of the drain's mark on the queue: when it last looked, how many drains have refused the receipt, and why. Deleted by the move that finally drains the receipt.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Attributes¶
refused_at
instance-attribute
¶
The instant the last committing drain refused the receipt, as a FHIR instant (UTC).
attempt_count = 1
class-attribute
instance-attribute
¶
How many committing drains have refused this receipt so far.
line
property
¶
The record as the one line a listing row shows: the first reason, or the bare fact.
SpooledReceipt
¶
Bases: BaseModel
One receipt's envelope as a listing reads it, without translating the resource it carries.
A listing answers where a receipt is and what it answers, and neither question needs the QuestionnaireResponse parsed - so a receipt whose resource this package's models cannot read is still a row rather than a hole.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Attributes¶
submitted_by = None
class-attribute
instance-attribute
¶
The DHIS2 username the facade validated this submission under, or None when it validated none.
refusal = None
class-attribute
instance-attribute
¶
The last committing drain's refusal of this still-queued receipt, when one is on disk.
SpoolContents
¶
Bases: BaseModel
Every receipt one project's spool holds, in each state, plus what is sitting in quarantine.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Functions:¶
resolve_spool_root(project_root, spool_dir=SPOOL_RELATIVE_PATH)
¶
Where one project's spool root sits: the stated directory, against the project unless absolute.
The single answer both the server and the forwarder resolve [serve] spool_dir through. A
relative path is the ordinary case and keeps the tree inside the project the receipts belong to;
an absolute one is taken as written, which is how a spool lands on a volume the operator chose -
and everything outside the project (version control, backups) is theirs to arrange from there.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
read_received_responses(layout)
¶
Read every pending receipt of one project, in arrival order with the receipt id as the tiebreaker.
A receipt id is a random hex string, so a directory listing orders on nothing a submission means.
received_at is what a drain has to act on: DHIS2 replaces an aggregate value in place, so the
instance ends up holding whichever submission was posted last, and posting the older one last
would leave it holding the older number. Two receipts stamped the same instant order on their
ids, which is arbitrary but the same on every run, so a drain stays reproducible.
An absent spool directory is not an error - it is a project nothing has been captured into yet -
and answers with no receipts at all. A file that will not read as a receipt is moved to
malformed/ with its reason beside it and named in the reading: a drain that aborted on it
would lose every other receipt's turn to one file, and one that skipped it silently would make a
submission disappear.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
read_receipt(layout, state, response_id)
¶
Read one named receipt out of one state, resource and all, refusing by name when it is not there.
What d2w fhir withdraw reads. A drain reads a whole directory because it acts on the queue; a
withdrawal acts on one receipt an operator named, so it opens that file and nothing else - and a
file that is not there, or will not read as a receipt, is a refusal rather than a quarantine,
because the operator is standing in front of the answer.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
read_spooled_receipts(layout)
¶
Read the envelope of every receipt in every state, quarantining whatever will not read as one.
What d2w fhir spool lists. It touches no DHIS2 and parses no payload: a receipt is named by its
envelope, which is the whole of what a queue listing states.
Each state is listed in arrival order with the receipt id as the tiebreaker, which is the order
read_received_responses drains in - so what a listing shows as next in the queue is what the
next drain posts first.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
malformed_files(layout)
¶
Every file sitting in malformed/, each with the reason written beside it when it was moved.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
record_refusal(spooled, record)
¶
Write one still-queued receipt's refusal record beside it, durably, and answer with where it sits.
The receipt itself is never rewritten; the record is a sibling file the next listing reads and the move that finally drains the receipt deletes. Written atomically and fsynced the way a receipt is, because a marker that vanishes with the power is a drain that never happened.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
read_refusal_record(directory, response_id)
¶
The refusal record beside one queued receipt, or None when no committing drain has refused it.
A record that will not parse answers None rather than raising: it is the marker that got corrupted, not the receipt that got lost, so a listing still names the receipt and simply says nothing about the drain that refused it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
move_to_forwarded(spooled, report)
¶
Write one acceptance's import report beside where the receipt is going, then move the receipt there.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
write_import_report(layout, state, response_id, report)
¶
Write the import report beside one already-filed receipt, replacing whatever is there.
What a drain uses to say something further about a receipt it has already moved - the answer to a completeness registration posted after the values landed. The receipt itself is never rewritten, and the write is atomic, so a reader either sees the previous report or this one.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
read_import_reports(layout, state)
¶
Every import report of one state, as the receipt id it belongs to and the JSON text it holds.
The text rather than a model: the shape a report carries is the service's business, and this module writes the file without reading what is in it. A file that will not read at all is left out rather than raised, exactly as an unreadable sidecar is elsewhere - one file must not cost a drain its answer about the rest.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
move_to_rejected(spooled, report)
¶
Write one rejection's import report beside where the receipt is going, then move the receipt there.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
move_to_withdrawn(spooled, report)
¶
Write what DHIS2 answered the delete beside withdrawn/, then move the forwarded receipt in after it.
THE FORWARD'S OWN IMPORT REPORT STAYS IN forwarded/. It states what DHIS2 did with the payload
when it took it, which is still true of that import and is the only record of what was landed;
the document written here states what DHIS2 did when it was asked to take it back. Two answers
to two questions, and neither is rewritten - the receipt itself never was.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
move_to_received(layout, response_id)
¶
Move one rejected receipt back into the queue, and answer with where it now sits.
THE SIDECAR STAYS IN rejected/. The report states what DHIS2 answered the last time this
payload was posted, which is still true of that post and is the only record of what the receipt
was requeued from; carrying it into received/ would put a drained receipt's answer beside a
receipt nothing has yet asked about. The next drain writes a fresh report wherever the receipt
lands - overwriting the stale one in place when the answer is the same, and leaving it behind as
history when the receipt is accepted this time.
ANY REFUSAL RECORD IN received/ GOES. This is the one way a receipt enters the queue from
another state, and a marker under its name there can only be a leftover - a drain killed between
the rename that filed the receipt and the unlink that cleared its marker. The receipt is entering
the queue with no drain having refused it, so nothing beside it may say one did.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
drain_lock(layout)
¶
Hold the spool's exclusive drain lock for one run, refusing at once when another run holds it.
Two drains over one spool would translate the same receipts, post both copies, and race each
other's renames - so the second one fails rather than waits: an operator who started it by
mistake wants to be told, and one who started it deliberately wants the first run's answer
rather than a queue behind it. The lockfile carries the holding process id so the refusal names
what to look for, and the lock is an flock on an open descriptor, so the kernel releases it
whether the drain returned, raised, or was killed.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
sweep_orphan_temporary_files(layout, *, older_than_seconds=ORPHAN_TEMPORARY_FILE_AGE_SECONDS)
¶
Delete the temporary files an interrupted write left behind, and answer with what was deleted.
THE AGE GUARD IS THE WHOLE OF THE SAFETY. A temporary file being written this instant is indistinguishable from one a killed process abandoned - same name shape, same directory - and the only thing the directory can say about the difference is how long ago the file was touched. So a young one is left alone even though it is probably nothing, because deleting the file a running capture is mid-write into would turn a durable write into a lost one.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/spool.py
Values a previous submission already sent¶
The identity of an aggregate data value, and the index a drain reads out of forwarded/ to
say that a value it is about to send is one an earlier submission already sent. DHIS2 counts
that write exactly as it counts a first entry, so the spool is where the answer lives.
overwrite
¶
The aggregate values a drain sends that an earlier submission already sent, and the index that finds them.
DHIS2 cannot answer this question, so the spool does. /api/dataValueSets applies its own
CREATE_AND_UPDATE to an envelope that names no strategy, so a second send of one cell replaces the
first in place; and the import summary counts updated: 1 for a genuinely first write and for a
replacement alike (BUGS.md 85), so nothing DHIS2 answers separates the two. What separates them here
is the record every drained receipt leaves: a forwarded receipt's <id>.report.json names the cells
its payload put in the instance, and the next drain reads those records back before it sends anything.
The cell is the full DHIS2 identity of a data value - data element, category option combo, period, organisation unit, and attribute option combo - because that tuple is what DHIS2 keys a value on, and anything shorter would call two different cells the same one.
THE SIDECAR IS THE RECORD, NOT THE RECEIPT. A forwarded receipt holds the FHIR submission, which means the cells it landed on are only knowable by translating it again - against today's guide, which may no longer be the guide it was sent under. The sidecar instead states what was actually posted and taken, which is the claim this index needs, and it is one file to read rather than a translation to re-run. It records the identity of each value and never the value itself: what the payload landed on is this index's business, and what it landed is the receipt's.
THE COST. The index is built once per drain and only when that drain carries an aggregate payload, so
a tracker-only run reads nothing at all. Building it is one read per <id>.report.json in
forwarded/ - the sidecar alone, never the receipt beside it, so a forwarded receipt costs one file
open rather than two - and a sidecar is parsed straight into the cells it recorded. forwarded/ grows
without bound, which is exactly why the read is the sidecar alone and why a whole run pays it once
rather than once per receipt: a spool of a few hundred forwarded receipts is read in a small fraction
of the time a single POST to DHIS2 takes, so the index disappears into the network cost of the drain
it serves. Nothing here truncates, and nothing here samples. A spool large enough for the read to be
felt is one where forwarded/ wants archiving for its own sake, and an index that quietly stopped
reading part of it would answer "no earlier submission" about a value that has one - which is the one
answer this module must never give.
Classes¶
AggregateCell
¶
Bases: BaseModel
The full DHIS2 identity of one aggregate data value - the five keys that name exactly one cell.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
Attributes¶
category_option_combo = None
class-attribute
instance-attribute
¶
Unset where the data element rides the default category combo, which is what DHIS2 files it under.
attribute_option_combo = None
class-attribute
instance-attribute
¶
Unset where the data set rides the default category combo, which is what DHIS2 files it under.
key
property
¶
The five keys as the one string an index is keyed on, an unset key included as its absence.
line
property
¶
The five keys as the one cell a report shows, since a data value has no other name.
ForwardedSubmission
¶
Bases: BaseModel
Which forwarded receipt sent a value, and when that receipt was received.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
OverwrittenValue
¶
Bases: BaseModel
One value a drain sends that a forwarded receipt already sent, and which receipt sent it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
ForwardOverwrite
¶
Bases: BaseModel
Every value one response of a drain sends that an earlier submission had already sent.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
ForwardedValueRecord
¶
Bases: BaseModel
The part of a forwarded receipt's sidecar this index reads: the cells it landed on, and when it arrived.
A projection of the sidecar rather than the whole of it, because the index has no use for DHIS2's counts, and reading them would tie this module to the report shape the service writes.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
ForwardedCellIndex
¶
Bases: BaseModel
Which forwarded receipt last sent each aggregate value a spool has landed in DHIS2.
Mutable for the length of one drain: a receipt filed to forwarded/ mid-run has landed its
values, so a later receipt of the same run that sends them again replaces them just as surely as
one from last week does. The run records each payload as DHIS2 takes it, and the answer stays
true inside the drain as well as across drains.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
Attributes¶
covered = Field(default_factory=dict)
class-attribute
instance-attribute
¶
The submission that last sent each cell, keyed by AggregateCell.key.
receipts_read = 0
class-attribute
instance-attribute
¶
How many forwarded sidecars this index read, which is what its cost is counted in.
receipts_without_values = 0
class-attribute
instance-attribute
¶
Aggregate receipts in forwarded/ whose sidecar records no cells, so this index cannot see them.
Methods:¶
already_sent(cells)
¶
Every cell of one payload a forwarded receipt already sent, in the order the payload names them.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
record(cells, submission)
¶
Record one submission as the sender of these cells, keeping the latest sender of each.
The receipt named for a cell is the last one to have sent it, because that is the submission
whose number the instance is holding. Arrival time decides, since forwarded/ is read in
file-name order and a receipt id is a random hex string that orders on nothing.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
Functions:¶
aggregate_cells(data_value_set)
¶
The full identity of every data value one /api/dataValueSets envelope carries.
A data value may name its own period, organisation unit, and attribute option combo, and DHIS2 reads the envelope's as the default for the ones it does not - so a cell is read the same way round here, the value's own key first and the envelope's behind it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
build_forwarded_cell_index(layout)
¶
Read every forwarded receipt's sidecar into the index of what this spool has already landed.
Only forwarded/ counts. A rejected receipt never landed its values, so it covers nothing, and a
receipt still in the queue has not been sent at all.
A sidecar that will not read is counted rather than raised: one unreadable file must not cost a drain its answer about the other two hundred, and the count is what keeps that silence visible.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/overwrite.py
A client for a running facade¶
FacadeClient is the typed path to a d2w fhir serve facade: construct it against a base
url, hand submit_response a filled form, and read the receipt id off what comes back. The
facade publishes no OpenAPI document - /metadata is its contract - so this client is
hand-written against the routes it mounts rather than generated from a schema.
Three shapes are worth knowing before the first call. A create answers an
OperationOutcome and not the resource, so the id of an accepted submission is the last
segment of the Location header - CaptureReceipt carries both, plus the warnings the
answer stated. A 201 means the facade stored the submission durably, not that anything
reached DHIS2; d2w fhir forward is what drains the queue. And an expression that will not
parse is a 200 carrying its line and column, so evaluate answers an EvaluationOutcome
rather than raising - FacadeError is reserved for a request the facade cannot serve at all.
from dhis2w_fhir import FacadeClient
async with FacadeClient("http://127.0.0.1:8123") as facade:
draft = await facade.generate("BfMAe6Itzgt", seed=20260)
receipt = await facade.submit_response(draft)
print(receipt.response_id, receipt.note)
stored = await facade.read_response(receipt.response_id)
Five runnable examples cover the surface, one method group each.
send_with_the_client.py
is the write half above, end to end.
search_with_the_client.py
is the read half - canonical_resource_types read off /metadata, a typed
ResourceQuery against the published forms and against the live register, and
resolve turning a canonical url into whichever type holds it.
evaluate_with_the_client.py
runs FHIRPath over both evaluation contexts and shows the diagnostic an
unparseable expression is answered with.
authenticate_with_the_client.py
presents a BearerToken to a facade started with --auth token, beside the 401
a caller with no credential and a caller with the wrong one get. And
handle_refusals_with_the_client.py
reads FacadeError itself: status_code, one typed issue per thing wrong, and
diagnostics for the log line that has to say why in one.
facade
¶
A typed async client for a d2w fhir serve facade: read it, fill a form, submit a capture, evaluate an expression.
This client is hand-written against the routes dhis2w_fhir_serve.routes mounts. The base URL is a
FHIR endpoint whose contract is the CapabilityStatement at /metadata and which publishes no OpenAPI
document at all, so a generated client was never on offer for the surface most of these methods read.
The facade's own API under /facade does publish one, at /facade/openapi.json - it is the contract
to read when writing against those endpoints directly, and evaluate below is this client's one
method that calls them. Every method here answers a model rather than a parsed document, and every
refusal arrives as a FacadeError carrying the OperationOutcome the facade stated its reason in.
WHAT THIS IS FOR. An integrator holding a filled form wants three lines, not a request builder:
construct a FacadeClient, hand submit_response the document, read the receipt id off what comes
back. generate and read_response close that loop, so a caller can prove a form is answerable
before writing a line of form-filling code - $generate output is postable at the same server,
unchanged, which is the invariant that operation exists for.
THE ID IS NOT IN THE BODY. R4 says a create answers an OperationOutcome, so the identity of an
accepted submission is the last segment of the Location header and the body is the server saying
what it did. CaptureReceipt carries both, which is why submit_response answers a receipt rather
than the OperationOutcome on its own: a caller reading only the body would find no id on any
capture the facade ever accepts.
A RECEIPT IS NOT A DHIS2 WRITE. A 201 means the facade understood the submission, checked it against
the published form, and wrote it to disk durably. Nothing has reached DHIS2 - d2w fhir forward is
what drains the queue into an instance, and until it runs the receipt is a promise about bytes the
facade is holding.
THE EVALUATION SHAPES ARE MIRRORED, NOT IMPORTED. POST /facade/evaluate is the facade's own endpoint
and its request and response models are defined in dhis2w_fhir_serve.evaluation and
dhis2w_fhir_serve.routes.evaluate, which this package cannot import: dhis2w-fhir-serve depends on
dhis2w-fhir, so the arrow points one way only. EvaluationRequest, EvaluationOutcome, and the
three context shapes below mirror those definitions field for field, and those two modules are the
source of truth for them.
Attributes¶
EvaluationContext = Annotated[StoredResourceContext | InlineResourceContext | RegisteredEntityContext, Field(discriminator='kind')]
module-attribute
¶
What an evaluation runs over, discriminated on kind exactly as the facade discriminates it.
Classes¶
FacadeCredential
¶
Bases: BaseModel
What a caller presents to a guarded facade, as the one Authorization value it becomes.
The facade runs under one of four postures - none, token, dhis2, jwt - and three of them
read a credential. A subclass exists per scheme rather than per posture, because token and
jwt both take a bearer token and differ only in who minted it, which is the server's business
and not the caller's.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
BearerToken
¶
Bases: FacadeCredential
A token the token posture holds in D2W_FHIR_SERVE_TOKENS, or one the jwt posture's issuer minted.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
UsernamePassword
¶
Bases: FacadeCredential
DHIS2 credentials the dhis2 posture replays against GET /api/me on the instance it reads.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
PersonalAccessToken
¶
Bases: FacadeCredential
A DHIS2 personal access token, which the dhis2 posture replays exactly as dhis2w-client sends it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
FacadeError
¶
Bases: Exception
Raised when the facade refuses a request, carrying the OperationOutcome it refused with.
Every refusal this facade makes renders as an OperationOutcome - register_error_handlers
wires that for the whole application, so a 404 on a resource and a 400 on a search parameter
arrive in the same shape. A capture refused at 400 or 422 carries one issue per thing wrong with
the submission, which is why issues is a tuple and not one value.
A connection that never reached the facade is not this: httpx2's own TransportError passes
through untouched, because a server that did not answer stated no outcome to carry.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Attributes¶
issues
property
¶
Every issue the refusal named, empty when the body carried no OperationOutcome.
diagnostics
property
¶
Every issue's own words, joined - what to print when one line has to say why.
Methods:¶
__init__(status_code, method, url, outcome, body_text)
¶
Capture the status, the request that drew it, and the outcome the facade answered with.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
CaptureReceipt
¶
Bases: BaseModel
What an accepted submission answers: where the receipt lives, and what the facade noted about it.
response_id is the last segment of Location rather than anything in the body, because the
body is an OperationOutcome and carries no id. note is the server's own "stored response
{id}; a stored response is the submission as received" line, and warnings is every other issue
the answer carried - a submission can be accepted and still have something worth saying about it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Attributes¶
response_id
instance-attribute
¶
The id the receipt is served from, which read_response reads it back by.
location
instance-attribute
¶
The absolute url the facade said the receipt lives at, verbatim from the Location header.
note = None
class-attribute
instance-attribute
¶
The informational issue the facade always answers with, saying what it stored.
warnings = ()
class-attribute
instance-attribute
¶
Everything else the answer carried - accepted, and worth reading.
outcome
instance-attribute
¶
The whole answer, for a caller that wants the issues in the order they were stated.
ResourceQuery
¶
Bases: BaseModel
The search parameters this facade answers, named rather than spelled.
The facade honours a small, fixed set and treats the rest two different ways: a store search IGNORES a parameter it does not know, and a register search REFUSES one. So a free-form mapping of query parameters is a quiet way to get the wrong answer on one route and a 400 on the other, and this model can only express something the facade actually reads.
Not every parameter answers on every type. _id, url, and identifier answer on the eleven
conformance types; QuestionnaireResponse answers _id and questionnaire and is paged;
the live register types answer identifier, _tag, d2-attribute, and _content.
Several values in one field are sent comma-separated, which is how the facade spells OR within one parameter. A value carrying a comma of its own cannot be expressed that way.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Attributes¶
ids = ()
class-attribute
instance-attribute
¶
_id - the resource ids to match.
urls = ()
class-attribute
instance-attribute
¶
url - the canonical urls to match, on the conformance types that declare it.
identifiers = ()
class-attribute
instance-attribute
¶
identifier - system|value tokens; a bare value matches any system.
questionnaire = None
class-attribute
instance-attribute
¶
questionnaire - the form canonical a receipt answers, matched exactly.
tags = ()
class-attribute
instance-attribute
¶
_tag - a register search's tag filter.
attribute_filters = ()
class-attribute
instance-attribute
¶
d2-attribute - a register search's DHIS2 tracked entity attribute filter.
text = None
class-attribute
instance-attribute
¶
_content - free-text search, answerable only where the projection backend is configured.
count = None
class-attribute
instance-attribute
¶
_count - a cap on the entries returned, not a page size; Bundle.total stays the whole set.
page = None
class-attribute
instance-attribute
¶
page - an opaque cursor the server minted; compose one only by following a Bundle's next link.
Methods:¶
to_query_parameters()
¶
This query as the parameter pairs the facade reads, omitting everything left unset.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
EvaluationLanguage
¶
Bases: StrEnum
Which of the three languages a source is written in - mirrors dhis2w_fhir_serve.evaluation.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
DiagnosticKind
¶
Bases: StrEnum
Whether the source never parsed, or parsed and then refused to run.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
StoredResourceContext
¶
Bases: BaseModel
Evaluate over a resource the facade already holds, named by type and id.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
InlineResourceContext
¶
Bases: BaseModel
Evaluate over a resource carried in the request itself - the expression is checked against exactly it.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Methods:¶
over(resource)
classmethod
¶
Evaluate over this resource, whether the caller holds a typed R4 model or a parsed document.
The field itself is a JsonResource, because that is what goes on the wire. This is the
constructor a caller reaches for: an integrator holding a QuestionnaireResponse and one
holding the document they just parsed are both one call from an evaluation.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
RegisteredEntityContext
¶
Bases: BaseModel
Evaluate over a person the live register holds, named by their DHIS2 tracked entity uid.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
EvaluationRequest
¶
Bases: BaseModel
One evaluation as a caller asks for it - mirrors dhis2w_fhir_serve.routes.evaluate.EvaluationRequest.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Attributes¶
source
instance-attribute
¶
The FHIRPath expression, the CQL library text, or the ELM library as JSON.
expression_name = None
class-attribute
instance-attribute
¶
Which define to answer. Omitted, a CQL or ELM library answers every define it declares.
context = None
class-attribute
instance-attribute
¶
The resource to evaluate over. Omitted, the expression runs over no resource at all.
EvaluationDiagnostic
¶
Bases: BaseModel
One thing that stopped the run, at the position the parser stated one.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
EvaluationResult
¶
Bases: BaseModel
What one expression or one define answered, as JSON.
values is always a collection, because FHIRPath answers collections and a CQL define answering
a single value is that value carried as one. An empty collection is an answer - the expression
matched nothing - and is not the same state as a refusal.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
EvaluationOutcome
¶
Bases: BaseModel
One evaluation, whole: what it answered, what it declares, and what stopped it.
A source that will not parse is a 200 carrying a diagnostic, never a refusal - which is why
evaluate answers this rather than raising on a bad expression. FacadeError is reserved for a
request the facade cannot serve at all: a stored resource it does not hold, a register it does
not publish.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
FacadeClient
¶
A typed async client for one d2w fhir serve facade.
Use it as an async context manager and it owns its connection pool; hand it an
httpx2.AsyncClient and it borrows that one, leaving it open at exit - which is what a caller
pooling several clients, or a test driving the application in-process, wants.
Every request carries Accept: application/fhir+json, except POST /facade/evaluate, which is
on the facade's own API rather than the FHIR surface and answers plain JSON. Every non-2xx raises
FacadeError.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
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 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 | |
Attributes¶
base_url
property
¶
The facade this client talks to, without a trailing slash.
Methods:¶
__init__(base_url, *, auth=None, timeout=30.0, http_client=None)
¶
Point a client at a facade, optionally with a credential and a pool to borrow.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
__aenter__()
async
¶
__aexit__(exc_type, exc, traceback)
async
¶
Close the pool this client opened.
aclose()
async
¶
Close the pool this client opened; a pool the caller supplied is left for the caller to close.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
capability(*, refresh=False)
async
¶
The facade's /metadata - its only contract, read once and held unless refresh asks again.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
read(resource_type, resource_id)
async
¶
One resource by type and id, carried as the document the facade served.
A stored resource is answered verbatim, so which keys it has is the document's business
rather than this client's - JsonResource keeps every one of them. read_response is the
typed path for the one type a caller reads back most.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
read_response(response_id)
async
¶
One receipt by id - the submission exactly as it arrived, not a view of anything DHIS2 holds.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
search(resource_type, query=None)
async
¶
One searchset Bundle for a resource type.
Bundle.total is the whole match set and entry is what this request asked for, so a
capped search answers a total larger than the entries it carries.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
resolve(canonical)
async
¶
The one resource a canonical url names, looked for across every type that declares url.
A canonical says what a resource is without saying where it lives, so resolving one means
asking each type in turn. None is an answer: this facade publishes nothing under that url.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
canonical_resource_types()
async
¶
Every resource type this facade's /metadata declares a url search parameter on.
Read off the CapabilityStatement rather than hard-coded, because the facade lists a type only
when its store actually holds instances of it - a project publishing no ConceptMap does not
declare one, and resolve should not ask.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
generate(questionnaire_id, *, seed=None)
async
¶
A filled draft answering one published form, against that form's own rules.
seed makes the draft byte-reproducible. What comes back is postable to submit_response
unchanged, which is the invariant this operation exists for.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
submit_response(questionnaire_response)
async
¶
Submit one filled form and answer the receipt the facade handed back.
Takes a model or a parsed document, because an integrator holding either should be one call
away. A submission the facade will not accept raises FacadeError carrying one issue per
thing wrong with it, which is what a capture screen renders.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
evaluate(language, source, *, expression_name=None, context=None)
async
¶
Evaluate one FHIRPath expression, CQL library, or compiled ELM library against this facade.
A source that will not parse answers an outcome carrying the line and column the parser
stopped on - it does not raise. FacadeError means the facade could not serve the request
at all, which is a different thing from an expression it could not run.
Source code in packages/dhis2w-fhir/src/dhis2w_fhir/facade.py
Functions:¶
Package surface¶
The names below re-export from dhis2w_fhir itself; the
generate page covers what each emitter
produces.
dhis2w_fhir
¶
Version-neutral FHIR IG generation: fhir.toml config, FSH emission, and project scaffolding.
Each component owns its schemas; this module is the one stable import
surface over them, so from dhis2w_fhir import GenerateConfig keeps working
however the components are arranged internally.