dirigent-integration 0.27.0__tar.gz → 0.28.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/PKG-INFO +7 -6
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/README.md +5 -4
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/pyproject.toml +2 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/pyproject.toml.orig +2 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-analytics-to-csv-report.yaml +4 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-export-to-s3.yaml +1 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-tracker-weekly-window.yaml +1 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-values-per-org-unit-to-parquet.yaml +10 -6
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-values-to-parquet.yaml +4 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/README.md +1 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/dhis2-to-fhir-observations.yaml +8 -4
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-capture-bundle-to-data-values.yaml +21 -13
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-conceptmap-driven-mapping.yaml +5 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-encounter-to-event.yaml +8 -6
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-measure-report-to-analytics-check.yaml +19 -7
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-nightly-window-sync.yaml +27 -17
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-patient-to-tracked-entity.yaml +4 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-questionnaire-response-to-data-values.yaml +11 -8
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/fhir/fhir-subscription-to-dhis2.yaml +5 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/README.md +1 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/csv-drop-to-data-values.yaml +5 -2
- dirigent_integration-0.27.0/src/dirigent_integration/shelves/inbound/endpoint-payload-to-tracker-event.yaml → dirigent_integration-0.28.0/src/dirigent_integration/shelves/inbound/endpoint-payload-to-data-values.yaml +10 -7
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/fan-out-per-facility-import.yaml +8 -6
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/fhir-observations-to-data-values.yaml +10 -7
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/http-json-to-data-values.yaml +6 -7
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/outside-to-dhis2-with-checks.yaml +5 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/parquet-lakehouse-to-dhis2.yaml +5 -2
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/inbound/weekly-window-pull-and-import.yaml +5 -3
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/parquet-to-dhis2-import.yaml +1 -1
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/LICENSE +0 -0
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/__init__.py +0 -0
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/py.typed +0 -0
- {dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/shelves/dhis2-metadata-snapshot.yaml +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirigent-integration
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.0
|
|
4
4
|
Summary: The control center for the dirigent plugin ecosystem: assemble every pack, run everyone's tests and examples together.
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
License-File: LICENSE
|
|
7
|
-
Requires-Dist: dirigent-plugin==0.
|
|
7
|
+
Requires-Dist: dirigent-plugin==0.28.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
|
|
@@ -139,15 +139,16 @@ Most of them read a `d2w fhir serve` facade, which publishes one DHIS2 instance
|
|
|
139
139
|
endpoint and takes captures back; where a general-purpose FHIR server is the point -- an
|
|
140
140
|
`Encounter`, an `Observation`, a `Subscription`, a `_lastUpdated` search, none of which that
|
|
141
141
|
facade serves -- the default is a public HAPI R4 sandbox instead, and the document's header
|
|
142
|
-
says so. Every write is a dry run: `dryRun=true` on `/api/dataValueSets`,
|
|
143
|
-
on `/api/tracker`.
|
|
142
|
+
says so. Every write to DHIS2 is a dry run: `dryRun=true` on `/api/dataValueSets`,
|
|
143
|
+
`importMode=VALIDATE` on `/api/tracker`. The one real write as shipped is
|
|
144
|
+
`dhis2-to-fhir-observations`, which publishes to that public sandbox.
|
|
144
145
|
|
|
145
146
|
[`examples/fhir/README.md`](examples/fhir/README.md) has the one-line table and the mapping
|
|
146
147
|
vocabulary the shelf is built on -- subject, capture, ConceptMap.
|
|
147
148
|
|
|
148
149
|
- **`examples/fhir/fhir-capture-bundle-to-data-values.yaml`** -- the capture pair end to end: a page
|
|
149
150
|
of captures pulled off the facade, translated, gated on the pack's own id and period formats,
|
|
150
|
-
rehearsed and imported.
|
|
151
|
+
rehearsed and imported, the import a dry run too until `commit_dry_run` is false.
|
|
151
152
|
- **`examples/fhir/fhir-questionnaire-response-to-data-values.yaml`** -- the general case, where a
|
|
152
153
|
`linkId` is a form designer's name and the lookup is the integration.
|
|
153
154
|
- **`examples/fhir/fhir-patient-to-tracked-entity.yaml`** -- the `Patient` register folded into an
|
|
@@ -205,7 +206,7 @@ export GITHUB_TOKEN=$(gh auth token) # only if a component repository is priva
|
|
|
205
206
|
### Running the stack
|
|
206
207
|
|
|
207
208
|
```sh
|
|
208
|
-
cp .env.example .env # set DIRIGENT_SECRET_KEY and
|
|
209
|
+
cp .env.example .env # set DIRIGENT_SECRET_KEY, DIRIGENT_BOOTSTRAP_ADMIN_PASSWORD and POSTGRES_PASSWORD
|
|
209
210
|
make up
|
|
210
211
|
```
|
|
211
212
|
|
|
@@ -129,15 +129,16 @@ Most of them read a `d2w fhir serve` facade, which publishes one DHIS2 instance
|
|
|
129
129
|
endpoint and takes captures back; where a general-purpose FHIR server is the point -- an
|
|
130
130
|
`Encounter`, an `Observation`, a `Subscription`, a `_lastUpdated` search, none of which that
|
|
131
131
|
facade serves -- the default is a public HAPI R4 sandbox instead, and the document's header
|
|
132
|
-
says so. Every write is a dry run: `dryRun=true` on `/api/dataValueSets`,
|
|
133
|
-
on `/api/tracker`.
|
|
132
|
+
says so. Every write to DHIS2 is a dry run: `dryRun=true` on `/api/dataValueSets`,
|
|
133
|
+
`importMode=VALIDATE` on `/api/tracker`. The one real write as shipped is
|
|
134
|
+
`dhis2-to-fhir-observations`, which publishes to that public sandbox.
|
|
134
135
|
|
|
135
136
|
[`examples/fhir/README.md`](examples/fhir/README.md) has the one-line table and the mapping
|
|
136
137
|
vocabulary the shelf is built on -- subject, capture, ConceptMap.
|
|
137
138
|
|
|
138
139
|
- **`examples/fhir/fhir-capture-bundle-to-data-values.yaml`** -- the capture pair end to end: a page
|
|
139
140
|
of captures pulled off the facade, translated, gated on the pack's own id and period formats,
|
|
140
|
-
rehearsed and imported.
|
|
141
|
+
rehearsed and imported, the import a dry run too until `commit_dry_run` is false.
|
|
141
142
|
- **`examples/fhir/fhir-questionnaire-response-to-data-values.yaml`** -- the general case, where a
|
|
142
143
|
`linkId` is a form designer's name and the lookup is the integration.
|
|
143
144
|
- **`examples/fhir/fhir-patient-to-tracked-entity.yaml`** -- the `Patient` register folded into an
|
|
@@ -195,7 +196,7 @@ export GITHUB_TOKEN=$(gh auth token) # only if a component repository is priva
|
|
|
195
196
|
### Running the stack
|
|
196
197
|
|
|
197
198
|
```sh
|
|
198
|
-
cp .env.example .env # set DIRIGENT_SECRET_KEY and
|
|
199
|
+
cp .env.example .env # set DIRIGENT_SECRET_KEY, DIRIGENT_BOOTSTRAP_ADMIN_PASSWORD and POSTGRES_PASSWORD
|
|
199
200
|
make up
|
|
200
201
|
```
|
|
201
202
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-integration"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.0"
|
|
4
4
|
description = "The control center for the dirigent plugin ecosystem: assemble every pack, run everyone's tests and examples together."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
license = "LicenseRef-Proprietary"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
|
-
dependencies = ["dirigent-plugin==0.
|
|
9
|
+
dependencies = ["dirigent-plugin==0.28.0"]
|
|
10
10
|
|
|
11
11
|
[project.entry-points."dirigent.plugins.v1"]
|
|
12
12
|
integration = "dirigent_integration:plugin"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dirigent-integration"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.28.0"
|
|
4
4
|
description = "The control center for the dirigent plugin ecosystem: assemble every pack, run everyone's tests and examples together."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -10,7 +10,7 @@ license-files = ["LICENSE"]
|
|
|
10
10
|
# A published install carries the cross-boundary shelves and nothing else: each document
|
|
11
11
|
# names the packs it needs under requires, and the Examples screen marks what is missing.
|
|
12
12
|
dependencies = [
|
|
13
|
-
"dirigent-plugin==0.
|
|
13
|
+
"dirigent-plugin==0.28.0",
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
# dirigent-testing is dev-only: the packs' suites import it, and nothing at runtime does,
|
|
@@ -38,7 +38,10 @@ format: dirigent/v1
|
|
|
38
38
|
kind: pipeline
|
|
39
39
|
code: dhis2-analytics-to-csv-report
|
|
40
40
|
name: An analytics grid as a csv report
|
|
41
|
-
description:
|
|
41
|
+
description: |
|
|
42
|
+
Turn a DHIS2 analytics query into a csv report in S3 and announce it.
|
|
43
|
+
|
|
44
|
+
The analytics grid is flattened into rows before the csv is written.
|
|
42
45
|
|
|
43
46
|
tags: [dhis2, s3, csv, cross-boundary]
|
|
44
47
|
|
|
@@ -24,7 +24,7 @@ format: dirigent/v1
|
|
|
24
24
|
kind: pipeline
|
|
25
25
|
code: dhis2-export-to-s3
|
|
26
26
|
name: Export a data value set to S3
|
|
27
|
-
description:
|
|
27
|
+
description: Export a DHIS2 data value set to object storage and archive a dated copy.
|
|
28
28
|
|
|
29
29
|
tags: [dhis2, s3, cross-boundary]
|
|
30
30
|
|
|
@@ -46,7 +46,7 @@ format: dirigent/v1
|
|
|
46
46
|
kind: pipeline
|
|
47
47
|
code: dhis2-tracker-weekly-window
|
|
48
48
|
name: A week of tracker events, as parquet
|
|
49
|
-
description:
|
|
49
|
+
description: Archive the tracker events of the week that just closed as parquet.
|
|
50
50
|
|
|
51
51
|
tags: [dhis2, parquet, s3, schedule, cross-boundary]
|
|
52
52
|
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
# WHY ONE TABLE AND NOT ONE FILE PER ORG UNIT. A value moves through step outputs, and a value
|
|
10
10
|
# leaves a run through storage.write; a block does not write storage itself. A fan-out step
|
|
11
11
|
# stores one output -- the list of its items' outputs, in item order, with a failed item absent
|
|
12
|
-
# from it
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# from it. A second fan could adopt the export's grid with `for_each: ${steps.export.items}` and
|
|
13
|
+
# write one file per org unit, which is what dhis2-export-per-org-unit.yaml in the dhis2 pack
|
|
14
|
+
# does. A query over the whole extraction wants one table, so here the fan-in is a single step
|
|
15
|
+
# over every export, and the org unit is read out of each data value where the join key can be
|
|
16
|
+
# seen, rather than out of a storage path.
|
|
17
17
|
#
|
|
18
18
|
# What happens, hop by hop:
|
|
19
19
|
#
|
|
@@ -51,7 +51,11 @@ format: dirigent/v1
|
|
|
51
51
|
kind: pipeline
|
|
52
52
|
code: dhis2-values-per-org-unit-to-parquet
|
|
53
53
|
name: A parquet table over a batch of org units
|
|
54
|
-
description:
|
|
54
|
+
description: |
|
|
55
|
+
Export DHIS2 values per org unit into one parquet table, with a manifest.
|
|
56
|
+
|
|
57
|
+
Each organisation unit gets its own data value set export, the batch is flattened into one
|
|
58
|
+
table, and the manifest says what landed.
|
|
55
59
|
|
|
56
60
|
tags: [dhis2, parquet, s3, cross-boundary]
|
|
57
61
|
|
|
@@ -21,7 +21,10 @@ format: dirigent/v1
|
|
|
21
21
|
kind: pipeline
|
|
22
22
|
code: dhis2-values-to-parquet
|
|
23
23
|
name: Export a data value set as parquet
|
|
24
|
-
description:
|
|
24
|
+
description: |
|
|
25
|
+
Save a DHIS2 data value set to object storage and convert it to parquet.
|
|
26
|
+
|
|
27
|
+
The parquet file is written beside the stored data value set.
|
|
25
28
|
|
|
26
29
|
tags: [dhis2, parquet, s3, cross-boundary]
|
|
27
30
|
|
|
@@ -53,7 +53,7 @@ option set is in the regenerated map, and the pipeline that reads it needs no ed
|
|
|
53
53
|
|
|
54
54
|
| File | What it teaches |
|
|
55
55
|
| --- | --- |
|
|
56
|
-
| [fhir-capture-bundle-to-data-values.yaml](fhir-capture-bundle-to-data-values.yaml) | The capture pair end to end: a page of `QuestionnaireResponse` captures pulled off the facade, translated to `/api/dataValueSets`, gated on the pack's `dhis2-uid` and `dhis2-period` formats, rehearsed and then imported. |
|
|
56
|
+
| [fhir-capture-bundle-to-data-values.yaml](fhir-capture-bundle-to-data-values.yaml) | The capture pair end to end: a page of `QuestionnaireResponse` captures pulled off the facade, translated to `/api/dataValueSets`, gated on the pack's `dhis2-uid` and `dhis2-period` formats, rehearsed and then imported, the import a dry run too until `commit_dry_run` is false. |
|
|
57
57
|
| [fhir-questionnaire-response-to-data-values.yaml](fhir-questionnaire-response-to-data-values.yaml) | The general case, where a `linkId` is a form designer's name rather than a DHIS2 uid: an explicit lookup in params, and every unmapped answer counted rather than dropped in silence. |
|
|
58
58
|
| [fhir-patient-to-tracked-entity.yaml](fhir-patient-to-tracked-entity.yaml) | The `Patient` register searched by identifier and folded into an `/api/tracker` registration -- and why the write is an `http.request` on the `dhis2` connection, because `dhis2.tracker` reads and does not write. |
|
|
59
59
|
| [fhir-encounter-to-event.yaml](fhir-encounter-to-event.yaml) | Cardinalities that do not line up: one `Encounter` plus every `Observation` naming it, folded into the single program stage event DHIS2 wants, with the LOINC codes mapped and the rest reported. |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# The other direction: DHIS2 aggregate values published as a FHIR Bundle of Observations.
|
|
2
2
|
#
|
|
3
3
|
# THE FLOW, END TO END. Export one data value set from DHIS2, turn every data value into an
|
|
4
|
-
# Observation, wrap them in a transaction Bundle, and POST that Bundle to a FHIR server.
|
|
4
|
+
# Observation, wrap them in a transaction Bundle, and POST that Bundle to a FHIR server. As
|
|
5
|
+
# written that server is a public HAPI sandbox, and the POST is real: this one is not a rehearsal.
|
|
5
6
|
#
|
|
6
7
|
# WHY IT IS HERE. Every other document on this shelf reads FHIR and writes DHIS2. This one is
|
|
7
8
|
# the mirror, and a reader who has both in front of them can see what is symmetric and what is
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
# WHAT LEAVES. A transaction Bundle, one entry per data value:
|
|
22
23
|
#
|
|
23
24
|
# {"resourceType": "Bundle", "type": "transaction",
|
|
24
|
-
# "entry": [{"request": {"method": "
|
|
25
|
+
# "entry": [{"request": {"method": "PUT",
|
|
26
|
+
# "url": "Observation?identifier=http://dhis2.org/fhir/id/data-value|s46m5MS0hxu.Prlt0C1RF0s-202608-vSbt6cezomG"},
|
|
25
27
|
# "resource": {
|
|
26
28
|
# "resourceType": "Observation", "status": "final",
|
|
27
29
|
# "identifier": [{"system": "http://dhis2.org/fhir/id/data-value",
|
|
@@ -61,8 +63,10 @@ kind: pipeline
|
|
|
61
63
|
code: dhis2-to-fhir-observations
|
|
62
64
|
name: DHIS2 data values to FHIR Observations
|
|
63
65
|
description: |
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
Publish DHIS2 data values to a FHIR server as a bundle of Observations.
|
|
67
|
+
|
|
68
|
+
A DHIS2 data value set is exported and published as a transaction `Bundle` of `Observation`
|
|
69
|
+
resources.
|
|
66
70
|
|
|
67
71
|
The outbound half of this shelf. Each entry is a conditional update keyed on the aggregate
|
|
68
72
|
cell's natural key -- `(dataElement.categoryOptionCombo, period, orgUnit)` -- so publishing
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# THE FLOW, END TO END. `d2w fhir serve` publishes one DHIS2 instance as a FHIR endpoint and
|
|
4
4
|
# takes captures back. This document reads the captures it is holding for one aggregate form,
|
|
5
5
|
# turns them into DHIS2's own `/api/dataValueSets` envelope, gates that envelope on DHIS2's id
|
|
6
|
-
# and period formats, rehearses the import as a dry run, and only then
|
|
6
|
+
# and period formats, rehearses the import as a dry run, and only then sends it for real -- which
|
|
7
|
+
# is a dry run too until `commit_dry_run` is set to false, because the demo is shared.
|
|
7
8
|
#
|
|
8
9
|
# THE CAPTURE PAIR. The facade's whole capture surface is two resource types, and the pair is
|
|
9
10
|
# the thing to learn:
|
|
@@ -54,8 +55,9 @@
|
|
|
54
55
|
# and a dotted one is one (dataElement, categoryOptionCombo) cell of it.
|
|
55
56
|
#
|
|
56
57
|
# WHERE A READER CHANGES IT. `questionnaire` names the form; on this facade that is a DHIS2
|
|
57
|
-
# data set uid. Point `
|
|
58
|
-
# data set you generated a form from,
|
|
58
|
+
# data set uid. Point the `fhir-facade` connection at your own `d2w fhir serve`, set `questionnaire_uid` to the
|
|
59
|
+
# data set you generated a form from, repoint the `dhis2` connection, and turn `commit_dry_run`
|
|
60
|
+
# off against an instance of your own.
|
|
59
61
|
#
|
|
60
62
|
# WHAT THIS DOES NOT DO. It does not invent values for unanswered items -- an absent answer and
|
|
61
63
|
# a zero are different facts -- and it does not register completeness. Completeness is a second
|
|
@@ -63,15 +65,19 @@
|
|
|
63
65
|
# values rather than accompany them.
|
|
64
66
|
#
|
|
65
67
|
# dg run --local examples/fhir/fhir-capture-bundle-to-data-values.yaml
|
|
66
|
-
# dg run --local examples/fhir/fhir-capture-bundle-to-data-values.yaml -p
|
|
68
|
+
# dg run --local examples/fhir/fhir-capture-bundle-to-data-values.yaml -p page_size=10
|
|
67
69
|
|
|
68
70
|
format: dirigent/v1
|
|
69
71
|
kind: pipeline
|
|
70
72
|
code: fhir-capture-bundle-to-data-values
|
|
71
73
|
name: FHIR captures to DHIS2 data values
|
|
72
74
|
description: |
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
Import the first completed FHIR form response on a page into DHIS2.
|
|
76
|
+
|
|
77
|
+
A page of `QuestionnaireResponse` captures is read off a `d2w fhir serve` facade. The first
|
|
78
|
+
completed one becomes aggregate data values, and the import is rehearsed first.
|
|
79
|
+
`commit_dry_run` defaults to true, so the second import is a rehearsal too until it is set to
|
|
80
|
+
false against an instance of your own.
|
|
75
81
|
|
|
76
82
|
The capture pair is the vocabulary: a `Questionnaire` is the form definition generated from
|
|
77
83
|
DHIS2 metadata, and a `QuestionnaireResponse` is one submission against it. Every `linkId`
|
|
@@ -95,8 +101,9 @@ connections:
|
|
|
95
101
|
config:
|
|
96
102
|
base_url: http://localhost:8095
|
|
97
103
|
timeout: 30s
|
|
98
|
-
# The instance the values land in. This is the demo, which is for reading:
|
|
99
|
-
#
|
|
104
|
+
# The instance the values land in. This is the demo, which is for reading: both imports below
|
|
105
|
+
# are dry runs against it, and repointing this connection and setting commit_dry_run to false
|
|
106
|
+
# is what makes the real one real.
|
|
100
107
|
dhis2-demo:
|
|
101
108
|
kind: dhis2
|
|
102
109
|
config:
|
|
@@ -108,10 +115,6 @@ connections:
|
|
|
108
115
|
params:
|
|
109
116
|
type: object
|
|
110
117
|
properties:
|
|
111
|
-
fhir_base:
|
|
112
|
-
type: string
|
|
113
|
-
description: The facade to read captures from.
|
|
114
|
-
default: http://localhost:8095
|
|
115
118
|
questionnaire_uid:
|
|
116
119
|
type: string
|
|
117
120
|
description: The aggregate form, which on this facade is the DHIS2 data set uid.
|
|
@@ -120,6 +123,10 @@ params:
|
|
|
120
123
|
type: integer
|
|
121
124
|
description: How many captures one page holds; the facade caps this at 500.
|
|
122
125
|
default: 50
|
|
126
|
+
commit_dry_run:
|
|
127
|
+
type: boolean
|
|
128
|
+
description: Whether the second import is still a rehearsal. False makes it real.
|
|
129
|
+
default: true
|
|
123
130
|
|
|
124
131
|
schemas:
|
|
125
132
|
# The gate is the pack's own formats, not a shape check: dhis2-uid refuses anything that is
|
|
@@ -219,12 +226,13 @@ steps:
|
|
|
219
226
|
|
|
220
227
|
# Second, and only because the rehearsal passed. The default all_success edge is what
|
|
221
228
|
# enforces that: a refused rehearsal leaves this skipped rather than writing anyway.
|
|
229
|
+
# commit_dry_run keeps this a rehearsal as well, so the shared demo is never written to.
|
|
222
230
|
import:
|
|
223
231
|
block: dhis2.data_value_set_import
|
|
224
232
|
depends_on: [rehearse]
|
|
225
233
|
config:
|
|
226
234
|
connection: dhis2-demo
|
|
227
235
|
data_values: "${steps.gate.output.value}"
|
|
228
|
-
dry_run:
|
|
236
|
+
dry_run: "${params.commit_dry_run}"
|
|
229
237
|
import_strategy: CREATE_AND_UPDATE
|
|
230
238
|
atomic_mode: ALL
|
|
@@ -60,8 +60,11 @@ kind: pipeline
|
|
|
60
60
|
code: fhir-conceptmap-driven-mapping
|
|
61
61
|
name: A ConceptMap drives the mapping
|
|
62
62
|
description: |
|
|
63
|
-
Fetch a
|
|
64
|
-
|
|
63
|
+
Fetch a code mapping from a FHIR server and use it to translate form answers.
|
|
64
|
+
|
|
65
|
+
A `ConceptMap` is flattened into a lookup and a capture's coded answers are translated through
|
|
66
|
+
it, so the mapping itself is not written in this document. The translated set goes to DHIS2 as
|
|
67
|
+
a dry run.
|
|
65
68
|
|
|
66
69
|
A `d2w fhir serve` option-set map carries two groups distinguished by `group.target`: one to
|
|
67
70
|
the DHIS2 option uid (always complete) and one to the DHIS2 option code (only where the
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# DHIS2 as forms and captures, so its whole clinical surface is the capture pair, and asking
|
|
9
9
|
# it for an Observation answers an OperationOutcome with code `not-supported`. Encounters come
|
|
10
10
|
# from the other kind of FHIR server: a hospital's own, an OpenMRS, a HAPI. That is the point
|
|
11
|
-
# of this example, so `
|
|
11
|
+
# of this example, so the `fhir-clinical` connection points at the public HAPI R4 test server. It is a shared
|
|
12
12
|
# sandbox anyone can write to, so the resources it holds change under you; every id below is a
|
|
13
13
|
# parameter for that reason, and the searches are written to survive not finding one.
|
|
14
14
|
#
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
# each entry is one LOINC code and the DHIS2 data element uid it lands in. The stage the event
|
|
52
52
|
# belongs to is `program_stage`, and the program and enrollment are `program` and
|
|
53
53
|
# `enrollment_uid`. An observation whose LOINC code is not in the table is dropped and counted
|
|
54
|
-
# -- conceptmap-driven-mapping.yaml is the same mapping fetched from a server instead.
|
|
54
|
+
# -- fhir-conceptmap-driven-mapping.yaml is the same mapping fetched from a server instead.
|
|
55
55
|
#
|
|
56
56
|
# WHAT THIS DOES NOT DO. It does not resolve the Encounter's Patient to a DHIS2 tracked entity.
|
|
57
|
-
# That is a real lookup against a real identifier crosswalk, and
|
|
58
|
-
# is the half that does it; here the entity and its enrollment are parameters.
|
|
57
|
+
# That is a real lookup against a real identifier crosswalk, and
|
|
58
|
+
# fhir-patient-to-tracked-entity.yaml is the half that does it; here the entity and its enrollment are parameters.
|
|
59
59
|
#
|
|
60
60
|
# dg run --local examples/fhir/fhir-encounter-to-event.yaml
|
|
61
61
|
# dg run --local examples/fhir/fhir-encounter-to-event.yaml -p encounter_id=137202491
|
|
@@ -65,7 +65,9 @@ kind: pipeline
|
|
|
65
65
|
code: fhir-encounter-to-event
|
|
66
66
|
name: Encounter and Observations to one event
|
|
67
67
|
description: |
|
|
68
|
-
|
|
68
|
+
Turn one FHIR visit and its measurements into one DHIS2 event, as a dry run.
|
|
69
|
+
|
|
70
|
+
One FHIR `Encounter` and every `Observation` naming it are folded into a single DHIS2 tracker
|
|
69
71
|
event, posted with `importMode=VALIDATE`.
|
|
70
72
|
|
|
71
73
|
The cardinalities do not line up: FHIR spreads a visit over an Encounter plus one
|
|
@@ -119,7 +121,7 @@ params:
|
|
|
119
121
|
tracked_entity:
|
|
120
122
|
type: string
|
|
121
123
|
description: The DHIS2 person the Encounter's Patient resolves to. A real crosswalk is
|
|
122
|
-
patient-to-tracked-entity.yaml.
|
|
124
|
+
fhir-patient-to-tracked-entity.yaml.
|
|
123
125
|
default: coxwIkMqNBB
|
|
124
126
|
enrollment_uid:
|
|
125
127
|
type: string
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
# population, Denominator, Denominator exclusion, Numerator, Numerator exclusion) -- over a
|
|
16
16
|
# Bundle of FHIR data, and a measure evaluation engine is what turns that scoring into an R4
|
|
17
17
|
# MeasureReport. The report is then a resource like any other, so this document fetches it
|
|
18
|
-
# from wherever it was published. `
|
|
18
|
+
# from wherever it was published. The `fhir-reports` connection points at a public HAPI R4
|
|
19
|
+
# sandbox by default.
|
|
19
20
|
#
|
|
20
21
|
# WHAT ARRIVES -- the report. A proportion measure scored over a period:
|
|
21
22
|
#
|
|
@@ -40,12 +41,15 @@
|
|
|
40
41
|
# checked and they agree" is a fact worth delivering too:
|
|
41
42
|
#
|
|
42
43
|
# {"measure": "...", "period": "202608", "org_unit": "ImspTQPwCqd",
|
|
43
|
-
# "
|
|
44
|
+
# "report_period": {"start": "2026-08-01", "end": "2026-08-31"},
|
|
45
|
+
# "fhir_score": 0.75, "dhis2_value": 0.71, "difference": 0.04, "verdict": "discrepant"}
|
|
44
46
|
#
|
|
45
47
|
# WHICH CODES LINE UP WITH WHAT. `measure_url` and `indicator` are the two halves of one claim
|
|
46
48
|
# -- that this FHIR measure and this DHIS2 data element or indicator count the same thing --
|
|
47
|
-
# and a reader changes both together or neither. `period`
|
|
48
|
-
#
|
|
49
|
+
# and a reader changes both together or neither. `period` and `report_period` are the same
|
|
50
|
+
# stretch of time in two vocabularies, and they change together too: `period` is a DHIS2 ISO
|
|
51
|
+
# period, and `report_period` a FHIR date, whose search matches a report inside that range. The
|
|
52
|
+
# record carries the range the report named, so a reader can see what was compared.
|
|
49
53
|
#
|
|
50
54
|
# WHEN IT FINDS NOTHING. A missing report and an empty grid are ordinary answers, not failures:
|
|
51
55
|
# the diff says `unavailable` and the receiver hears about it. A reconciliation that crashed
|
|
@@ -59,8 +63,10 @@ kind: pipeline
|
|
|
59
63
|
code: fhir-measure-report-to-analytics-check
|
|
60
64
|
name: MeasureReport against DHIS2 analytics
|
|
61
65
|
description: |
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
Compare a FHIR measure report with DHIS2 analytics and report the difference.
|
|
67
|
+
|
|
68
|
+
The newest FHIR `MeasureReport` for the measure and period is held against the DHIS2 analytics
|
|
69
|
+
value for that period and one org unit, and the gap is delivered.
|
|
64
70
|
|
|
65
71
|
A reconciliation, so it writes nothing: safe on a schedule. The report is produced by
|
|
66
72
|
scoring a CQL measure over FHIR data -- `d2w fhir serve` publishes no `Measure` or
|
|
@@ -114,8 +120,12 @@ params:
|
|
|
114
120
|
default: ImspTQPwCqd
|
|
115
121
|
period:
|
|
116
122
|
type: string
|
|
117
|
-
description:
|
|
123
|
+
description: The DHIS2 ISO period analytics is asked for.
|
|
118
124
|
default: "202608"
|
|
125
|
+
report_period:
|
|
126
|
+
type: string
|
|
127
|
+
description: The same period as a FHIR date, which the report search matches within.
|
|
128
|
+
default: "2026-08"
|
|
119
129
|
tolerance:
|
|
120
130
|
type: number
|
|
121
131
|
description: How far apart the two may be before the difference is called a discrepancy.
|
|
@@ -133,6 +143,7 @@ steps:
|
|
|
133
143
|
method: GET
|
|
134
144
|
query:
|
|
135
145
|
measure: "${params.measure_url}"
|
|
146
|
+
period: "${params.report_period}"
|
|
136
147
|
_sort: "-date"
|
|
137
148
|
_count: 1
|
|
138
149
|
headers:
|
|
@@ -181,6 +192,7 @@ steps:
|
|
|
181
192
|
| {measure: $measure,
|
|
182
193
|
period: $period,
|
|
183
194
|
org_unit: $org_unit,
|
|
195
|
+
report_period: $report.period,
|
|
184
196
|
fhir_score: $score,
|
|
185
197
|
dhis2_value: $value,
|
|
186
198
|
populations: [(($report.group[0].population // [])[])
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
# first page and reports whether there were more, which is the smallest thing that does not
|
|
52
52
|
# quietly lie about completeness.
|
|
53
53
|
#
|
|
54
|
+
# WHERE THIS GOES NEXT. The sync exists to feed DHIS2, and the shelf's other documents are what
|
|
55
|
+
# turn each of these types into an import: Patient through fhir-patient-to-tracked-entity.yaml,
|
|
56
|
+
# Encounter through fhir-encounter-to-event.yaml, QuestionnaireResponse through
|
|
57
|
+
# fhir-capture-bundle-to-data-values.yaml. This one only pulls, and its last step only asks the
|
|
58
|
+
# instance they import into whether it answers.
|
|
59
|
+
#
|
|
54
60
|
# WHERE A READER CHANGES IT. `resource_types` is the fan-out and the whole scope of the sync;
|
|
55
61
|
# `page_size` is what one page holds. Add a type to the list and the grid grows on the next
|
|
56
62
|
# firing with nothing else edited.
|
|
@@ -65,8 +71,11 @@ kind: pipeline
|
|
|
65
71
|
code: fhir-nightly-window-sync
|
|
66
72
|
name: Nightly windowed FHIR sync
|
|
67
73
|
description: |
|
|
68
|
-
Pull
|
|
69
|
-
|
|
74
|
+
Pull last night's changes from a FHIR server, type by type, into one summary.
|
|
75
|
+
|
|
76
|
+
Each run reads what changed inside the interval the nightly schedule just closed, with one
|
|
77
|
+
fan-out item per resource type, and joins the results. Only the first page of each type is
|
|
78
|
+
read, and the summary says whether more were left.
|
|
70
79
|
|
|
71
80
|
The window belongs to the run, not to this document: `${run.window.start}` and
|
|
72
81
|
`${run.window.end}` are half-open ISO instants, so consecutive firings tile the timeline
|
|
@@ -80,17 +89,7 @@ requires:
|
|
|
80
89
|
- transform.jq
|
|
81
90
|
|
|
82
91
|
connections:
|
|
83
|
-
#
|
|
84
|
-
# half-open range needs the same query parameter twice and the query map holds one per key.
|
|
85
|
-
fhir-source:
|
|
86
|
-
kind: http
|
|
87
|
-
config:
|
|
88
|
-
base_url: https://hapi.fhir.org/baseR4
|
|
89
|
-
timeout: 60s
|
|
90
|
-
# Not read by this document, which only pulls. It is here because the sync exists to feed
|
|
91
|
-
# DHIS2, and the shelf's other documents are what turn each of these types into an import:
|
|
92
|
-
# Patient through patient-to-tracked-entity.yaml, Encounter through encounter-to-event.yaml,
|
|
93
|
-
# QuestionnaireResponse through capture-bundle-to-data-values.yaml.
|
|
92
|
+
# The instance this sync feeds, read by the target step and written by nothing here.
|
|
94
93
|
dhis2-demo:
|
|
95
94
|
kind: dhis2
|
|
96
95
|
config:
|
|
@@ -119,10 +118,9 @@ params:
|
|
|
119
118
|
default: 50
|
|
120
119
|
|
|
121
120
|
steps:
|
|
122
|
-
# for_each
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
# /metadata: a run's shape is fixed before its first request.
|
|
121
|
+
# for_each reads a parameter here, so the grid is written when the run is created. It could
|
|
122
|
+
# read a step's output, and discover the types from /metadata, but then the grid would not
|
|
123
|
+
# exist until that step settled. A parameter fixes the run's shape before its first request.
|
|
126
124
|
pull:
|
|
127
125
|
block: http.request
|
|
128
126
|
for_each: "${params.resource_types}"
|
|
@@ -130,6 +128,7 @@ steps:
|
|
|
130
128
|
config:
|
|
131
129
|
url: "${params.fhir_base}/${item}?_lastUpdated=ge${run.window.start}&_lastUpdated=lt${run.window.end}&_count=${params.page_size}&_sort=_lastUpdated"
|
|
132
130
|
method: GET
|
|
131
|
+
timeout: 60s
|
|
133
132
|
headers:
|
|
134
133
|
Accept: application/fhir+json
|
|
135
134
|
# A type this server does not serve answers 404 or an OperationOutcome, and under
|
|
@@ -170,6 +169,17 @@ steps:
|
|
|
170
169
|
# what keeps a partial sync from reading like a complete one.
|
|
171
170
|
types_missed: ($asked - [$seen[] | .type])}
|
|
172
171
|
|
|
172
|
+
# One short read of the instance the types above are imported into. Tolerated, because a
|
|
173
|
+
# sleeping DHIS2 does not change what the FHIR server said.
|
|
174
|
+
target:
|
|
175
|
+
block: http.request
|
|
176
|
+
depends_on: [summarise]
|
|
177
|
+
continue_on_failure: true
|
|
178
|
+
config:
|
|
179
|
+
connection: dhis2-demo
|
|
180
|
+
path: /api/system/info
|
|
181
|
+
method: GET
|
|
182
|
+
|
|
173
183
|
triggers:
|
|
174
184
|
schedules:
|
|
175
185
|
- code: nightly
|
|
@@ -69,8 +69,10 @@ kind: pipeline
|
|
|
69
69
|
code: fhir-patient-to-tracked-entity
|
|
70
70
|
name: FHIR Patient to a DHIS2 tracked entity
|
|
71
71
|
description: |
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
Turn a FHIR Patient into a DHIS2 tracked entity and enrollment, as a dry run.
|
|
73
|
+
|
|
74
|
+
The run searches the facade's `Patient` register and builds the `/api/tracker` payload for one
|
|
75
|
+
tracked entity and its enrollment, posted with `importMode=VALIDATE`.
|
|
74
76
|
|
|
75
77
|
`dhis2.tracker` reads and does not write, so the write is an ordinary `http.request` on the
|
|
76
78
|
`dhis2` connection -- which works because the HTTP blocks read `base_url` and the basic
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# One QuestionnaireResponse mapped onto data elements by a linkId lookup the document carries.
|
|
2
2
|
#
|
|
3
|
-
# THE FLOW, END TO END.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# THE FLOW, END TO END. Take one capture, written inline, translate its answers into data
|
|
4
|
+
# values through an explicit linkId-to-data-element table held in params, and rehearse the
|
|
5
|
+
# import as a dry run. It writes nothing, here or on any instance, until the rehearse step's
|
|
6
|
+
# dry_run is turned off.
|
|
6
7
|
#
|
|
7
8
|
# WHY A LOOKUP AT ALL. On a `d2w fhir serve` facade the linkIds already ARE DHIS2 uids, so
|
|
8
|
-
# capture-bundle-to-data-values.yaml needs no table: it splits the dotted linkId and is done.
|
|
9
|
+
# fhir-capture-bundle-to-data-values.yaml needs no table: it splits the dotted linkId and is done.
|
|
9
10
|
# Every other FHIR server in the world uses linkIds a form designer chose -- "anc.visits.total",
|
|
10
11
|
# "q3b", "weight_kg" -- and then the table IS the integration. This document is the general
|
|
11
|
-
# case, and the
|
|
12
|
+
# case, and the inline response stands in for what such a server sends.
|
|
12
13
|
#
|
|
13
14
|
# WHAT ARRIVES. One QuestionnaireResponse, answers keyed by whatever the form calls them:
|
|
14
15
|
#
|
|
@@ -38,8 +39,10 @@ kind: pipeline
|
|
|
38
39
|
code: fhir-questionnaire-response-to-data-values
|
|
39
40
|
name: QuestionnaireResponse through a linkId table
|
|
40
41
|
description: |
|
|
41
|
-
Map one
|
|
42
|
-
|
|
42
|
+
Map one FHIR form response onto DHIS2 data values using a lookup table.
|
|
43
|
+
|
|
44
|
+
The `QuestionnaireResponse` goes through an explicit linkId lookup, for the general case where
|
|
45
|
+
a form's `linkId` is a designer's name rather than a DHIS2 uid. The import is a dry run.
|
|
43
46
|
|
|
44
47
|
A linkId the table does not name is dropped and counted, never guessed at.
|
|
45
48
|
|
|
@@ -94,7 +97,7 @@ params:
|
|
|
94
97
|
steps:
|
|
95
98
|
# The capture, written inline so the document runs with nothing set up. In service it is the
|
|
96
99
|
# body of an endpoint delivery (fhir-subscription-to-dhis2.yaml) or one entry of a
|
|
97
|
-
# searchset Bundle (capture-bundle-to-data-values.yaml); either way this step's output is
|
|
100
|
+
# searchset Bundle (fhir-capture-bundle-to-data-values.yaml); either way this step's output is
|
|
98
101
|
# exactly one QuestionnaireResponse, and nothing downstream knows where it came from.
|
|
99
102
|
receive:
|
|
100
103
|
block: value.const
|
|
@@ -94,8 +94,11 @@ kind: pipeline
|
|
|
94
94
|
code: fhir-subscription-to-dhis2
|
|
95
95
|
name: A FHIR Subscription notification imported
|
|
96
96
|
description: |
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
Receive a FHIR notification and rehearse importing it into DHIS2.
|
|
98
|
+
|
|
99
|
+
A FHIR `Subscription` rest-hook notification arrives on an endpoint source, and the notified
|
|
100
|
+
`QuestionnaireResponse` is translated into a DHIS2 data value set before the import is
|
|
101
|
+
rehearsed.
|
|
99
102
|
|
|
100
103
|
The subscription contract is three fields: `criteria` (a search string), `channel.type`
|
|
101
104
|
(`rest-hook` is an HTTP POST), and `channel.endpoint` (the source's route). The server
|
|
@@ -24,7 +24,7 @@ setting (`storage_connections: {s3: <code>}`); the rest need only a network.
|
|
|
24
24
|
| --- | --- |
|
|
25
25
|
| [`http-json-to-data-values.yaml`](http-json-to-data-values.yaml) | The shortest inbound path: pull a public JSON API, reshape it, gate it on DHIS2's own id and period formats, import it as a dry run. |
|
|
26
26
|
| [`csv-drop-to-data-values.yaml`](csv-drop-to-data-values.yaml) | The file drop: wait for a csv to land in a bucket, decode it, translate the sender's codes to uids through a lookup, drop the rows that carry no measurement. |
|
|
27
|
-
| [`endpoint-payload-to-
|
|
27
|
+
| [`endpoint-payload-to-data-values.yaml`](endpoint-payload-to-data-values.yaml) | Push instead of poll: an endpoint source takes one posted encounter, and the jq step that picks its named fields is the whole security boundary. |
|
|
28
28
|
| [`parquet-lakehouse-to-dhis2.yaml`](parquet-lakehouse-to-dhis2.yaml) | Grain: a parquet table of encounter-level rows reduced to the figures DHIS2 stores, then a completeness sensor that observes rather than registers. |
|
|
29
29
|
| [`fhir-observations-to-data-values.yaml`](fhir-observations-to-data-values.yaml) | Two standards meeting: LOINC codes mapped onto data elements, instants truncated to periods, and the org unit the payload cannot supply. |
|
|
30
30
|
| [`weekly-window-pull-and-import.yaml`](weekly-window-pull-and-import.yaml) | The window: a schedule whose runs pull the week that closed, `${run.window.start}` to `${run.window.end}`, so a late run and a backfill read the same week. |
|
|
@@ -39,8 +39,11 @@ kind: pipeline
|
|
|
39
39
|
code: csv-drop-to-data-values
|
|
40
40
|
name: A csv drop into DHIS2
|
|
41
41
|
description: |
|
|
42
|
-
Wait for a csv
|
|
43
|
-
|
|
42
|
+
Wait for a csv in object storage and import its rows into DHIS2.
|
|
43
|
+
|
|
44
|
+
The csv is decoded, the sender's codes are translated into DHIS2 uids, the rows that carry no
|
|
45
|
+
measurement are dropped, and what is left is imported. `dry_run` defaults to true, so a first
|
|
46
|
+
run writes nothing.
|
|
44
47
|
|
|
45
48
|
tags: [inbound, dhis2, storage, transform, cross-boundary]
|
|
46
49
|
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
#
|
|
35
35
|
# RUN IT BY HAND, here, with no server. No --input takes the source's sample:
|
|
36
36
|
#
|
|
37
|
-
# dg run --local examples/inbound/endpoint-payload-to-
|
|
38
|
-
# dg run --local examples/inbound/endpoint-payload-to-
|
|
37
|
+
# dg run --local examples/inbound/endpoint-payload-to-data-values.yaml
|
|
38
|
+
# dg run --local examples/inbound/endpoint-payload-to-data-values.yaml --input '{body: {
|
|
39
39
|
# encounter: {facility: DiszpKrYNg8, occurred_at: "2026-06-14T09:20:00Z",
|
|
40
40
|
# items: [{code: NVP_WITHIN_72H, quantity: 3}]}}}'
|
|
41
41
|
#
|
|
42
42
|
# ON A SERVER, apply it, keep the token the apply printed, and post an encounter:
|
|
43
43
|
#
|
|
44
|
-
# dg apply examples/inbound/endpoint-payload-to-
|
|
45
|
-
# dg source call endpoint-payload-to-
|
|
44
|
+
# dg apply examples/inbound/endpoint-payload-to-data-values.yaml
|
|
45
|
+
# dg source call endpoint-payload-to-data-values posted --source-token "$TOKEN" --body '{
|
|
46
46
|
# "encounter": {"facility": "DiszpKrYNg8", "occurred_at": "2026-06-14T09:20:00Z",
|
|
47
47
|
# "items": [{"code": "EXPOSED_REGISTERED", "quantity": 2}]}}'
|
|
48
48
|
# curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
|
49
49
|
# --data @encounter.json http://localhost:3333/endpoints/captures
|
|
50
|
-
# dg source inputs endpoint-payload-to-
|
|
50
|
+
# dg source inputs endpoint-payload-to-data-values posted
|
|
51
51
|
#
|
|
52
52
|
# The token lives on the instance and is shown once; `dg source rotate-token` mints another.
|
|
53
53
|
#
|
|
@@ -55,11 +55,14 @@
|
|
|
55
55
|
|
|
56
56
|
format: dirigent/v1
|
|
57
57
|
kind: pipeline
|
|
58
|
-
code: endpoint-payload-to-
|
|
58
|
+
code: endpoint-payload-to-data-values
|
|
59
59
|
name: A pushed encounter into DHIS2
|
|
60
60
|
description: |
|
|
61
|
+
Take an encounter posted by an outside system and import it into DHIS2.
|
|
62
|
+
|
|
61
63
|
An outside system POSTs one captured encounter to an endpoint source; a jq step picks the
|
|
62
|
-
named fields, and the run reshapes them into a DHIS2 data value set and imports it.
|
|
64
|
+
named fields, and the run reshapes them into a DHIS2 data value set and imports it. `dry_run`
|
|
65
|
+
defaults to true, so a first run writes nothing.
|
|
63
66
|
|
|
64
67
|
The pack's `dhis2.tracker` block reads `/api/tracker` and does not write it, so an
|
|
65
68
|
event-shaped capture lands as the aggregate figures it contributes to.
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
#
|
|
7
7
|
# WHY THE FAN CONVERGES AT THE IMPORT. Two reasons, and both are worth knowing:
|
|
8
8
|
#
|
|
9
|
-
# for_each
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# should not be -- it would make the shape of the run depend on the weather.
|
|
9
|
+
# A for_each may read another step's output, so a step fanned over "the facilities that
|
|
10
|
+
# answered" could be written. It should not be: it would make the shape of the run depend
|
|
11
|
+
# on the weather, and its grid would not exist until the pull had settled.
|
|
13
12
|
#
|
|
14
13
|
# A data value set carries orgUnit per value, so one document can hold every facility at
|
|
15
14
|
# once. Fifteen imports of one facility each would be fifteen round trips, fifteen
|
|
@@ -50,8 +49,11 @@ kind: pipeline
|
|
|
50
49
|
code: fan-out-per-facility-import
|
|
51
50
|
name: A facility fan-out into one district import
|
|
52
51
|
description: |
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
Collect from each facility, import what answered, and report who did not.
|
|
53
|
+
|
|
54
|
+
There is one pull per facility, tolerating the ones that are down. What answered is imported as
|
|
55
|
+
a single data value set, and a reconciliation of asked-for against imported is posted. `dry_run`
|
|
56
|
+
defaults to true, so a first run writes nothing.
|
|
55
57
|
|
|
56
58
|
tags: [inbound, dhis2, http, graph, transform, cross-boundary]
|
|
57
59
|
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
# The hops, and the shape at each one:
|
|
19
19
|
#
|
|
20
20
|
# 1. pull -> a FHIR Bundle: {resourceType: "Bundle", entry: [{resource: {...}}]}
|
|
21
|
-
# 2. flatten -> one row per entry that has both a mapped LOINC code and a
|
|
22
|
-
#
|
|
23
|
-
# 3. aggregate -> {dataValues: [...]}, one value per (element, period) pair,
|
|
21
|
+
# 2. flatten -> one row per entry that has both a mapped LOINC code and a time it
|
|
22
|
+
# happened; everything else is dropped, silently and on purpose
|
|
23
|
+
# 3. aggregate -> {dataValues: [...]}, one value per (element, period) pair, counted
|
|
24
24
|
# 4. gate -> refused unless the ids and periods are DHIS2's own
|
|
25
25
|
# 5. import -> the instance's summary
|
|
26
26
|
#
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
# perfectly predictable in shape -- which is the half this example teaches. If a run finds
|
|
35
35
|
# nothing mappable, the gate refuses an empty set rather than importing nothing quietly.
|
|
36
36
|
#
|
|
37
|
-
# What makes it yours:
|
|
38
|
-
# org unit.
|
|
37
|
+
# What makes it yours: the `fhir-server` connection pointed at your server, the lookup table,
|
|
38
|
+
# and the org unit.
|
|
39
39
|
#
|
|
40
40
|
# dg run --local examples/inbound/fhir-observations-to-data-values.yaml
|
|
41
41
|
# dg run --local examples/inbound/fhir-observations-to-data-values.yaml -p count=50
|
|
@@ -45,8 +45,11 @@ kind: pipeline
|
|
|
45
45
|
code: fhir-observations-to-data-values
|
|
46
46
|
name: FHIR Observations into DHIS2
|
|
47
47
|
description: |
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
Count FHIR Observations by month and import the counts into DHIS2.
|
|
49
|
+
|
|
50
|
+
A FHIR Observation bundle is pulled, LOINC codes are mapped onto DHIS2 data elements, and the
|
|
51
|
+
monthly counts are imported as a data value set. `dry_run` defaults to true, so a first run
|
|
52
|
+
writes nothing.
|
|
50
53
|
|
|
51
54
|
tags: [inbound, dhis2, http, transform, validate, cross-boundary]
|
|
52
55
|
|
|
@@ -27,15 +27,18 @@
|
|
|
27
27
|
# write nothing. The play server is shared, so leave it true there.
|
|
28
28
|
#
|
|
29
29
|
# dg run --local examples/inbound/http-json-to-data-values.yaml
|
|
30
|
-
# dg run --local examples/inbound/http-json-to-data-values.yaml
|
|
30
|
+
# dg run --local examples/inbound/http-json-to-data-values.yaml \
|
|
31
|
+
# -p source_url=https://api.worldbank.org/v2/country/NGA/indicator/SP.POP.TOTL
|
|
31
32
|
|
|
32
33
|
format: dirigent/v1
|
|
33
34
|
kind: pipeline
|
|
34
35
|
code: http-json-to-data-values
|
|
35
36
|
name: A public JSON API into DHIS2
|
|
36
37
|
description: |
|
|
37
|
-
Pull a public JSON feed
|
|
38
|
-
|
|
38
|
+
Pull a public JSON feed and rehearse importing it into DHIS2.
|
|
39
|
+
|
|
40
|
+
The feed is reshaped into a DHIS2 data value set, gated on DHIS2's own id and period formats,
|
|
41
|
+
and handed to `dhis2.data_value_set_import` as a dry run.
|
|
39
42
|
|
|
40
43
|
tags: [inbound, dhis2, http, transform, validate, cross-boundary]
|
|
41
44
|
|
|
@@ -62,10 +65,6 @@ params:
|
|
|
62
65
|
type: string
|
|
63
66
|
description: The outside feed. Anything answering JSON works; the reshape is what changes.
|
|
64
67
|
default: https://api.worldbank.org/v2/country/SLE/indicator/SP.POP.TOTL
|
|
65
|
-
country:
|
|
66
|
-
type: string
|
|
67
|
-
description: The ISO3 code spliced into source_url when it is left at its default.
|
|
68
|
-
default: SLE
|
|
69
68
|
data_element:
|
|
70
69
|
type: string
|
|
71
70
|
description: The uid every pulled observation lands on; Total Population, yearly.
|
|
@@ -40,16 +40,19 @@
|
|
|
40
40
|
# becomes real by setting it false against an instance of your own.
|
|
41
41
|
#
|
|
42
42
|
# dg run --local examples/inbound/outside-to-dhis2-with-checks.yaml
|
|
43
|
-
# dg run --local examples/inbound/outside-to-dhis2-with-checks.yaml -p commit_dry_run=false
|
|
43
|
+
# dg run --local examples/inbound/outside-to-dhis2-with-checks.yaml -p commit_dry_run=false # against your own instance
|
|
44
44
|
|
|
45
45
|
format: dirigent/v1
|
|
46
46
|
kind: pipeline
|
|
47
47
|
code: outside-to-dhis2-with-checks
|
|
48
48
|
name: An inbound pipeline with every guard
|
|
49
49
|
description: |
|
|
50
|
+
Import outside data into DHIS2 with a safety check at every stage.
|
|
51
|
+
|
|
50
52
|
A readiness sensor, a schema on what arrived, a schema on what was built, a rehearsed
|
|
51
53
|
import, the real import behind it, and the completeness sensor -- each guard buying off a
|
|
52
|
-
different failure.
|
|
54
|
+
different failure. `commit_dry_run` defaults to true, so the second import is a rehearsal too
|
|
55
|
+
until it is set to false.
|
|
53
56
|
|
|
54
57
|
tags: [inbound, dhis2, http, sensor, transform, validate, cross-boundary]
|
|
55
58
|
|
|
@@ -41,8 +41,11 @@ kind: pipeline
|
|
|
41
41
|
code: parquet-lakehouse-to-dhis2
|
|
42
42
|
name: A lakehouse table into DHIS2
|
|
43
43
|
description: |
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
Sum a parquet export down to DHIS2 figures, import them, and await sign-off.
|
|
45
|
+
|
|
46
|
+
The parquet export holds encounter-grain rows, which are aggregated to DHIS2's grain before the
|
|
47
|
+
import, and the run then waits for the month to be signed off. `dry_run` defaults to true, so a
|
|
48
|
+
first run writes nothing.
|
|
46
49
|
|
|
47
50
|
tags: [inbound, dhis2, parquet, storage, transform, cross-boundary]
|
|
48
51
|
|
|
@@ -42,9 +42,11 @@ kind: pipeline
|
|
|
42
42
|
code: weekly-window-pull-and-import
|
|
43
43
|
name: A weekly windowed pull into DHIS2
|
|
44
44
|
description: |
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
Pull last week's data every Monday, import it into DHIS2, and await sign-off.
|
|
46
|
+
|
|
47
|
+
A Monday schedule's runs pull the week that just closed -- `${run.window.start}` up to but not
|
|
48
|
+
including `${run.window.end}` -- reshape it into a data value set, import it, and wait for the
|
|
49
|
+
week to be signed off. `dry_run` defaults to true, so a first run writes nothing.
|
|
48
50
|
|
|
49
51
|
tags: [inbound, dhis2, http, schedule, transform, cross-boundary]
|
|
50
52
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
# (storage_connections: {s3: <code>}).
|
|
42
42
|
#
|
|
43
43
|
# dg run --local examples/parquet-to-dhis2-import.yaml
|
|
44
|
-
# dg run --local examples/parquet-to-dhis2-import.yaml -p dry_run=false
|
|
44
|
+
# dg run --local examples/parquet-to-dhis2-import.yaml -p dry_run=false # against your own instance
|
|
45
45
|
|
|
46
46
|
format: dirigent/v1
|
|
47
47
|
kind: pipeline
|
|
File without changes
|
{dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/__init__.py
RENAMED
|
File without changes
|
{dirigent_integration-0.27.0 → dirigent_integration-0.28.0}/src/dirigent_integration/py.typed
RENAMED
|
File without changes
|