dirigent-integration 0.23.2__py3-none-any.whl
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/__init__.py +29 -0
- dirigent_integration/py.typed +0 -0
- dirigent_integration/shelves/dhis2-analytics-to-csv-report.yaml +174 -0
- dirigent_integration/shelves/dhis2-export-to-s3.yaml +66 -0
- dirigent_integration/shelves/dhis2-metadata-snapshot.yaml +143 -0
- dirigent_integration/shelves/dhis2-tracker-weekly-window.yaml +162 -0
- dirigent_integration/shelves/dhis2-values-per-org-unit-to-parquet.yaml +186 -0
- dirigent_integration/shelves/dhis2-values-to-parquet.yaml +64 -0
- dirigent_integration/shelves/fhir/README.md +79 -0
- dirigent_integration/shelves/fhir/dhis2-to-fhir-observations.yaml +212 -0
- dirigent_integration/shelves/fhir/fhir-capture-bundle-to-data-values.yaml +230 -0
- dirigent_integration/shelves/fhir/fhir-conceptmap-driven-mapping.yaml +222 -0
- dirigent_integration/shelves/fhir/fhir-encounter-to-event.yaml +248 -0
- dirigent_integration/shelves/fhir/fhir-measure-report-to-analytics-check.yaml +205 -0
- dirigent_integration/shelves/fhir/fhir-nightly-window-sync.yaml +182 -0
- dirigent_integration/shelves/fhir/fhir-patient-to-tracked-entity.yaml +225 -0
- dirigent_integration/shelves/fhir/fhir-questionnaire-response-to-data-values.yaml +187 -0
- dirigent_integration/shelves/fhir/fhir-subscription-webhook-to-dhis2.yaml +218 -0
- dirigent_integration/shelves/inbound/README.md +32 -0
- dirigent_integration/shelves/inbound/csv-drop-to-data-values.yaml +176 -0
- dirigent_integration/shelves/inbound/fan-out-per-facility-import.yaml +202 -0
- dirigent_integration/shelves/inbound/fhir-observations-to-data-values.yaml +193 -0
- dirigent_integration/shelves/inbound/http-json-to-data-values.yaml +151 -0
- dirigent_integration/shelves/inbound/outside-to-dhis2-with-checks.yaml +246 -0
- dirigent_integration/shelves/inbound/parquet-lakehouse-to-dhis2.yaml +158 -0
- dirigent_integration/shelves/inbound/webhook-payload-to-tracker-event.yaml +176 -0
- dirigent_integration/shelves/inbound/weekly-window-pull-and-import.yaml +175 -0
- dirigent_integration/shelves/parquet-to-dhis2-import.yaml +195 -0
- dirigent_integration-0.23.2.dist-info/METADATA +231 -0
- dirigent_integration-0.23.2.dist-info/RECORD +33 -0
- dirigent_integration-0.23.2.dist-info/WHEEL +4 -0
- dirigent_integration-0.23.2.dist-info/entry_points.txt +3 -0
- dirigent_integration-0.23.2.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# A page of captures pulled off a FHIR facade and imported into DHIS2 as aggregate values.
|
|
2
|
+
#
|
|
3
|
+
# THE FLOW, END TO END. `d2w fhir serve` publishes one DHIS2 instance as a FHIR endpoint and
|
|
4
|
+
# takes captures back. This document reads the captures it is holding for one aggregate form,
|
|
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 writes it.
|
|
7
|
+
#
|
|
8
|
+
# THE CAPTURE PAIR. The facade's whole capture surface is two resource types, and the pair is
|
|
9
|
+
# the thing to learn:
|
|
10
|
+
#
|
|
11
|
+
# Questionnaire the form DEFINITION, generated from DHIS2 metadata. One per
|
|
12
|
+
# aggregate data set, event program, tracker program, program stage,
|
|
13
|
+
# or tracked entity type. Its `item.linkId` values ARE DHIS2 uids.
|
|
14
|
+
# QuestionnaireResponse one SUBMISSION against that form: it points at the form through
|
|
15
|
+
# `questionnaire`, names a `subject`, and answers item by item on the
|
|
16
|
+
# same linkIds. One aggregate response is exactly one DHIS2
|
|
17
|
+
# (data set, org unit, period, attribute option combo) cell block.
|
|
18
|
+
#
|
|
19
|
+
# Because the linkIds are DHIS2 uids, a response is readable back into DHIS2 without consulting
|
|
20
|
+
# the form -- which is what makes the jq below short.
|
|
21
|
+
#
|
|
22
|
+
# WHAT ARRIVES. One searchset Bundle of aggregate responses, each shaped like this:
|
|
23
|
+
#
|
|
24
|
+
# {"resourceType": "QuestionnaireResponse",
|
|
25
|
+
# "questionnaire": "http://example.org/fhir/Questionnaire/lyLU2wR22tC",
|
|
26
|
+
# "status": "completed",
|
|
27
|
+
# "subject": {"reference": "Location/dczh6Jfd4no"},
|
|
28
|
+
# "extension": [
|
|
29
|
+
# {"url": ".../d2-period",
|
|
30
|
+
# "extension": [{"url": "iso", "valueString": "202608"},
|
|
31
|
+
# {"url": "type", "valueCode": "Monthly"}]},
|
|
32
|
+
# {"url": ".../d2-attribute-option-combo",
|
|
33
|
+
# "valueCoding": {"code": "N7QFN41eTN8"}}],
|
|
34
|
+
# "item": [{"linkId": "GMd99K8gVut",
|
|
35
|
+
# "item": [{"linkId": "GMd99K8gVut.qNCMOhkoQju",
|
|
36
|
+
# "answer": [{"valueDecimal": 426.3}]}]}]}
|
|
37
|
+
#
|
|
38
|
+
# WHAT LEAVES. One `/api/dataValueSets` document per response:
|
|
39
|
+
#
|
|
40
|
+
# {"dataSet": "lyLU2wR22tC", "period": "202608", "orgUnit": "dczh6Jfd4no",
|
|
41
|
+
# "attributeOptionCombo": "N7QFN41eTN8",
|
|
42
|
+
# "dataValues": [{"dataElement": "GMd99K8gVut",
|
|
43
|
+
# "categoryOptionCombo": "qNCMOhkoQju", "value": "426.3"}]}
|
|
44
|
+
#
|
|
45
|
+
# WHICH CODE BECOMES WHAT. Five rules, and every one of them is a field a reader changes:
|
|
46
|
+
#
|
|
47
|
+
# questionnaire canonical, last segment -> dataSet
|
|
48
|
+
# D2Period extension, its `iso` slice -> period (a DHIS2 ISO period, not a date range)
|
|
49
|
+
# subject.reference "Location/<uid>" -> orgUnit
|
|
50
|
+
# D2AttributeOptionCombo coding's code -> attributeOptionCombo
|
|
51
|
+
# an answered linkId "<de>" or "<de>.<coc>" -> one dataValue, split on the dot
|
|
52
|
+
#
|
|
53
|
+
# That dotted linkId is the entire disaggregation mapping: a plain linkId is a data element,
|
|
54
|
+
# and a dotted one is one (dataElement, categoryOptionCombo) cell of it.
|
|
55
|
+
#
|
|
56
|
+
# WHERE A READER CHANGES IT. `questionnaire` names the form; on this facade that is a DHIS2
|
|
57
|
+
# data set uid. Point `fhir_base` at your own `d2w fhir serve`, set `questionnaire_uid` to the
|
|
58
|
+
# data set you generated a form from, and repoint the `dhis2` connection.
|
|
59
|
+
#
|
|
60
|
+
# WHAT THIS DOES NOT DO. It does not invent values for unanswered items -- an absent answer and
|
|
61
|
+
# a zero are different facts -- and it does not register completeness. Completeness is a second
|
|
62
|
+
# write, keyed by (data set, period, org unit, attribute option combo), and it must follow the
|
|
63
|
+
# values rather than accompany them.
|
|
64
|
+
#
|
|
65
|
+
# 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 fhir_base=http://localhost:8096
|
|
67
|
+
|
|
68
|
+
format: dirigent/v1
|
|
69
|
+
kind: pipeline
|
|
70
|
+
code: fhir-capture-bundle-to-data-values
|
|
71
|
+
name: FHIR captures to DHIS2 data values
|
|
72
|
+
description: |
|
|
73
|
+
Read a page of `QuestionnaireResponse` captures off a `d2w fhir serve` facade and import
|
|
74
|
+
them into DHIS2 as aggregate data values, rehearsing the import first.
|
|
75
|
+
|
|
76
|
+
The capture pair is the vocabulary: a `Questionnaire` is the form definition generated from
|
|
77
|
+
DHIS2 metadata, and a `QuestionnaireResponse` is one submission against it. Every `linkId`
|
|
78
|
+
is a DHIS2 uid, and a dotted one, `<dataElement>.<categoryOptionCombo>`, is one
|
|
79
|
+
disaggregated cell.
|
|
80
|
+
|
|
81
|
+
tags: [fhir, dhis2, cross-boundary]
|
|
82
|
+
|
|
83
|
+
requires:
|
|
84
|
+
blocks:
|
|
85
|
+
- http.request
|
|
86
|
+
- transform.jq
|
|
87
|
+
- validate.schema
|
|
88
|
+
- dhis2.data_value_set_import
|
|
89
|
+
|
|
90
|
+
connections:
|
|
91
|
+
# The facade. It takes no credential in the default single-user setup, so the connection
|
|
92
|
+
# carries only where it is; an instance running behind auth gets basic_username here.
|
|
93
|
+
fhir-facade:
|
|
94
|
+
kind: http
|
|
95
|
+
config:
|
|
96
|
+
base_url: http://localhost:8095
|
|
97
|
+
timeout: 30s
|
|
98
|
+
# The instance the values land in. This is the demo, which is for reading: the import below
|
|
99
|
+
# is a dry run against it, and repointing this connection is what makes the real one real.
|
|
100
|
+
dhis2-demo:
|
|
101
|
+
kind: dhis2
|
|
102
|
+
config:
|
|
103
|
+
base_url: https://play.im.dhis2.org/stable-2-43-1
|
|
104
|
+
basic_username: admin
|
|
105
|
+
basic_password: district
|
|
106
|
+
timeout: 60s
|
|
107
|
+
|
|
108
|
+
params:
|
|
109
|
+
type: object
|
|
110
|
+
properties:
|
|
111
|
+
fhir_base:
|
|
112
|
+
type: string
|
|
113
|
+
description: The facade to read captures from.
|
|
114
|
+
default: http://localhost:8095
|
|
115
|
+
questionnaire_uid:
|
|
116
|
+
type: string
|
|
117
|
+
description: The aggregate form, which on this facade is the DHIS2 data set uid.
|
|
118
|
+
default: lyLU2wR22tC
|
|
119
|
+
page_size:
|
|
120
|
+
type: integer
|
|
121
|
+
description: How many captures one page holds; the facade caps this at 500.
|
|
122
|
+
default: 50
|
|
123
|
+
|
|
124
|
+
schemas:
|
|
125
|
+
# The gate is the pack's own formats, not a shape check: dhis2-uid refuses anything that is
|
|
126
|
+
# not an 11-character DHIS2 uid, dhis2-period anything that is not an ISO period. A capture
|
|
127
|
+
# whose linkId was hand-edited fails here rather than at the instance.
|
|
128
|
+
dhis2-data-value-set:
|
|
129
|
+
type: object
|
|
130
|
+
required: [dataSet, period, orgUnit, dataValues]
|
|
131
|
+
properties:
|
|
132
|
+
dataSet: { type: string, format: dhis2-uid }
|
|
133
|
+
period: { type: string, format: dhis2-period }
|
|
134
|
+
orgUnit: { type: string, format: dhis2-uid }
|
|
135
|
+
attributeOptionCombo: { type: string, format: dhis2-uid }
|
|
136
|
+
dataValues:
|
|
137
|
+
type: array
|
|
138
|
+
items:
|
|
139
|
+
type: object
|
|
140
|
+
required: [dataElement, value]
|
|
141
|
+
properties:
|
|
142
|
+
dataElement: { type: string, format: dhis2-uid }
|
|
143
|
+
categoryOptionCombo: { type: string, format: dhis2-uid }
|
|
144
|
+
value: { type: string }
|
|
145
|
+
|
|
146
|
+
steps:
|
|
147
|
+
# The facade's one paged search. `questionnaire` is the canonical of the form, so this asks
|
|
148
|
+
# for every capture against one data set's form and nothing else. There is no _lastUpdated
|
|
149
|
+
# here -- the store is a receipt spool, and its cursor is the opaque `page` token on the
|
|
150
|
+
# Bundle's next link, which is what fhir-nightly-window-sync.yaml pages a real server by.
|
|
151
|
+
read_captures:
|
|
152
|
+
block: http.request
|
|
153
|
+
config:
|
|
154
|
+
connection: fhir-facade
|
|
155
|
+
path: /QuestionnaireResponse
|
|
156
|
+
method: GET
|
|
157
|
+
query:
|
|
158
|
+
questionnaire: "http://example.org/fhir/Questionnaire/${params.questionnaire_uid}"
|
|
159
|
+
_count: "${params.page_size}"
|
|
160
|
+
headers:
|
|
161
|
+
Accept: application/fhir+json
|
|
162
|
+
|
|
163
|
+
# One Bundle in, one DHIS2 document out. The whole response has to be visible at once,
|
|
164
|
+
# because the envelope comes from the extensions and the subject while the values come from
|
|
165
|
+
# the items, so this is one program rather than a map over the entries.
|
|
166
|
+
#
|
|
167
|
+
# Only `completed` responses are translated. An aggregate response that is still in-progress
|
|
168
|
+
# is a half-filled form, and importing one would claim a reporting period is answered.
|
|
169
|
+
to_data_value_set:
|
|
170
|
+
block: transform.jq
|
|
171
|
+
depends_on: [read_captures]
|
|
172
|
+
config:
|
|
173
|
+
input: "${steps.read_captures.output.body}"
|
|
174
|
+
program: |
|
|
175
|
+
[.entry[]?.resource | select(.status == "completed")]
|
|
176
|
+
| .[0] as $r
|
|
177
|
+
| ($r.extension[] | select(.url | endswith("/d2-period")).extension[]
|
|
178
|
+
| select(.url == "iso").valueString) as $period
|
|
179
|
+
# Items nest one level per section and one per disaggregated data element, so the
|
|
180
|
+
# leaves are found by recursion. An item carrying no answer is skipped entirely: a
|
|
181
|
+
# question that was asked and not answered must not import a zero.
|
|
182
|
+
| {dataSet: ($r.questionnaire | split("/") | last),
|
|
183
|
+
period: $period,
|
|
184
|
+
orgUnit: ($r.subject.reference | ltrimstr("Location/")),
|
|
185
|
+
attributeOptionCombo: (
|
|
186
|
+
[$r.extension[] | select(.url | endswith("/d2-attribute-option-combo"))][0]
|
|
187
|
+
| .valueCoding.code),
|
|
188
|
+
dataValues: [
|
|
189
|
+
$r.item[] | recurse(.item[]?)
|
|
190
|
+
| select((.answer // []) | length > 0)
|
|
191
|
+
| (.linkId | split(".")) as $key
|
|
192
|
+
| (.answer[0] | (.valueDecimal // .valueInteger // .valueString // .valueBoolean)) as $v
|
|
193
|
+
| {dataElement: $key[0], value: ($v | tostring)}
|
|
194
|
+
+ (if ($key | length) > 1 then {categoryOptionCombo: $key[1]} else {} end)]}
|
|
195
|
+
| del(.attributeOptionCombo | select(. == null))
|
|
196
|
+
|
|
197
|
+
# The gate is also the waypoint: every step past it references the validated value, so the
|
|
198
|
+
# import provably received a document whose ids and period the formats admit.
|
|
199
|
+
gate:
|
|
200
|
+
block: validate.schema
|
|
201
|
+
depends_on: [to_data_value_set]
|
|
202
|
+
config:
|
|
203
|
+
input: "${steps.to_data_value_set.output.value}"
|
|
204
|
+
schema: dhis2-data-value-set
|
|
205
|
+
|
|
206
|
+
# Dry run first, always. On /api/dataValueSets a dry run is dryRun=true; on /api/tracker the
|
|
207
|
+
# same intention is importMode=VALIDATE, and on /api/metadata it is importMode=VALIDATE too.
|
|
208
|
+
# Reaching for the wrong spelling does not fail -- it imports.
|
|
209
|
+
rehearse:
|
|
210
|
+
block: dhis2.data_value_set_import
|
|
211
|
+
depends_on: [gate]
|
|
212
|
+
config:
|
|
213
|
+
connection: dhis2-demo
|
|
214
|
+
data_values: "${steps.gate.output.value}"
|
|
215
|
+
dry_run: true
|
|
216
|
+
# ALL means one refused value refuses the whole document, which is what a rehearsal
|
|
217
|
+
# wants: the answer is "would all of this land", not "how much of it".
|
|
218
|
+
atomic_mode: ALL
|
|
219
|
+
|
|
220
|
+
# Second, and only because the rehearsal passed. The default all_success edge is what
|
|
221
|
+
# enforces that: a refused rehearsal leaves this skipped rather than writing anyway.
|
|
222
|
+
import:
|
|
223
|
+
block: dhis2.data_value_set_import
|
|
224
|
+
depends_on: [rehearse]
|
|
225
|
+
config:
|
|
226
|
+
connection: dhis2-demo
|
|
227
|
+
data_values: "${steps.gate.output.value}"
|
|
228
|
+
dry_run: false
|
|
229
|
+
import_strategy: CREATE_AND_UPDATE
|
|
230
|
+
atomic_mode: ALL
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# The code mapping fetched from the server at run time instead of written into the document.
|
|
2
|
+
#
|
|
3
|
+
# THE FLOW, END TO END. Fetch one ConceptMap off the FHIR server, turn its groups into a flat
|
|
4
|
+
# lookup, and apply that lookup to the coded answers of a capture -- so the document carries no
|
|
5
|
+
# codes at all and a mapping published today is in force tonight.
|
|
6
|
+
#
|
|
7
|
+
# THE POINT. questionnaire-response-to-data-values.yaml holds its table in params, which is
|
|
8
|
+
# right when the mapping is a decision your project made. But a terminology mapping is not that:
|
|
9
|
+
# it belongs to whoever publishes the terminology, and FHIR has a resource for exactly this.
|
|
10
|
+
# A ConceptMap is a published, versioned, addressable translation between two code systems, and
|
|
11
|
+
# a pipeline that fetches it can never drift from what the server means. When an option is
|
|
12
|
+
# added to a DHIS2 option set, the regenerated ConceptMap has it, and NOTHING here changes.
|
|
13
|
+
#
|
|
14
|
+
# WHAT ARRIVES -- the map. `d2w fhir serve` publishes one ConceptMap per DHIS2 option set,
|
|
15
|
+
# `d2-os-<optionSetUid>-cm`, with TWO groups distinguished by `group.target`:
|
|
16
|
+
#
|
|
17
|
+
# {"resourceType": "ConceptMap", "id": "d2-os-cshsFEran9U-cm",
|
|
18
|
+
# "identifier": {"system": "http://dhis2.org/fhir/id/option-set", "value": "cshsFEran9U"},
|
|
19
|
+
# "group": [
|
|
20
|
+
# {"source": ".../CodeSystem/d2-os-cshsFEran9U-cs",
|
|
21
|
+
# "target": "http://dhis2.org/fhir/id/option",
|
|
22
|
+
# "element": [{"code": "YQe3PFbATvz", "display": "NVP only",
|
|
23
|
+
# "target": [{"code": "YQe3PFbATvz", "equivalence": "equal"}]}]},
|
|
24
|
+
# {"source": ".../CodeSystem/d2-os-cshsFEran9U-cs",
|
|
25
|
+
# "target": "http://dhis2.org/fhir/id/option-code",
|
|
26
|
+
# "element": [{"code": "YQe3PFbATvz",
|
|
27
|
+
# "target": [{"code": "NVP-only", "equivalence": "equal"}]}]}]}
|
|
28
|
+
#
|
|
29
|
+
# The uid group is always complete; the code group only covers options DHIS2 gave a code, so
|
|
30
|
+
# it may be partial or missing entirely. `target_system` below picks which group is in force,
|
|
31
|
+
# and picking the uid group is what makes this safe by default.
|
|
32
|
+
#
|
|
33
|
+
# WHAT ARRIVES -- the capture. A coded answer is a valueCoding naming a concept of the option
|
|
34
|
+
# set's own CodeSystem, and it is the `code` that has to be translated:
|
|
35
|
+
#
|
|
36
|
+
# {"linkId": "s46m5MS0hxu",
|
|
37
|
+
# "answer": [{"valueCoding": {"system": ".../d2-os-cshsFEran9U-cs",
|
|
38
|
+
# "code": "dTVOMl2RHg3", "display": "AZT and NVP"}}]}
|
|
39
|
+
#
|
|
40
|
+
# WHAT LEAVES. The usual `/api/dataValueSets` document, its values translated:
|
|
41
|
+
#
|
|
42
|
+
# {"dataSet": "lyLU2wR22tC", "period": "202608", "orgUnit": "dczh6Jfd4no",
|
|
43
|
+
# "dataValues": [{"dataElement": "s46m5MS0hxu", "value": "dTVOMl2RHg3"}]}
|
|
44
|
+
#
|
|
45
|
+
# WHERE A READER CHANGES IT. `concept_map_id` names the map, and `target_system` picks which
|
|
46
|
+
# of its groups is authoritative -- `.../id/option` for DHIS2 uids, `.../id/option-code` for
|
|
47
|
+
# DHIS2 codes. Neither the source codes nor the targets appear anywhere in this file.
|
|
48
|
+
#
|
|
49
|
+
# THE OTHER WAY. The facade also answers `GET /ConceptMap/$translate?system=&code=`, one
|
|
50
|
+
# concept per call, returning Parameters with a `result` boolean and a `match` part. That is
|
|
51
|
+
# the right shape for a handful of lookups and the wrong one for a bundle: fetching the map
|
|
52
|
+
# once and applying it in-process is one request rather than one per answer.
|
|
53
|
+
#
|
|
54
|
+
# dg run --local examples/fhir/fhir-conceptmap-driven-mapping.yaml
|
|
55
|
+
# dg run --local examples/fhir/fhir-conceptmap-driven-mapping.yaml \
|
|
56
|
+
# -p target_system=http://dhis2.org/fhir/id/option-code
|
|
57
|
+
|
|
58
|
+
format: dirigent/v1
|
|
59
|
+
kind: pipeline
|
|
60
|
+
code: fhir-conceptmap-driven-mapping
|
|
61
|
+
name: A ConceptMap drives the mapping
|
|
62
|
+
description: |
|
|
63
|
+
Fetch a `ConceptMap` from the FHIR server, flatten it into a lookup, and translate a
|
|
64
|
+
capture's coded answers through it -- so no code appears in this document at all.
|
|
65
|
+
|
|
66
|
+
A `d2w fhir serve` option-set map carries two groups distinguished by `group.target`: one to
|
|
67
|
+
the DHIS2 option uid (always complete) and one to the DHIS2 option code (only where the
|
|
68
|
+
option has one). `target_system` picks which is in force.
|
|
69
|
+
|
|
70
|
+
tags: [fhir, dhis2, cross-boundary]
|
|
71
|
+
|
|
72
|
+
requires:
|
|
73
|
+
blocks:
|
|
74
|
+
- http.request
|
|
75
|
+
- value.const
|
|
76
|
+
- transform.jq
|
|
77
|
+
- dhis2.data_value_set_import
|
|
78
|
+
|
|
79
|
+
connections:
|
|
80
|
+
fhir-facade:
|
|
81
|
+
kind: http
|
|
82
|
+
config:
|
|
83
|
+
base_url: http://localhost:8095
|
|
84
|
+
timeout: 30s
|
|
85
|
+
# The public DHIS2 play server, carried inline so this document runs standalone. Against an
|
|
86
|
+
# instance you own, name a connection that instance holds instead of carrying one.
|
|
87
|
+
dhis2-demo:
|
|
88
|
+
kind: dhis2
|
|
89
|
+
config:
|
|
90
|
+
base_url: https://play.im.dhis2.org/stable-2-43-1
|
|
91
|
+
basic_username: admin
|
|
92
|
+
basic_password: district
|
|
93
|
+
timeout: 60s
|
|
94
|
+
|
|
95
|
+
params:
|
|
96
|
+
type: object
|
|
97
|
+
properties:
|
|
98
|
+
concept_map_id:
|
|
99
|
+
type: string
|
|
100
|
+
description: The published map. On this facade, d2-os-<optionSetUid>-cm.
|
|
101
|
+
default: d2-os-cshsFEran9U-cm
|
|
102
|
+
target_system:
|
|
103
|
+
type: string
|
|
104
|
+
description: Which group of the map is authoritative -- the uid group or the code group.
|
|
105
|
+
default: http://dhis2.org/fhir/id/option
|
|
106
|
+
data_set:
|
|
107
|
+
type: string
|
|
108
|
+
default: lyLU2wR22tC
|
|
109
|
+
org_unit:
|
|
110
|
+
type: string
|
|
111
|
+
default: dczh6Jfd4no
|
|
112
|
+
period:
|
|
113
|
+
type: string
|
|
114
|
+
default: "202608"
|
|
115
|
+
|
|
116
|
+
steps:
|
|
117
|
+
# A read by id, not a search: a ConceptMap's id is stable and the read is byte-faithful to
|
|
118
|
+
# what the project published. `GET /ConceptMap?identifier=<option-set system>|<uid>` is the
|
|
119
|
+
# same map found the other way round, by the DHIS2 option set it came from.
|
|
120
|
+
fetch_map:
|
|
121
|
+
block: http.request
|
|
122
|
+
config:
|
|
123
|
+
connection: fhir-facade
|
|
124
|
+
path: "/ConceptMap/${params.concept_map_id}"
|
|
125
|
+
method: GET
|
|
126
|
+
headers:
|
|
127
|
+
Accept: application/fhir+json
|
|
128
|
+
|
|
129
|
+
# Groups in, one flat object out: source code -> target code. Only groups whose target is
|
|
130
|
+
# the system asked for contribute, so switching target_system switches the whole mapping
|
|
131
|
+
# without touching this program. `equivalence` is kept alongside the code because a map is
|
|
132
|
+
# allowed to say `wider` or `inexact`, and a translation that quietly used one of those
|
|
133
|
+
# would be reporting a different fact than the one that was captured.
|
|
134
|
+
build_lookup:
|
|
135
|
+
block: transform.jq
|
|
136
|
+
depends_on: [fetch_map]
|
|
137
|
+
config:
|
|
138
|
+
input:
|
|
139
|
+
map: "${steps.fetch_map.output.body}"
|
|
140
|
+
target_system: "${params.target_system}"
|
|
141
|
+
program: |
|
|
142
|
+
. as {$map, $target_system}
|
|
143
|
+
| [$map.group[] | select(.target == $target_system) | .element[]
|
|
144
|
+
| {key: .code,
|
|
145
|
+
value: {code: .target[0].code, equivalence: (.target[0].equivalence // "equal")}}]
|
|
146
|
+
| from_entries
|
|
147
|
+
| if . == {} then
|
|
148
|
+
error("\($target_system) is not a target group of this ConceptMap")
|
|
149
|
+
else . end
|
|
150
|
+
|
|
151
|
+
# The capture, inline so the document runs with nothing set up. Its answers are codes of the
|
|
152
|
+
# option set's own CodeSystem -- which is exactly the source side of the map fetched above.
|
|
153
|
+
receive:
|
|
154
|
+
block: value.const
|
|
155
|
+
config:
|
|
156
|
+
value:
|
|
157
|
+
- linkId: s46m5MS0hxu
|
|
158
|
+
code: dTVOMl2RHg3
|
|
159
|
+
- linkId: p1MDHOT6ENy
|
|
160
|
+
code: YQe3PFbATvz
|
|
161
|
+
# Coded against something this map has never heard of. It must not be invented into a
|
|
162
|
+
# value, and it must not disappear without a word either.
|
|
163
|
+
- linkId: cZnQDuF3IDz
|
|
164
|
+
code: NotAMappedCode
|
|
165
|
+
|
|
166
|
+
# The join. Both sides ride in on `input` -- the lookup as a value, not as text spliced into
|
|
167
|
+
# the program: a ${...} reference resolves into a jq program as Python's own rendering of the
|
|
168
|
+
# object, which is not jq syntax and does not compile. A program takes its data through
|
|
169
|
+
# input, always, and this is the step where that rule bites.
|
|
170
|
+
translate_answers:
|
|
171
|
+
block: transform.jq
|
|
172
|
+
depends_on: [build_lookup, receive]
|
|
173
|
+
config:
|
|
174
|
+
input:
|
|
175
|
+
answers: "${steps.receive.output.value}"
|
|
176
|
+
lookup: "${steps.build_lookup.output.value}"
|
|
177
|
+
program: |
|
|
178
|
+
. as {$answers, $lookup}
|
|
179
|
+
| [$answers[]
|
|
180
|
+
| . as $answer
|
|
181
|
+
| $lookup[$answer.code] as $hit
|
|
182
|
+
| {linkId: $answer.linkId,
|
|
183
|
+
source: $answer.code,
|
|
184
|
+
translated: $hit.code,
|
|
185
|
+
equivalence: $hit.equivalence}]
|
|
186
|
+
|
|
187
|
+
# Only `equal` becomes a data value. Anything else -- a wider match, a code the map does not
|
|
188
|
+
# carry -- is reported instead of imported, which is the whole reason the equivalence was
|
|
189
|
+
# carried this far.
|
|
190
|
+
to_data_values:
|
|
191
|
+
block: transform.jq
|
|
192
|
+
depends_on: [translate_answers]
|
|
193
|
+
config:
|
|
194
|
+
input:
|
|
195
|
+
answers: "${steps.translate_answers.output.value}"
|
|
196
|
+
data_set: "${params.data_set}"
|
|
197
|
+
org_unit: "${params.org_unit}"
|
|
198
|
+
period: "${params.period}"
|
|
199
|
+
program: |
|
|
200
|
+
. as {$answers, $data_set, $org_unit, $period}
|
|
201
|
+
| {document: {
|
|
202
|
+
dataSet: $data_set,
|
|
203
|
+
period: $period,
|
|
204
|
+
orgUnit: $org_unit,
|
|
205
|
+
dataValues: [$answers[]
|
|
206
|
+
| select(.translated != null and .equivalence == "equal")
|
|
207
|
+
| {dataElement: .linkId, value: .translated}]},
|
|
208
|
+
untranslated: [$answers[] | select(.translated == null) | .source],
|
|
209
|
+
inexact: [$answers[] | select(.translated != null and .equivalence != "equal")]}
|
|
210
|
+
|
|
211
|
+
# The translated set goes to DHIS2 as a rehearsal: dry_run is fixed on, because the point of
|
|
212
|
+
# this example is the mapping, and the import step is here so the whole leg is exercised
|
|
213
|
+
# against a real instance without writing to a shared one. Only the document is sent; the
|
|
214
|
+
# untranslated and inexact lists stay on the step above, where a reader looks for them.
|
|
215
|
+
rehearse:
|
|
216
|
+
block: dhis2.data_value_set_import
|
|
217
|
+
depends_on: [to_data_values]
|
|
218
|
+
config:
|
|
219
|
+
connection: dhis2-demo
|
|
220
|
+
data_values: ${steps.to_data_values.output.value.document}
|
|
221
|
+
dry_run: true
|
|
222
|
+
import_strategy: CREATE_AND_UPDATE
|