Receive captures¶
Who this is for: the developer whose middleware takes submissions from another system and lands them in DHIS2.
Before you start: Build a middleware, and what a valid submission is: The capture contract.
You will be able to:
- submit a capture, and hear what DHIS2 said
- do the same work one step at a time: check it against the published forms, translate it, and post it
- keep it durably through a spool, and drain that into DHIS2
The runnable version of this page is steps 2 to 5 of
examples/facade/.
If you mount the routes, POST /QuestionnaireResponse does all of this for you: see
Mount the routes. This page is the same work as calls you make yourself, in a queue worker or a
route of your own.
Submit¶
submit is the one call to reach for. It validates a submission, posts it to DHIS2 at once, and says what became of it.
from dhis2w_fhir_engine.facade import DeliveryKind
from dhis2w_fhir_engine.facade.refusals import ServeError
try:
outcome = await facade.submit(body, submitted_by="an-integration")
except ServeError as refusal:
print(refusal) # DHIS2's reasons. Nothing was stored, and the sender is who can fix it
else:
print(outcome.kind is DeliveryKind.SENT, outcome.note)
outcome.kind |
What happened | What to tell the sender |
|---|---|---|
sent |
DHIS2 took it, and the receipt is filed as forwarded | Done |
held |
DHIS2 could not be reached, or this facade holds no connection. The receipt is in the queue | Not yet sent to DHIS2 |
Refusals raise, because they are the submitter's to hear. CaptureRejection is a submission the guide refuses, and is
what Validate describes. These three share ServeError, and store nothing:
| Raised | Why |
|---|---|
CaptureRefusedByInstanceError |
DHIS2 refused it, and status_code is what DHIS2 answered |
CaptureNotTranslatedError |
The guide accepts it and the translator cannot turn it into a DHIS2 payload |
CaptureOverwriteRefusedError |
It holds values already sent, and the project states [forward] overwrites = "refuse" |
Something has to send what is held. submit keeps a capture DHIS2 was away for, and leaves it. Call
drain on your own schedule, or have the facade try the queue until it closes:
deliverer = facade.deliverer()
facade.keep_delivering(deliverer) # it needs a running event loop
outcome = await deliverer.deliver(facade.validate_capture(body))
The rest of this page is the same work one step at a time.
Validate¶
validate_capture is what POST /QuestionnaireResponse runs. A refusal is a typed exception a service can answer however
it answers refusals.
from dhis2w_fhir_engine.facade.capture.outcome import CaptureRejection
try:
validated = facade.validate_capture(body)
except CaptureRejection as rejection:
print(rejection.http_status, rejection.issues[0].diagnostics)
$ uv run python examples/facade/step2_validate_a_capture.py
10 forms published, first: ANC follow-up
accepted as a tracker capture, 0 warning(s)
refused: the request body names no `resourceType`; this endpoint receives a QuestionnaireResponse
facade.forms() lists what the guide publishes, and facade.synthesize(form) draws a valid response to one, which is what
$generate answers. They are how to have a capture in hand before writing any form-filling code.
Translate¶
Translation is a pure function of the response and the guide. DHIS2 is not asked, and a refusal is about the response alone.
result = facade.translate(response)
if result.is_refused:
print([refusal.reason for refusal in result.refusals])
else:
print(result.target_kind, result.payload)
Post, and read the verdict¶
post picks the endpoint from the payload's shape and reads whichever report that endpoint answers with. A dry run is the
default and a real call: DHIS2's own rules decide, under the endpoint's validate-only mode, and nothing is written.
outcome = await facade.post(result) # validate only
outcome = await facade.post(result, dry_run=False) # commits
Read outcome.verdict. A refused import is an outcome to read rather than an exception:
outcome.verdict |
What happened | What to do |
|---|---|---|
accepted |
DHIS2's own report arrived and names no refusal | Done |
rejected |
DHIS2's own report refuses it, and outcome.issues says why |
Tell the sender: the fix is in the data |
none |
Something answered that was no import report: a 401, a 403, a 429 | Keep the capture and try again. This says nothing about the payload |
A 5xx raises Dhis2ApiError, because DHIS2 failing is about the run rather than about this capture. Do not use
outcome.is_rejected to decide this: it is also true of a 401 carrying an error status.
Spool, then drain¶
capture validates and writes a receipt durably, so a submission survives DHIS2 being away. drain is what
d2w fhir forward runs: it translates every receipt, posts each, and files what became of it.
envelope = facade.capture(body, submitted_by="an-integration")
report = await facade.drain() # validate only
report = await facade.drain(import_responses=True) # commits, and moves each receipt
$ uv run python examples/facade/step5_spool_and_drain.py
receipt dc905809bdc4470e80a2e68f0e47d278 for ANC follow-up
receipt 342ae11492b0492c9cf5b79792ebd9b8 for ANC follow-up - ANC visit
2 spooled, 2 translated, 0 refused, 2 posted (validate only), 1 accepted, 0 rejected, 1 unverifiable
dc905809bdc4470e80a2e68f0e47d278: accepted
342ae11492b0492c9cf5b79792ebd9b8: unverifiable
unverifiable is a dry run being honest: that event belongs to an enrollment the first capture would create, and a dry run
created nothing. capture blocks on an fsync, so run it off the event loop inside an async route.
The drain posts as the connection's account, whoever submitted. See Who can do what. Repair the capture queue explains the states a receipt moves through, and they are the same files here.
Next: Read forms and people.