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.
Files changed (33) hide show
  1. dirigent_integration/__init__.py +29 -0
  2. dirigent_integration/py.typed +0 -0
  3. dirigent_integration/shelves/dhis2-analytics-to-csv-report.yaml +174 -0
  4. dirigent_integration/shelves/dhis2-export-to-s3.yaml +66 -0
  5. dirigent_integration/shelves/dhis2-metadata-snapshot.yaml +143 -0
  6. dirigent_integration/shelves/dhis2-tracker-weekly-window.yaml +162 -0
  7. dirigent_integration/shelves/dhis2-values-per-org-unit-to-parquet.yaml +186 -0
  8. dirigent_integration/shelves/dhis2-values-to-parquet.yaml +64 -0
  9. dirigent_integration/shelves/fhir/README.md +79 -0
  10. dirigent_integration/shelves/fhir/dhis2-to-fhir-observations.yaml +212 -0
  11. dirigent_integration/shelves/fhir/fhir-capture-bundle-to-data-values.yaml +230 -0
  12. dirigent_integration/shelves/fhir/fhir-conceptmap-driven-mapping.yaml +222 -0
  13. dirigent_integration/shelves/fhir/fhir-encounter-to-event.yaml +248 -0
  14. dirigent_integration/shelves/fhir/fhir-measure-report-to-analytics-check.yaml +205 -0
  15. dirigent_integration/shelves/fhir/fhir-nightly-window-sync.yaml +182 -0
  16. dirigent_integration/shelves/fhir/fhir-patient-to-tracked-entity.yaml +225 -0
  17. dirigent_integration/shelves/fhir/fhir-questionnaire-response-to-data-values.yaml +187 -0
  18. dirigent_integration/shelves/fhir/fhir-subscription-webhook-to-dhis2.yaml +218 -0
  19. dirigent_integration/shelves/inbound/README.md +32 -0
  20. dirigent_integration/shelves/inbound/csv-drop-to-data-values.yaml +176 -0
  21. dirigent_integration/shelves/inbound/fan-out-per-facility-import.yaml +202 -0
  22. dirigent_integration/shelves/inbound/fhir-observations-to-data-values.yaml +193 -0
  23. dirigent_integration/shelves/inbound/http-json-to-data-values.yaml +151 -0
  24. dirigent_integration/shelves/inbound/outside-to-dhis2-with-checks.yaml +246 -0
  25. dirigent_integration/shelves/inbound/parquet-lakehouse-to-dhis2.yaml +158 -0
  26. dirigent_integration/shelves/inbound/webhook-payload-to-tracker-event.yaml +176 -0
  27. dirigent_integration/shelves/inbound/weekly-window-pull-and-import.yaml +175 -0
  28. dirigent_integration/shelves/parquet-to-dhis2-import.yaml +195 -0
  29. dirigent_integration-0.23.2.dist-info/METADATA +231 -0
  30. dirigent_integration-0.23.2.dist-info/RECORD +33 -0
  31. dirigent_integration-0.23.2.dist-info/WHEEL +4 -0
  32. dirigent_integration-0.23.2.dist-info/entry_points.txt +3 -0
  33. dirigent_integration-0.23.2.dist-info/licenses/LICENSE +15 -0
@@ -0,0 +1,176 @@
1
+ # An event posted by an outside system, landing in DHIS2 the moment it is captured.
2
+ #
3
+ # The other examples on this shelf poll or wait. This one is pushed: a mobile capture app,
4
+ # an EMR, a lab middleware POSTs one encounter to this pipeline's webhook and the run starts
5
+ # with that encounter already in its parameters. Nothing is scheduled and nothing is
6
+ # scanned.
7
+ #
8
+ # WHERE THIS LANDS, AND WHY IT IS NOT A TRACKER EVENT. An event-shaped capture is what
9
+ # arrives, but `dhis2.tracker` in this pack READS `/api/tracker` -- tracked entities,
10
+ # enrollments, events -- and has no write side, so there is nothing to hand an event to
11
+ # today. The encounter is therefore summed into the aggregate figures it contributes to and
12
+ # imported as a data value set, which is what an aggregate DHIS2 wants from an event stream
13
+ # anyway. When a tracker write block lands, only the reshape and the import change; the
14
+ # intake, the mapping table and the guard do not.
15
+ #
16
+ # The hops, and the shape at each one:
17
+ #
18
+ # 1. the POST -> {encounter: {facility, occurred_at, items: [{code, quantity}]}}
19
+ # 2. the mapping -> three params, each one path into that body and nothing else
20
+ # 3. reshape -> {dataValues: [{dataElement, period, orgUnit, value}]}, one value per
21
+ # distinct item code, quantities summed
22
+ # 4. gate -> the same, refused unless the ids and the period are DHIS2's own
23
+ # 5. import -> the instance's summary
24
+ #
25
+ # THE MAPPING IS THE WHOLE SECURITY BOUNDARY. A caller reaches exactly the three parameters
26
+ # named under params_from_payload, and each mapped value is validated against the params
27
+ # schema like any other run's. A payload carrying a connection code, a dry_run: false, or a
28
+ # different org unit reaches none of them, because none of them is mapped.
29
+ #
30
+ # The period is derived rather than accepted. A capture knows when it happened; it does not
31
+ # get to say which DHIS2 month it belongs to, because that is the receiving instance's
32
+ # vocabulary and a caller that gets it wrong would silently write into the wrong month.
33
+ #
34
+ # Locally, the payload is supplied as parameters, which is exactly what a delivery does:
35
+ #
36
+ # dg run --local examples/inbound/webhook-payload-to-tracker-event.yaml \
37
+ # -p facility=DiszpKrYNg8 -p occurred_at=2026-06-14T09:20:00Z
38
+ #
39
+ # Against an instance, where the endpoint exists:
40
+ #
41
+ # dg apply examples/inbound/webhook-payload-to-tracker-event.yaml
42
+ # dg webhook rotate-token webhook-payload-to-tracker-event capture-posted
43
+ # curl -X POST "$DG_URL/hooks/$TOKEN" -d '{"encounter": {
44
+ # "facility": "DiszpKrYNg8", "occurred_at": "2026-06-14T09:20:00Z",
45
+ # "items": [{"code": "EXPOSED_REGISTERED", "quantity": 2}]}}'
46
+
47
+ format: dirigent/v1
48
+ kind: pipeline
49
+ code: webhook-payload-to-tracker-event
50
+ name: A pushed encounter into DHIS2
51
+ description: |
52
+ An outside system POSTs one captured encounter; the run reshapes it into a DHIS2 data
53
+ value set and imports it.
54
+
55
+ The pack's `dhis2.tracker` block reads `/api/tracker` and does not write it, so an
56
+ event-shaped capture lands as the aggregate figures it contributes to.
57
+
58
+ tags: [inbound, dhis2, webhook, transform, validate, cross-boundary]
59
+
60
+ requires:
61
+ blocks:
62
+ - transform.jq
63
+ - validate.schema
64
+ - dhis2.data_value_set_import
65
+
66
+ connections:
67
+ dhis2-demo:
68
+ kind: dhis2
69
+ config:
70
+ base_url: https://play.im.dhis2.org/stable-2-43-1
71
+ basic_username: admin
72
+ basic_password: district
73
+ timeout: 60s
74
+
75
+ params:
76
+ type: object
77
+ properties:
78
+ facility:
79
+ type: string
80
+ description: The uid of the facility the encounter was captured at.
81
+ default: DiszpKrYNg8
82
+ occurred_at:
83
+ type: string
84
+ format: date-time
85
+ description: When the encounter happened, which is what the DHIS2 period is derived from.
86
+ default: "2026-06-14T09:20:00Z"
87
+ items:
88
+ type: array
89
+ description: What was given or done, in the sending system's own item codes.
90
+ default:
91
+ - { code: EXPOSED_REGISTERED, quantity: 2 }
92
+ - { code: NVP_WITHIN_72H, quantity: 1 }
93
+ - { code: EXPOSED_REGISTERED, quantity: 1 }
94
+ items:
95
+ type: object
96
+ required: [code, quantity]
97
+ properties:
98
+ code: { type: string }
99
+ quantity: { type: integer }
100
+ dry_run:
101
+ type: boolean
102
+ description: Whether the instance validates the import and writes nothing.
103
+ default: true
104
+
105
+ schemas:
106
+ data-value-set:
107
+ type: object
108
+ required: [dataValues]
109
+ properties:
110
+ dataValues:
111
+ type: array
112
+ minItems: 1
113
+ items:
114
+ type: object
115
+ required: [dataElement, period, orgUnit, value]
116
+ properties:
117
+ dataElement: { type: string, format: dhis2-uid }
118
+ period: { type: string, format: dhis2-period }
119
+ orgUnit: { type: string, format: dhis2-uid }
120
+ value: { type: string }
121
+
122
+ steps:
123
+ # One encounter can carry the same item twice -- two children registered in one visit --
124
+ # so the items are grouped and summed before they become data values. Importing them as
125
+ # two values would have the second silently replace the first rather than add to it.
126
+ reshape:
127
+ block: transform.jq
128
+ config:
129
+ input:
130
+ facility: "${params.facility}"
131
+ occurred_at: "${params.occurred_at}"
132
+ items: "${params.items}"
133
+ lookup:
134
+ EXPOSED_REGISTERED: x0PshcPLSk1
135
+ NVP_WITHIN_72H: wZqi8EXN5x4
136
+ program: |
137
+ .lookup as $lookup
138
+ | .facility as $ou
139
+ # A monthly DHIS2 period is YYYYMM, which is the first seven characters of an ISO
140
+ # instant with the dash taken out. The capture's own clock decides the month.
141
+ | (.occurred_at[0:7] | gsub("-"; "")) as $period
142
+ | {dataValues: [
143
+ .items
144
+ | group_by(.code)[]
145
+ | {dataElement: $lookup[.[0].code],
146
+ period: $period,
147
+ orgUnit: $ou,
148
+ value: ([.[].quantity] | add | tostring)}
149
+ ]}
150
+
151
+ # An item code the table does not know left dataElement null, and null is not a uid: the
152
+ # gate is what turns a sender's typo into a failed run instead of a refused import.
153
+ gate:
154
+ block: validate.schema
155
+ depends_on: [reshape]
156
+ config:
157
+ input: "${steps.reshape.output.value}"
158
+ schema: data-value-set
159
+
160
+ import:
161
+ block: dhis2.data_value_set_import
162
+ depends_on: [gate]
163
+ config:
164
+ connection: dhis2-demo
165
+ data_values: "${steps.gate.output.value}"
166
+ dry_run: "${params.dry_run}"
167
+
168
+ triggers:
169
+ webhooks:
170
+ - code: capture-posted
171
+ name: One encounter, pushed
172
+ description: The capture app POSTs an encounter as it is recorded.
173
+ params_from_payload:
174
+ facility: "$.encounter.facility"
175
+ occurred_at: "$.encounter.occurred_at"
176
+ items: "$.encounter.items"
@@ -0,0 +1,175 @@
1
+ # A weekly pull bounded by the run's own window, not by "now".
2
+ #
3
+ # THE WINDOW IS THE POINT. A schedule says when the run starts; the window says what the run
4
+ # covers. Monday's run is meant to read the week that just closed, and it must read exactly
5
+ # that week whether it fires on time, fires an hour late after a worker restart, or is
6
+ # backfilled next year. ${run.window.start} and ${run.window.end} are that interval, and
7
+ # they are half-open -- start included, end excluded -- so consecutive firings tile the
8
+ # timeline with no overlap and no gap. A pull bounded by "now minus seven days" has neither
9
+ # property.
10
+ #
11
+ # Nothing in this document declares the window: the cadence at the bottom computes it. So an
12
+ # ad hoc run has to be given one, and a run carrying none refuses the reference rather than
13
+ # resolving it to nothing and asking the outside system for its entire history.
14
+ #
15
+ # The hops, and the shape at each one:
16
+ #
17
+ # 1. pull -> the USGS earthquake catalogue, a GeoJSON FeatureCollection:
18
+ # {type, metadata, features: [...]} -- one feature per event in the week
19
+ # 2. reshape -> {dataSet, dataValues: [{dataElement, period, orgUnit, value}]}: one
20
+ # value, the week's event count, filed against the ISO week the window
21
+ # opened in
22
+ # 3. import -> the instance's summary
23
+ # 4. signed_off -> the sensor that waits for a person to sign the week off
24
+ #
25
+ # A public catalogue that takes an explicit time range is what makes this runnable with no
26
+ # setup; the mechanism is the same for a district register or a lab system that takes
27
+ # ?from=&to=. The catalogue's event counts stand in for a surveillance system's weekly case
28
+ # counts -- that pairing is a stand-in and nothing else here is.
29
+ #
30
+ # A count of zero is a measurement and is imported as one -- the week genuinely had no
31
+ # qualifying event. That is the opposite of the empty cell in csv-drop-to-data-values.yaml,
32
+ # where nothing was reported at all.
33
+ #
34
+ # dg run --local examples/inbound/weekly-window-pull-and-import.yaml \
35
+ # --window 2026-08-03..2026-08-10
36
+ # dg apply examples/inbound/weekly-window-pull-and-import.yaml
37
+ # dg backfill weekly-window-pull-and-import --schedule monday-morning \
38
+ # --from 2026-06-01T06:00:00Z --to 2026-08-31T06:00:00Z --dry-run
39
+
40
+ format: dirigent/v1
41
+ kind: pipeline
42
+ code: weekly-window-pull-and-import
43
+ name: A weekly windowed pull into DHIS2
44
+ description: |
45
+ A Monday schedule whose runs pull the week that just closed -- `${run.window.start}` up
46
+ to but not including `${run.window.end}` -- reshape it into a data value set, import it,
47
+ and wait for the week to be signed off.
48
+
49
+ tags: [inbound, dhis2, http, schedule, transform, cross-boundary]
50
+
51
+ requires:
52
+ blocks:
53
+ - http.request
54
+ - transform.jq
55
+ - dhis2.data_value_set_import
56
+ - dhis2.data_set_complete
57
+
58
+ connections:
59
+ dhis2-demo:
60
+ kind: dhis2
61
+ config:
62
+ base_url: https://play.im.dhis2.org/stable-2-43-1
63
+ basic_username: admin
64
+ basic_password: district
65
+ timeout: 60s
66
+ catalogue:
67
+ kind: http
68
+ config:
69
+ base_url: https://earthquake.usgs.gov
70
+ timeout: 60s
71
+
72
+ params:
73
+ type: object
74
+ properties:
75
+ min_magnitude:
76
+ type: number
77
+ description: What counts as an event worth reporting; the schedule pins it per firing.
78
+ default: 5
79
+ data_element:
80
+ type: string
81
+ description: The uid the weekly count lands on; a weekly surveillance element.
82
+ default: UsSUX0cpKsH
83
+ org_unit:
84
+ type: string
85
+ description: The facility the weekly count is reported for.
86
+ default: DiszpKrYNg8
87
+ data_set:
88
+ type: string
89
+ description: The weekly data set the count belongs to, and whose sign-off closes it.
90
+ default: Nyh6laLdBEJ
91
+ dry_run:
92
+ type: boolean
93
+ description: Whether the instance validates the import and writes nothing.
94
+ default: true
95
+
96
+ steps:
97
+ # Both ends of the interval go on the wire, because the outside system is the one that
98
+ # must not double-count: >= start and < end is the same half-open rule the window is.
99
+ pull:
100
+ block: http.request
101
+ config:
102
+ connection: catalogue
103
+ path: /fdsnws/event/1/query
104
+ method: GET
105
+ query:
106
+ format: geojson
107
+ starttime: "${run.window.start}"
108
+ endtime: "${run.window.end}"
109
+ minmagnitude: "${params.min_magnitude}"
110
+
111
+ # A DHIS2 weekly period is YYYYWnn, and getting there from an instant has two traps.
112
+ # strptime alone leaves the weekday and year-day fields empty, so the week number comes
113
+ # out wrong unless the parse is pushed through mktime|gmtime first; and DHIS2 writes W6,
114
+ # not W06, so the zero-padded %V is put through tonumber. The date is truncated to ten
115
+ # characters so the same program reads both an instant and a plain date.
116
+ reshape:
117
+ block: transform.jq
118
+ depends_on: [pull]
119
+ config:
120
+ input:
121
+ events: "${steps.pull.output.body}"
122
+ window_start: "${run.window.start}"
123
+ data_element: "${params.data_element}"
124
+ org_unit: "${params.org_unit}"
125
+ data_set: "${params.data_set}"
126
+ program: |
127
+ (.window_start[0:10] | strptime("%Y-%m-%d") | mktime | gmtime) as $opened
128
+ # The envelope names its data set because this element belongs to two of them, and
129
+ # an import that cannot tell which set a value is for refuses the whole document.
130
+ | {dataSet: .data_set,
131
+ dataValues: [{
132
+ dataElement: .data_element,
133
+ period: "\($opened | strftime("%G"))W\($opened | strftime("%V") | tonumber)",
134
+ orgUnit: .org_unit,
135
+ value: (.events.features | length | tostring)
136
+ }]}
137
+
138
+ import:
139
+ block: dhis2.data_value_set_import
140
+ depends_on: [reshape]
141
+ config:
142
+ connection: dhis2-demo
143
+ data_values: "${steps.reshape.output.value}"
144
+ dry_run: "${params.dry_run}"
145
+ # A backfilled week must land on top of whatever a previous run put there, because
146
+ # the catalogue revises events after the fact and the newer read is the truer one.
147
+ import_strategy: CREATE_AND_UPDATE
148
+
149
+ # Observes, never registers -- the same sensor, and the same reasoning, as
150
+ # parquet-lakehouse-to-dhis2.yaml. The period is read back off the document that was
151
+ # imported rather than derived a second time: one derivation, one truth.
152
+ signed_off:
153
+ block: dhis2.data_set_complete
154
+ depends_on: [import]
155
+ poll: 30m
156
+ deadline: 72h
157
+ on_timeout: skip
158
+ config:
159
+ connection: dhis2-demo
160
+ data_set: "${params.data_set}"
161
+ period: "${steps.reshape.output.value.dataValues.0.period}"
162
+ org_unit: "${params.org_unit}"
163
+
164
+ triggers:
165
+ schedules:
166
+ - code: monday-morning
167
+ name: Monday, Freetown time
168
+ description: Reads the week that closed at six on Monday morning.
169
+ cron: "0 6 * * 1"
170
+ # The zone belongs to the schedule, so the window it derives is a week of the
171
+ # reporting country's clock rather than a fixed 168 hours from a server's idea of
172
+ # midnight.
173
+ timezone: Africa/Freetown
174
+ params:
175
+ min_magnitude: 5
@@ -0,0 +1,195 @@
1
+ # The other direction: a parquet file somebody drops in a bucket, ending up inside DHIS2.
2
+ #
3
+ # dhis2-values-to-parquet.yaml reads DHIS2 and writes parquet. This is the return leg, and it
4
+ # is the one that needs a gate: bytes arriving from outside are not a data value set until
5
+ # something has checked that they are. Three packs meet here -- storage-s3 for the drop,
6
+ # parquet for the decode, builtin for the reshape and the schema gate, dhis2 for the write --
7
+ # which is why it lives in the integration and in none of them.
8
+ #
9
+ # What happens, hop by hop:
10
+ #
11
+ # dropped storage.exists is a sensor: each poke is one cheap head request, "not yet" is
12
+ # the expected answer, and the run parks rather than holding a worker. min_size
13
+ # is what stops it opening a half-written object a producer is still uploading.
14
+ # decode convert.arrow parquet -> json, into the run's scratch space. Parquet is bytes and
15
+ # never travels as a value, so both ends of a conversion are URIs: source and
16
+ # target. The json it writes is an array of row objects, one per data value,
17
+ # because that is what a parquet table is -- rows and columns, with no envelope.
18
+ # load storage.read, that json object as a value. storage.read is the one door a value
19
+ # comes in by, and max_size is how much of it this step will hold: an object past
20
+ # it is refused rather than read half way.
21
+ # shape the list is wrapped: DHIS2 wants {"dataValues": [...]}, the file carries the bare
22
+ # list. This is the whole impedance mismatch between a columnar file and a DHIS2
23
+ # payload, and it is one line of jq.
24
+ # gate validate.schema against the carried schema below. Every dataElement and orgUnit
25
+ # must be a real DHIS2 uid and every period a real DHIS2 period, checked by the
26
+ # dhis2-uid and dhis2-period formats the dhis2 pack contributes -- so a typo is a
27
+ # refused run and not a partially-written instance. The gate has no `rule`, so the
28
+ # default all_success applies: the import below simply never starts if this fails.
29
+ # import dhis2.data_value_set_import of the value the gate handed on. atomic_mode ALL
30
+ # means one conflicted value refuses the whole set, which is the right default for
31
+ # a file: half an import is worse than none.
32
+ # signed_off dhis2.data_set_complete is a sensor, not a writer -- it waits for a human in
33
+ # DHIS2 to mark the set complete for that period and org unit. That is the honest
34
+ # end of a load: the data is in, and the run stays open until somebody stands
35
+ # behind it. on_timeout: skip ends the run cleanly when nobody does.
36
+ #
37
+ # To make it yours: point drop_uri at the object your producer writes, name your own data set,
38
+ # period and org unit, and turn dry_run off once you are importing into an instance you own.
39
+ # It is on by default because the play server is shared: the import is fully exercised and
40
+ # nothing is written. Which connection backs s3:// is an instance setting
41
+ # (storage_connections: {s3: <code>}).
42
+ #
43
+ # dg run --local examples/parquet-to-dhis2-import.yaml
44
+ # dg run --local examples/parquet-to-dhis2-import.yaml -p dry_run=false
45
+
46
+ format: dirigent/v1
47
+ kind: pipeline
48
+ code: parquet-to-dhis2-import
49
+ name: A parquet drop imported into DHIS2
50
+ description: Wait for a parquet object, check it, then import it as a DHIS2 data value set.
51
+
52
+ tags: [dhis2, parquet, s3, validate, cross-boundary]
53
+
54
+ requires:
55
+ blocks:
56
+ - storage.exists
57
+ - convert.arrow
58
+ - storage.read
59
+ - transform.jq
60
+ - validate.schema
61
+ - dhis2.data_value_set_import
62
+ - dhis2.data_set_complete
63
+
64
+ # The public DHIS2 play server, carried inline so this document runs standalone. Against an
65
+ # instance you own, name a connection that instance holds instead of carrying one.
66
+ connections:
67
+ dhis2-demo:
68
+ kind: dhis2
69
+ config:
70
+ base_url: https://play.im.dhis2.org/stable-2-43-1
71
+ basic_username: admin
72
+ basic_password: district
73
+ timeout: 60s
74
+
75
+ # Carried in the document, so the gate needs nothing handed to it and a --local run checks the
76
+ # same shape a server would. dhis2-uid and dhis2-period are formats the dhis2 pack contributes
77
+ # to the catalog; an instance without that pack cannot run this document at all, which is the
78
+ # point of naming them in requires.
79
+ schemas:
80
+ dhis2-data-value-set:
81
+ type: object
82
+ required: [dataValues]
83
+ additionalProperties: false
84
+ properties:
85
+ dataValues:
86
+ type: array
87
+ minItems: 1
88
+ items:
89
+ type: object
90
+ required: [dataElement, period, orgUnit, value]
91
+ properties:
92
+ dataElement: { type: string, format: dhis2-uid }
93
+ period: { type: string, format: dhis2-period }
94
+ orgUnit: { type: string, format: dhis2-uid }
95
+ categoryOptionCombo: { type: string, format: dhis2-uid }
96
+ attributeOptionCombo: { type: string, format: dhis2-uid }
97
+ value: { type: string }
98
+ comment: { type: string }
99
+
100
+ params:
101
+ type: object
102
+ properties:
103
+ drop_uri:
104
+ type: string
105
+ default: s3://dirigent-incoming/dhis2/data-values.parquet
106
+ description: The object the upstream producer writes; this run starts when it appears.
107
+ data_set:
108
+ type: string
109
+ default: BfMAe6Itzgt
110
+ period:
111
+ type: string
112
+ default: "202507"
113
+ org_unit:
114
+ type: string
115
+ default: vSbt6cezomG
116
+ dry_run:
117
+ type: boolean
118
+ default: true
119
+ description: On by default because the play server is shared; off on an instance you own.
120
+
121
+ steps:
122
+ dropped:
123
+ block: storage.exists
124
+ poll: 1m
125
+ deadline: 12h
126
+ # Nobody dropped a file inside the window: that is a quiet day, not a broken pipeline.
127
+ on_timeout: skip
128
+ config:
129
+ uri: ${params.drop_uri}
130
+ # A producer uploading a large object makes the key visible before the bytes are all
131
+ # there. A floor stops the decode reading a truncated file.
132
+ min_size: 1kb
133
+
134
+ decode:
135
+ block: convert.arrow
136
+ depends_on: [dropped]
137
+ config:
138
+ source: ${params.drop_uri}
139
+ from: parquet
140
+ to: json
141
+ # The run's own scratch space: these bytes exist only so the read below has something to
142
+ # decode, and the object the producer dropped is left exactly as it was.
143
+ target: ${run.scratch}/data-values.json
144
+
145
+ load:
146
+ block: storage.read
147
+ depends_on: [decode]
148
+ config:
149
+ # Where the conversion said it wrote, not the same path typed twice.
150
+ source: ${steps.decode.output.target}
151
+ # A data value set that fits a single import call also fits a step output; a file bigger
152
+ # than this is refused here rather than truncated into a wrong one.
153
+ max_size: 64mb
154
+
155
+ shape:
156
+ block: transform.jq
157
+ depends_on: [load]
158
+ config:
159
+ input: ${steps.load.output.value}
160
+ # The object literal is the envelope DHIS2 requires and a parquet table has no room for.
161
+ program: |
162
+ {dataValues: .}
163
+
164
+ gate:
165
+ block: validate.schema
166
+ depends_on: [shape]
167
+ config:
168
+ input: ${steps.shape.output.value}
169
+ schema: dhis2-data-value-set
170
+
171
+ import:
172
+ block: dhis2.data_value_set_import
173
+ depends_on: [gate]
174
+ config:
175
+ connection: dhis2-demo
176
+ # Read from the gate rather than from shape: the reference is what proves, in the
177
+ # document itself, that nothing reaches DHIS2 without passing the schema.
178
+ data_values: ${steps.gate.output.value}
179
+ dry_run: ${params.dry_run}
180
+ import_strategy: CREATE_AND_UPDATE
181
+ # One refused value refuses the whole file. A partially imported month is harder to
182
+ # reason about than a failed run, and the file can simply be dropped again.
183
+ atomic_mode: ALL
184
+
185
+ signed_off:
186
+ block: dhis2.data_set_complete
187
+ depends_on: [import]
188
+ poll: 30m
189
+ deadline: 72h
190
+ on_timeout: skip
191
+ config:
192
+ connection: dhis2-demo
193
+ data_set: ${params.data_set}
194
+ period: ${params.period}
195
+ org_unit: ${params.org_unit}