dirigent-integration 0.23.2__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.
Files changed (33) hide show
  1. dirigent_integration-0.23.2/LICENSE +15 -0
  2. dirigent_integration-0.23.2/PKG-INFO +231 -0
  3. dirigent_integration-0.23.2/README.md +221 -0
  4. dirigent_integration-0.23.2/pyproject.toml +192 -0
  5. dirigent_integration-0.23.2/pyproject.toml.orig +107 -0
  6. dirigent_integration-0.23.2/src/dirigent_integration/__init__.py +29 -0
  7. dirigent_integration-0.23.2/src/dirigent_integration/py.typed +0 -0
  8. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-analytics-to-csv-report.yaml +174 -0
  9. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-export-to-s3.yaml +66 -0
  10. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-metadata-snapshot.yaml +143 -0
  11. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-tracker-weekly-window.yaml +162 -0
  12. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-values-per-org-unit-to-parquet.yaml +186 -0
  13. dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-values-to-parquet.yaml +64 -0
  14. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/README.md +79 -0
  15. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/dhis2-to-fhir-observations.yaml +212 -0
  16. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-capture-bundle-to-data-values.yaml +230 -0
  17. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-conceptmap-driven-mapping.yaml +222 -0
  18. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-encounter-to-event.yaml +248 -0
  19. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-measure-report-to-analytics-check.yaml +205 -0
  20. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-nightly-window-sync.yaml +182 -0
  21. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-patient-to-tracked-entity.yaml +225 -0
  22. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-questionnaire-response-to-data-values.yaml +187 -0
  23. dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-subscription-webhook-to-dhis2.yaml +218 -0
  24. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/README.md +32 -0
  25. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/csv-drop-to-data-values.yaml +176 -0
  26. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/fan-out-per-facility-import.yaml +202 -0
  27. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/fhir-observations-to-data-values.yaml +193 -0
  28. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/http-json-to-data-values.yaml +151 -0
  29. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/outside-to-dhis2-with-checks.yaml +246 -0
  30. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/parquet-lakehouse-to-dhis2.yaml +158 -0
  31. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/webhook-payload-to-tracker-event.yaml +176 -0
  32. dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/weekly-window-pull-and-import.yaml +175 -0
  33. dirigent_integration-0.23.2/src/dirigent_integration/shelves/parquet-to-dhis2-import.yaml +195 -0
@@ -0,0 +1,15 @@
1
+ Copyright (c) 2026 Morten Olav Hansen <morten@winterop.com>. All rights reserved.
2
+
3
+ This source code and accompanying documentation are the property of
4
+ Morten Olav Hansen. No license, express or implied, is granted to use, copy,
5
+ modify, merge, publish, distribute, sublicense, or sell copies of this
6
+ software or its derivatives.
7
+
8
+ The source is published for reference only. Any use beyond reading
9
+ requires written permission from the copyright holder.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
12
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
13
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
14
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES,
15
+ OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: dirigent-integration
3
+ Version: 0.23.2
4
+ Summary: The control center for the dirigent plugin ecosystem: assemble every pack, run everyone's tests and examples together.
5
+ License-Expression: LicenseRef-Proprietary
6
+ License-File: LICENSE
7
+ Requires-Dist: dirigent-plugin==0.23.2
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ # dirigent-integration
12
+
13
+ [![Integration](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml)
14
+ [![Release](https://img.shields.io/github/v/release/winterop-com/dirigent-integration?label=release)](https://github.com/winterop-com/dirigent-integration/releases)
15
+
16
+ The control center for the dirigent plugin ecosystem. This is the one place aware of every
17
+ official pack at once -- it assembles them into a single environment and runs everyone
18
+ together to prove they still compose.
19
+
20
+ ## The principle it protects
21
+
22
+ dirigent-core knows nothing about any pack. Every adapter, DHIS2 today and more to come --
23
+ anything the core is deliberately ignorant of -- lives only in its own repository and
24
+ self-tests there, against nothing but its own dependencies. The integration is the only place
25
+ the whole set is installed side by side, so it is the only place that can prove they still
26
+ compose: that every pack's blocks land in one catalog without colliding, that every pack's
27
+ examples still validate against that merged catalog, and that a pipeline spanning two packs is
28
+ runnable at all.
29
+
30
+ ## What is here
31
+
32
+ - **`ecosystem.yaml`** -- the manifest: every component and the ref it tracks, the source of
33
+ truth for what gets assembled. `dirigent` (the runtime monorepo) is `role: runtime`,
34
+ installed as dependencies and never cloned; each adapter is `role: pack`, cloned and
35
+ self-tested here. `dirigent-dhis2` is the first; a future pack becomes real with a single
36
+ entry.
37
+ - **`pyproject.toml`** -- the assembled runtime: every dirigent runtime package plus every
38
+ pack, wired through `[tool.uv.sources]` as git dependencies on `branch = main`, unpinned, so
39
+ the integration always assembles the latest ecosystem.
40
+ - **`scripts/run_integration.py`** -- the runner. It reads the manifest, clones each pack, and
41
+ runs everything against the assembled environment. It is generic: it iterates the packs the
42
+ manifest names and has no per-pack knowledge.
43
+ - **`infra/`** -- the batteries-included image and the stack that runs it: `Dockerfile` builds
44
+ dirigent plus every pack plus the web UI, `compose.yaml` runs it against postgres.
45
+ - **`src/dirigent_integration/shelves/`** -- cross-boundary example pipelines, authored here
46
+ because they span more than one pack and belong to none. They ship as package data: this
47
+ repository is itself a distribution registering one plugin whose `examples()` hook carries
48
+ the shelves, so an instance with it installed lists them with `dg examples list --plugin
49
+ integration`. The root `examples/` is a symlink to them, so a checkout's paths still read.
50
+ - **`tests/`** -- the integration's own tests: the merged catalog is coherent, and the
51
+ cross-boundary examples validate against it.
52
+
53
+ ## Running it locally
54
+
55
+ ```sh
56
+ make test
57
+ ```
58
+
59
+ One target, one green or red:
60
+
61
+ 1. clone every `role: pack` in `ecosystem.yaml` into `checkouts/` (gitignored), at its ref;
62
+ 2. run each pack's own pytest suite against the assembled environment;
63
+ 3. validate each pack's own examples against the merged catalog;
64
+ 4. validate the cross-boundary examples against the merged catalog;
65
+ 5. run the integration's own tests.
66
+
67
+ A pack whose checkout is already present is moved to its ref's tip, so the tests read from the
68
+ same commit the source was installed from; one with local changes is left where it is and
69
+ reported, which is what lets a copy of a sibling working tree stand in for a clone. Other
70
+ targets: `make lock`, `make sync`, `make clone`, `make lint`, `make clean`.
71
+
72
+ ## An instance to look at
73
+
74
+ ```sh
75
+ make dev # an empty instance on http://127.0.0.1:3333
76
+ make dev-seeded # the same instance with every corpus in it
77
+ ```
78
+
79
+ `make dev-seeded` names no directory at all: every corpus is installed -- dirigent's own as
80
+ `dirigent-examples`, each pack's inside the pack, and this repository's inside
81
+ `dirigent_integration` -- and each ships through the `examples()` plugin hook, so
82
+ `dg dev --seed-installed` reaches all of them. A pack added to `ecosystem.yaml` is seeded by
83
+ being installed. Schedules land paused, and a document the instance will not store is reported
84
+ and passed over -- a corpus holds those on purpose, and a refusal is itself something to look
85
+ at.
86
+
87
+ The runtime is installed from git rather than cloned, so `make clone` also checks out
88
+ `checkouts/dirigent`, which exists for one thing: `make ui` builds the web bundle there.
89
+ `make test` neither needs nor makes that checkout. `DEV_HOST` and `DEV_PORT` move where both
90
+ targets listen.
91
+
92
+ `dev-seeded` mints a `DIRIGENT_SECRET_KEY` per boot, because a connection carrying a credential
93
+ cannot be stored without one and the state is wiped first anyway, and it allows the unsafe
94
+ blocks the corpora teach with (`UNSAFE_BLOCKS`). What stays refused is what no local instance
95
+ holds: an object-store or warehouse connection nothing created, a schema a sibling document
96
+ registers, a `docker.compose` step, a pipeline the corpus applies later.
97
+
98
+ dirigent-server installed from git carries no UI bundle -- its `static/` is gitignored there --
99
+ so both targets build one in the cloned checkout (`make ui`, which needs bun) and point the
100
+ server at it with `DIRIGENT_UI_DIR`. Without bun the build is skipped with a note and the
101
+ instance answers the API alone; the stack `make up` runs always has the UI.
102
+
103
+ ## The cross-boundary examples
104
+
105
+ Pipelines that no single pack owns because they span several. Today they exercise the first
106
+ pack, DHIS2, across the built-in storage and transform packs; as more adapters land,
107
+ cross-boundary pipelines across them are authored here the same way.
108
+
109
+ - **`examples/dhis2-export-to-s3.yaml`** -- a `dhis2.data_value_set_export` written to the
110
+ `s3://` scheme with `storage.write`, then a `storage.copy` archiving it.
111
+ - **`examples/dhis2-values-to-parquet.yaml`** -- the same export staged in `s3://` and fed to
112
+ `convert.arrow`, re-encoding to parquet beside it: three packs in one pipeline.
113
+ - **`examples/dhis2-values-per-org-unit-to-parquet.yaml`** -- that export fanned out over a list
114
+ of organisation units, flattened into one parquet table, with a manifest of what landed.
115
+ - **`examples/dhis2-analytics-to-csv-report.yaml`** -- a `dhis2.analytics_query` grid flattened
116
+ into rows, written to `s3://` as csv, and announced with a `webhook.post` summary.
117
+ - **`examples/parquet-to-dhis2-import.yaml`** -- the return leg: a parquet drop waited on with
118
+ `storage.exists`, decoded, gated on a carried schema, then imported into DHIS2.
119
+ - **`examples/dhis2-tracker-weekly-window.yaml`** -- a windowed weekly schedule driving
120
+ `dhis2.tracker` over the week that just closed, archived as ndjson and parquet in `s3://`.
121
+ - **`examples/dhis2-metadata-snapshot.yaml`** -- a metadata snapshot written to `s3://`, then
122
+ `pipeline.run` of `dhis2-export-to-s3`: composition across the corpus itself.
123
+
124
+ ## FHIR examples
125
+
126
+ `examples/fhir/` is a shelf of its own: nine pipelines moving data between a FHIR endpoint and
127
+ DHIS2, in both directions. They are cross-boundary twice over -- the DHIS2 blocks come from the
128
+ `dirigent-dhis2` pack and the HTTP, transform and validate blocks from the runtime, and neither
129
+ knows the other exists.
130
+
131
+ Most of them read a `d2w fhir serve` facade, which publishes one DHIS2 instance as a FHIR
132
+ endpoint and takes captures back; where a general-purpose FHIR server is the point -- an
133
+ `Encounter`, an `Observation`, a `Subscription`, a `_lastUpdated` search, none of which that
134
+ facade serves -- the default is a public HAPI R4 sandbox instead, and the document's header
135
+ says so. Every write is a dry run: `dryRun=true` on `/api/dataValueSets`, `importMode=VALIDATE`
136
+ on `/api/tracker`.
137
+
138
+ [`examples/fhir/README.md`](examples/fhir/README.md) has the one-line table and the mapping
139
+ vocabulary the shelf is built on -- subject, capture, ConceptMap.
140
+
141
+ - **`examples/fhir/fhir-capture-bundle-to-data-values.yaml`** -- the capture pair end to end: a page
142
+ of captures pulled off the facade, translated, gated on the pack's own id and period formats,
143
+ rehearsed and imported.
144
+ - **`examples/fhir/fhir-questionnaire-response-to-data-values.yaml`** -- the general case, where a
145
+ `linkId` is a form designer's name and the lookup is the integration.
146
+ - **`examples/fhir/fhir-patient-to-tracked-entity.yaml`** -- the `Patient` register folded into an
147
+ `/api/tracker` registration, posted by `http.request` because `dhis2.tracker` only reads.
148
+ - **`examples/fhir/fhir-encounter-to-event.yaml`** -- one `Encounter` and its `Observation`s folded
149
+ into the single program stage event DHIS2 wants.
150
+ - **`examples/fhir/fhir-conceptmap-driven-mapping.yaml`** -- the mapping fetched from the server at
151
+ run time rather than written into the document.
152
+ - **`examples/fhir/fhir-measure-report-to-analytics-check.yaml`** -- a `MeasureReport` held against
153
+ `dhis2.analytics_query` for the same period, and the gap delivered by `webhook.post`.
154
+ - **`examples/fhir/dhis2-to-fhir-observations.yaml`** -- the outbound leg: a data value set
155
+ published as a transaction `Bundle` of conditionally-updated `Observation`s.
156
+ - **`examples/fhir/fhir-subscription-webhook-to-dhis2.yaml`** -- the subscription contract,
157
+ landing on a webhook whose payload mapping no caller can reach past.
158
+ - **`examples/fhir/fhir-nightly-window-sync.yaml`** -- a half-open `_lastUpdated` window, a
159
+ fan-out per resource type under `items: continue`, and a join that says what it missed.
160
+
161
+ ## Inbound examples
162
+
163
+ `examples/inbound/` is a shelf of eight pipelines going one direction -- an outside system, a
164
+ transform, DHIS2 -- teaching the shapes an inbound integration actually takes: a public JSON
165
+ API, a csv dropped in a bucket, a pushed webhook, a parquet table from a lakehouse, a FHIR
166
+ Observation feed, a windowed weekly pull, a fan-out across facilities, and the defensive
167
+ version with every guard on. Each names every block it needs, carries its DHIS2 connection
168
+ pointed at the public play server, and imports as a dry run by default, so
169
+ `dg run --local examples/inbound/<file>` works with nothing set up.
170
+ `examples/inbound/README.md` lists them one line each.
171
+
172
+ ## The batteries-included image
173
+
174
+ `dirigent-full` is dirigent with every official pack the manifest names, plus the web UI, in
175
+ one image, built here with `make image` and not published. The published image is the core
176
+ one, `ghcr.io/winterop-com/dirigent`, which carries no adapter: a deployment that wants a pack
177
+ builds one layer on it (dirigent's operations guide, "The image"), and `dg init --template
178
+ compose` writes that Dockerfile. This repository builds the assembled image because it is the
179
+ one place the whole set is already resolved, and the stack below runs it.
180
+
181
+ `infra/Dockerfile` builds it from this repository's `pyproject.toml` and `uv.lock` in four
182
+ stages: a lock stage where `scripts/dirigent_rev.py` reads the dirigent commit out of the lock,
183
+ a bun stage that builds the UI bundle from that very commit (dirigent-server installed from git
184
+ carries no bundle, because its `static/` is gitignored there), a uv stage that runs
185
+ `uv sync --locked --no-dev`, and a runtime stage that mirrors the runtime stage of dirigent's
186
+ own `infra/Dockerfile` and must move with it.
187
+
188
+ The build clones the components over public HTTPS and needs no credentials. A component
189
+ repository that is private needs a GitHub read token instead; it goes in as a BuildKit secret
190
+ and is exposed to git only inside the one `RUN` that needs it, so it never lands in a layer or
191
+ a config file.
192
+
193
+ ```sh
194
+ make image # public components, no token
195
+ export GITHUB_TOKEN=$(gh auth token) # only if a component repository is private
196
+ ```
197
+
198
+ ### Running the stack
199
+
200
+ ```sh
201
+ cp .env.example .env # set DIRIGENT_SECRET_KEY and DIRIGENT_BOOTSTRAP_ADMIN_PASSWORD
202
+ make up
203
+ ```
204
+
205
+ Four services -- postgres, a one-shot migration, the server and a worker -- with the
206
+ cross-boundary shelves mounted as the apply directory, so those pipelines are seeded at boot.
207
+ The UI is on http://localhost:3333; `make down` removes it, volumes and all.
208
+
209
+ ## CI
210
+
211
+ The assembled check is expensive -- clone every pack, install the whole runtime, run every
212
+ suite -- so it is deliberately infrequent. Per-commit safety belongs to each repository's own
213
+ CI: `dirigent` tests itself on its pushes, each pack tests itself on its own. The integration
214
+ is the integration truth, caught once a day. `.github/workflows/ci.yaml` therefore triggers
215
+ only on **`schedule:`** (a nightly cron) and **`workflow_dispatch:`** (manual); it never runs
216
+ on push, which would turn one commit into a full-ecosystem rebuild. A lightweight
217
+ `.github/workflows/lint.yaml` runs on push so every commit still gets a status without
218
+ assembling anything.
219
+
220
+ `repository_dispatch` (type `component-released`) is kept as an optional, opt-in trigger for a
221
+ component's release or tag boundary only; nothing sends it today.
222
+
223
+ The components are public, so the git dependencies uv resolves and the pack clones the runner
224
+ makes need no credentials and the workflow needs no secret.
225
+
226
+ ## Licence
227
+
228
+ Copyright (c) 2026 Morten Olav Hansen. All rights reserved. See [LICENSE](LICENSE).
229
+
230
+ The source is published for reference only: no licence to use, copy, modify or distribute it
231
+ is granted, and any use beyond reading requires written permission.
@@ -0,0 +1,221 @@
1
+ # dirigent-integration
2
+
3
+ [![Integration](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml)
4
+ [![Release](https://img.shields.io/github/v/release/winterop-com/dirigent-integration?label=release)](https://github.com/winterop-com/dirigent-integration/releases)
5
+
6
+ The control center for the dirigent plugin ecosystem. This is the one place aware of every
7
+ official pack at once -- it assembles them into a single environment and runs everyone
8
+ together to prove they still compose.
9
+
10
+ ## The principle it protects
11
+
12
+ dirigent-core knows nothing about any pack. Every adapter, DHIS2 today and more to come --
13
+ anything the core is deliberately ignorant of -- lives only in its own repository and
14
+ self-tests there, against nothing but its own dependencies. The integration is the only place
15
+ the whole set is installed side by side, so it is the only place that can prove they still
16
+ compose: that every pack's blocks land in one catalog without colliding, that every pack's
17
+ examples still validate against that merged catalog, and that a pipeline spanning two packs is
18
+ runnable at all.
19
+
20
+ ## What is here
21
+
22
+ - **`ecosystem.yaml`** -- the manifest: every component and the ref it tracks, the source of
23
+ truth for what gets assembled. `dirigent` (the runtime monorepo) is `role: runtime`,
24
+ installed as dependencies and never cloned; each adapter is `role: pack`, cloned and
25
+ self-tested here. `dirigent-dhis2` is the first; a future pack becomes real with a single
26
+ entry.
27
+ - **`pyproject.toml`** -- the assembled runtime: every dirigent runtime package plus every
28
+ pack, wired through `[tool.uv.sources]` as git dependencies on `branch = main`, unpinned, so
29
+ the integration always assembles the latest ecosystem.
30
+ - **`scripts/run_integration.py`** -- the runner. It reads the manifest, clones each pack, and
31
+ runs everything against the assembled environment. It is generic: it iterates the packs the
32
+ manifest names and has no per-pack knowledge.
33
+ - **`infra/`** -- the batteries-included image and the stack that runs it: `Dockerfile` builds
34
+ dirigent plus every pack plus the web UI, `compose.yaml` runs it against postgres.
35
+ - **`src/dirigent_integration/shelves/`** -- cross-boundary example pipelines, authored here
36
+ because they span more than one pack and belong to none. They ship as package data: this
37
+ repository is itself a distribution registering one plugin whose `examples()` hook carries
38
+ the shelves, so an instance with it installed lists them with `dg examples list --plugin
39
+ integration`. The root `examples/` is a symlink to them, so a checkout's paths still read.
40
+ - **`tests/`** -- the integration's own tests: the merged catalog is coherent, and the
41
+ cross-boundary examples validate against it.
42
+
43
+ ## Running it locally
44
+
45
+ ```sh
46
+ make test
47
+ ```
48
+
49
+ One target, one green or red:
50
+
51
+ 1. clone every `role: pack` in `ecosystem.yaml` into `checkouts/` (gitignored), at its ref;
52
+ 2. run each pack's own pytest suite against the assembled environment;
53
+ 3. validate each pack's own examples against the merged catalog;
54
+ 4. validate the cross-boundary examples against the merged catalog;
55
+ 5. run the integration's own tests.
56
+
57
+ A pack whose checkout is already present is moved to its ref's tip, so the tests read from the
58
+ same commit the source was installed from; one with local changes is left where it is and
59
+ reported, which is what lets a copy of a sibling working tree stand in for a clone. Other
60
+ targets: `make lock`, `make sync`, `make clone`, `make lint`, `make clean`.
61
+
62
+ ## An instance to look at
63
+
64
+ ```sh
65
+ make dev # an empty instance on http://127.0.0.1:3333
66
+ make dev-seeded # the same instance with every corpus in it
67
+ ```
68
+
69
+ `make dev-seeded` names no directory at all: every corpus is installed -- dirigent's own as
70
+ `dirigent-examples`, each pack's inside the pack, and this repository's inside
71
+ `dirigent_integration` -- and each ships through the `examples()` plugin hook, so
72
+ `dg dev --seed-installed` reaches all of them. A pack added to `ecosystem.yaml` is seeded by
73
+ being installed. Schedules land paused, and a document the instance will not store is reported
74
+ and passed over -- a corpus holds those on purpose, and a refusal is itself something to look
75
+ at.
76
+
77
+ The runtime is installed from git rather than cloned, so `make clone` also checks out
78
+ `checkouts/dirigent`, which exists for one thing: `make ui` builds the web bundle there.
79
+ `make test` neither needs nor makes that checkout. `DEV_HOST` and `DEV_PORT` move where both
80
+ targets listen.
81
+
82
+ `dev-seeded` mints a `DIRIGENT_SECRET_KEY` per boot, because a connection carrying a credential
83
+ cannot be stored without one and the state is wiped first anyway, and it allows the unsafe
84
+ blocks the corpora teach with (`UNSAFE_BLOCKS`). What stays refused is what no local instance
85
+ holds: an object-store or warehouse connection nothing created, a schema a sibling document
86
+ registers, a `docker.compose` step, a pipeline the corpus applies later.
87
+
88
+ dirigent-server installed from git carries no UI bundle -- its `static/` is gitignored there --
89
+ so both targets build one in the cloned checkout (`make ui`, which needs bun) and point the
90
+ server at it with `DIRIGENT_UI_DIR`. Without bun the build is skipped with a note and the
91
+ instance answers the API alone; the stack `make up` runs always has the UI.
92
+
93
+ ## The cross-boundary examples
94
+
95
+ Pipelines that no single pack owns because they span several. Today they exercise the first
96
+ pack, DHIS2, across the built-in storage and transform packs; as more adapters land,
97
+ cross-boundary pipelines across them are authored here the same way.
98
+
99
+ - **`examples/dhis2-export-to-s3.yaml`** -- a `dhis2.data_value_set_export` written to the
100
+ `s3://` scheme with `storage.write`, then a `storage.copy` archiving it.
101
+ - **`examples/dhis2-values-to-parquet.yaml`** -- the same export staged in `s3://` and fed to
102
+ `convert.arrow`, re-encoding to parquet beside it: three packs in one pipeline.
103
+ - **`examples/dhis2-values-per-org-unit-to-parquet.yaml`** -- that export fanned out over a list
104
+ of organisation units, flattened into one parquet table, with a manifest of what landed.
105
+ - **`examples/dhis2-analytics-to-csv-report.yaml`** -- a `dhis2.analytics_query` grid flattened
106
+ into rows, written to `s3://` as csv, and announced with a `webhook.post` summary.
107
+ - **`examples/parquet-to-dhis2-import.yaml`** -- the return leg: a parquet drop waited on with
108
+ `storage.exists`, decoded, gated on a carried schema, then imported into DHIS2.
109
+ - **`examples/dhis2-tracker-weekly-window.yaml`** -- a windowed weekly schedule driving
110
+ `dhis2.tracker` over the week that just closed, archived as ndjson and parquet in `s3://`.
111
+ - **`examples/dhis2-metadata-snapshot.yaml`** -- a metadata snapshot written to `s3://`, then
112
+ `pipeline.run` of `dhis2-export-to-s3`: composition across the corpus itself.
113
+
114
+ ## FHIR examples
115
+
116
+ `examples/fhir/` is a shelf of its own: nine pipelines moving data between a FHIR endpoint and
117
+ DHIS2, in both directions. They are cross-boundary twice over -- the DHIS2 blocks come from the
118
+ `dirigent-dhis2` pack and the HTTP, transform and validate blocks from the runtime, and neither
119
+ knows the other exists.
120
+
121
+ Most of them read a `d2w fhir serve` facade, which publishes one DHIS2 instance as a FHIR
122
+ endpoint and takes captures back; where a general-purpose FHIR server is the point -- an
123
+ `Encounter`, an `Observation`, a `Subscription`, a `_lastUpdated` search, none of which that
124
+ facade serves -- the default is a public HAPI R4 sandbox instead, and the document's header
125
+ says so. Every write is a dry run: `dryRun=true` on `/api/dataValueSets`, `importMode=VALIDATE`
126
+ on `/api/tracker`.
127
+
128
+ [`examples/fhir/README.md`](examples/fhir/README.md) has the one-line table and the mapping
129
+ vocabulary the shelf is built on -- subject, capture, ConceptMap.
130
+
131
+ - **`examples/fhir/fhir-capture-bundle-to-data-values.yaml`** -- the capture pair end to end: a page
132
+ of captures pulled off the facade, translated, gated on the pack's own id and period formats,
133
+ rehearsed and imported.
134
+ - **`examples/fhir/fhir-questionnaire-response-to-data-values.yaml`** -- the general case, where a
135
+ `linkId` is a form designer's name and the lookup is the integration.
136
+ - **`examples/fhir/fhir-patient-to-tracked-entity.yaml`** -- the `Patient` register folded into an
137
+ `/api/tracker` registration, posted by `http.request` because `dhis2.tracker` only reads.
138
+ - **`examples/fhir/fhir-encounter-to-event.yaml`** -- one `Encounter` and its `Observation`s folded
139
+ into the single program stage event DHIS2 wants.
140
+ - **`examples/fhir/fhir-conceptmap-driven-mapping.yaml`** -- the mapping fetched from the server at
141
+ run time rather than written into the document.
142
+ - **`examples/fhir/fhir-measure-report-to-analytics-check.yaml`** -- a `MeasureReport` held against
143
+ `dhis2.analytics_query` for the same period, and the gap delivered by `webhook.post`.
144
+ - **`examples/fhir/dhis2-to-fhir-observations.yaml`** -- the outbound leg: a data value set
145
+ published as a transaction `Bundle` of conditionally-updated `Observation`s.
146
+ - **`examples/fhir/fhir-subscription-webhook-to-dhis2.yaml`** -- the subscription contract,
147
+ landing on a webhook whose payload mapping no caller can reach past.
148
+ - **`examples/fhir/fhir-nightly-window-sync.yaml`** -- a half-open `_lastUpdated` window, a
149
+ fan-out per resource type under `items: continue`, and a join that says what it missed.
150
+
151
+ ## Inbound examples
152
+
153
+ `examples/inbound/` is a shelf of eight pipelines going one direction -- an outside system, a
154
+ transform, DHIS2 -- teaching the shapes an inbound integration actually takes: a public JSON
155
+ API, a csv dropped in a bucket, a pushed webhook, a parquet table from a lakehouse, a FHIR
156
+ Observation feed, a windowed weekly pull, a fan-out across facilities, and the defensive
157
+ version with every guard on. Each names every block it needs, carries its DHIS2 connection
158
+ pointed at the public play server, and imports as a dry run by default, so
159
+ `dg run --local examples/inbound/<file>` works with nothing set up.
160
+ `examples/inbound/README.md` lists them one line each.
161
+
162
+ ## The batteries-included image
163
+
164
+ `dirigent-full` is dirigent with every official pack the manifest names, plus the web UI, in
165
+ one image, built here with `make image` and not published. The published image is the core
166
+ one, `ghcr.io/winterop-com/dirigent`, which carries no adapter: a deployment that wants a pack
167
+ builds one layer on it (dirigent's operations guide, "The image"), and `dg init --template
168
+ compose` writes that Dockerfile. This repository builds the assembled image because it is the
169
+ one place the whole set is already resolved, and the stack below runs it.
170
+
171
+ `infra/Dockerfile` builds it from this repository's `pyproject.toml` and `uv.lock` in four
172
+ stages: a lock stage where `scripts/dirigent_rev.py` reads the dirigent commit out of the lock,
173
+ a bun stage that builds the UI bundle from that very commit (dirigent-server installed from git
174
+ carries no bundle, because its `static/` is gitignored there), a uv stage that runs
175
+ `uv sync --locked --no-dev`, and a runtime stage that mirrors the runtime stage of dirigent's
176
+ own `infra/Dockerfile` and must move with it.
177
+
178
+ The build clones the components over public HTTPS and needs no credentials. A component
179
+ repository that is private needs a GitHub read token instead; it goes in as a BuildKit secret
180
+ and is exposed to git only inside the one `RUN` that needs it, so it never lands in a layer or
181
+ a config file.
182
+
183
+ ```sh
184
+ make image # public components, no token
185
+ export GITHUB_TOKEN=$(gh auth token) # only if a component repository is private
186
+ ```
187
+
188
+ ### Running the stack
189
+
190
+ ```sh
191
+ cp .env.example .env # set DIRIGENT_SECRET_KEY and DIRIGENT_BOOTSTRAP_ADMIN_PASSWORD
192
+ make up
193
+ ```
194
+
195
+ Four services -- postgres, a one-shot migration, the server and a worker -- with the
196
+ cross-boundary shelves mounted as the apply directory, so those pipelines are seeded at boot.
197
+ The UI is on http://localhost:3333; `make down` removes it, volumes and all.
198
+
199
+ ## CI
200
+
201
+ The assembled check is expensive -- clone every pack, install the whole runtime, run every
202
+ suite -- so it is deliberately infrequent. Per-commit safety belongs to each repository's own
203
+ CI: `dirigent` tests itself on its pushes, each pack tests itself on its own. The integration
204
+ is the integration truth, caught once a day. `.github/workflows/ci.yaml` therefore triggers
205
+ only on **`schedule:`** (a nightly cron) and **`workflow_dispatch:`** (manual); it never runs
206
+ on push, which would turn one commit into a full-ecosystem rebuild. A lightweight
207
+ `.github/workflows/lint.yaml` runs on push so every commit still gets a status without
208
+ assembling anything.
209
+
210
+ `repository_dispatch` (type `component-released`) is kept as an optional, opt-in trigger for a
211
+ component's release or tag boundary only; nothing sends it today.
212
+
213
+ The components are public, so the git dependencies uv resolves and the pack clones the runner
214
+ makes need no credentials and the workflow needs no secret.
215
+
216
+ ## Licence
217
+
218
+ Copyright (c) 2026 Morten Olav Hansen. All rights reserved. See [LICENSE](LICENSE).
219
+
220
+ The source is published for reference only: no licence to use, copy, modify or distribute it
221
+ is granted, and any use beyond reading requires written permission.
@@ -0,0 +1,192 @@
1
+ [project]
2
+ name = "dirigent-integration"
3
+ version = "0.23.2"
4
+ description = "The control center for the dirigent plugin ecosystem: assemble every pack, run everyone's tests and examples together."
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ license = "LicenseRef-Proprietary"
8
+ license-files = ["LICENSE"]
9
+ dependencies = ["dirigent-plugin==0.23.2"]
10
+
11
+ [project.entry-points."dirigent.plugins.v1"]
12
+ integration = "dirigent_integration:plugin"
13
+
14
+ [dependency-groups]
15
+ assembly = [
16
+ "dirigent-cli",
17
+ "dirigent-server",
18
+ "dirigent-core[otlp]",
19
+ "dirigent-blocks",
20
+ "dirigent-block-base",
21
+ "dirigent-block-http",
22
+ "dirigent-block-storage",
23
+ "dirigent-block-execute",
24
+ "dirigent-block-sql",
25
+ "dirigent-block-duckdb",
26
+ "dirigent-block-jq",
27
+ "dirigent-block-queues",
28
+ "dirigent-block-parquet",
29
+ "dirigent-storage-s3",
30
+ "dirigent-common",
31
+ "dirigent-client",
32
+ "dirigent-examples",
33
+ "dirigent-dhis2",
34
+ ]
35
+ dev = [
36
+ "dirigent-testing",
37
+ "pytest>=8",
38
+ "pytest-asyncio>=0.24",
39
+ "pyyaml>=6",
40
+ "ruff>=0.16",
41
+ ]
42
+
43
+ [tool.uv]
44
+ default-groups = [
45
+ "dev",
46
+ "assembly",
47
+ ]
48
+
49
+ [tool.uv.sources.dirigent-cli]
50
+ git = "https://github.com/winterop-com/dirigent"
51
+ subdirectory = "packages/dirigent-cli"
52
+ branch = "main"
53
+
54
+ [tool.uv.sources.dirigent-server]
55
+ git = "https://github.com/winterop-com/dirigent"
56
+ subdirectory = "packages/dirigent-server"
57
+ branch = "main"
58
+
59
+ [tool.uv.sources.dirigent-core]
60
+ git = "https://github.com/winterop-com/dirigent"
61
+ subdirectory = "packages/dirigent-core"
62
+ branch = "main"
63
+
64
+ [tool.uv.sources.dirigent-blocks]
65
+ git = "https://github.com/winterop-com/dirigent"
66
+ subdirectory = "packages/dirigent-blocks"
67
+ branch = "main"
68
+
69
+ [tool.uv.sources.dirigent-block-base]
70
+ git = "https://github.com/winterop-com/dirigent"
71
+ subdirectory = "packages/dirigent-block-base"
72
+ branch = "main"
73
+
74
+ [tool.uv.sources.dirigent-block-http]
75
+ git = "https://github.com/winterop-com/dirigent"
76
+ subdirectory = "packages/dirigent-block-http"
77
+ branch = "main"
78
+
79
+ [tool.uv.sources.dirigent-block-storage]
80
+ git = "https://github.com/winterop-com/dirigent"
81
+ subdirectory = "packages/dirigent-block-storage"
82
+ branch = "main"
83
+
84
+ [tool.uv.sources.dirigent-block-execute]
85
+ git = "https://github.com/winterop-com/dirigent"
86
+ subdirectory = "packages/dirigent-block-execute"
87
+ branch = "main"
88
+
89
+ [tool.uv.sources.dirigent-block-sql]
90
+ git = "https://github.com/winterop-com/dirigent"
91
+ subdirectory = "packages/dirigent-block-sql"
92
+ branch = "main"
93
+
94
+ [tool.uv.sources.dirigent-block-duckdb]
95
+ git = "https://github.com/winterop-com/dirigent"
96
+ subdirectory = "packages/dirigent-block-duckdb"
97
+ branch = "main"
98
+
99
+ [tool.uv.sources.dirigent-block-jq]
100
+ git = "https://github.com/winterop-com/dirigent"
101
+ subdirectory = "packages/dirigent-block-jq"
102
+ branch = "main"
103
+
104
+ [tool.uv.sources.dirigent-block-queues]
105
+ git = "https://github.com/winterop-com/dirigent"
106
+ subdirectory = "packages/dirigent-block-queues"
107
+ branch = "main"
108
+
109
+ [tool.uv.sources.dirigent-block-parquet]
110
+ git = "https://github.com/winterop-com/dirigent"
111
+ subdirectory = "packages/dirigent-block-parquet"
112
+ branch = "main"
113
+
114
+ [tool.uv.sources.dirigent-storage-s3]
115
+ git = "https://github.com/winterop-com/dirigent"
116
+ subdirectory = "packages/dirigent-storage-s3"
117
+ branch = "main"
118
+
119
+ [tool.uv.sources.dirigent-plugin]
120
+ git = "https://github.com/winterop-com/dirigent"
121
+ subdirectory = "packages/dirigent-plugin"
122
+ branch = "main"
123
+
124
+ [tool.uv.sources.dirigent-common]
125
+ git = "https://github.com/winterop-com/dirigent"
126
+ subdirectory = "packages/dirigent-common"
127
+ branch = "main"
128
+
129
+ [tool.uv.sources.dirigent-client]
130
+ git = "https://github.com/winterop-com/dirigent"
131
+ subdirectory = "packages/dirigent-client"
132
+ branch = "main"
133
+
134
+ [tool.uv.sources.dirigent-examples]
135
+ git = "https://github.com/winterop-com/dirigent"
136
+ subdirectory = "packages/dirigent-examples"
137
+ branch = "main"
138
+
139
+ [tool.uv.sources.dirigent-testing]
140
+ git = "https://github.com/winterop-com/dirigent"
141
+ subdirectory = "packages/dirigent-testing"
142
+ branch = "main"
143
+
144
+ [tool.uv.sources.dirigent-dhis2]
145
+ git = "https://github.com/winterop-com/dirigent-dhis2"
146
+ branch = "main"
147
+
148
+ [tool.pytest.ini_options]
149
+ asyncio_mode = "auto"
150
+ testpaths = ["tests"]
151
+
152
+ [tool.ruff]
153
+ target-version = "py313"
154
+ line-length = 120
155
+ src = [
156
+ "src",
157
+ "scripts",
158
+ "tests",
159
+ ]
160
+
161
+ [tool.ruff.lint]
162
+ select = [
163
+ "E",
164
+ "W",
165
+ "F",
166
+ "I",
167
+ "D",
168
+ "SIM",
169
+ "UP",
170
+ "B",
171
+ "TID",
172
+ ]
173
+
174
+ [tool.ruff.lint.per-file-ignores]
175
+ "tests/**" = [
176
+ "D103",
177
+ "D100",
178
+ "D101",
179
+ "D102",
180
+ "D104",
181
+ ]
182
+
183
+ [tool.ruff.lint.pydocstyle]
184
+ convention = "google"
185
+
186
+ [tool.ruff.format]
187
+ quote-style = "double"
188
+ docstring-code-format = true
189
+
190
+ [build-system]
191
+ requires = ["uv_build>=0.12.0,<0.13.0"]
192
+ build-backend = "uv_build"