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.
- dirigent_integration-0.23.2/LICENSE +15 -0
- dirigent_integration-0.23.2/PKG-INFO +231 -0
- dirigent_integration-0.23.2/README.md +221 -0
- dirigent_integration-0.23.2/pyproject.toml +192 -0
- dirigent_integration-0.23.2/pyproject.toml.orig +107 -0
- dirigent_integration-0.23.2/src/dirigent_integration/__init__.py +29 -0
- dirigent_integration-0.23.2/src/dirigent_integration/py.typed +0 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-analytics-to-csv-report.yaml +174 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-export-to-s3.yaml +66 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-metadata-snapshot.yaml +143 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-tracker-weekly-window.yaml +162 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-values-per-org-unit-to-parquet.yaml +186 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/dhis2-values-to-parquet.yaml +64 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/README.md +79 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/dhis2-to-fhir-observations.yaml +212 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-capture-bundle-to-data-values.yaml +230 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-conceptmap-driven-mapping.yaml +222 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-encounter-to-event.yaml +248 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-measure-report-to-analytics-check.yaml +205 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-nightly-window-sync.yaml +182 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-patient-to-tracked-entity.yaml +225 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-questionnaire-response-to-data-values.yaml +187 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/fhir/fhir-subscription-webhook-to-dhis2.yaml +218 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/README.md +32 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/csv-drop-to-data-values.yaml +176 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/fan-out-per-facility-import.yaml +202 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/fhir-observations-to-data-values.yaml +193 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/http-json-to-data-values.yaml +151 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/outside-to-dhis2-with-checks.yaml +246 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/parquet-lakehouse-to-dhis2.yaml +158 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/webhook-payload-to-tracker-event.yaml +176 -0
- dirigent_integration-0.23.2/src/dirigent_integration/shelves/inbound/weekly-window-pull-and-import.yaml +175 -0
- 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
|
+
[](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml)
|
|
14
|
+
[](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
|
+
[](https://github.com/winterop-com/dirigent-integration/actions/workflows/ci.yaml)
|
|
4
|
+
[](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"
|