dsimaging-admin 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,244 @@
1
+ # IMPROVEMENTS
2
+
3
+ Candidate improvements to make `dsimaging-admin` a complete administration tool
4
+ for `dsimaging-store`, beyond the happy-path coverage shipped in **v0.2.0**.
5
+
6
+ This is a roadmap, not a backlog. Items are prioritised by how much real
7
+ operator pain they remove on the current `dsImaging` / `dsHPC` deployment.
8
+
9
+ ## Current coverage (v0.2.0)
10
+
11
+ | Command | What it does |
12
+ |-----------|------------------------------------------------------------------------|
13
+ | `init` | Write `~/.dsimaging.yaml`. |
14
+ | `publish` | Local dir -> S3 (upload + indexes + manifest, optional Opal register). |
15
+ | `rescan` | Recompute indexes / manifest from current S3 contents. |
16
+ | `list` | Enumerate datasets; `published` if `manifest.yaml` exists. |
17
+ | `doctor` | Check S3 connectivity, bucket existence, versioning, dataset count. |
18
+
19
+ Two structural observations that motivate most of what follows:
20
+
21
+ 1. The CLI does **not** talk to the `dsimaging-store` controller
22
+ (`:8080/health`, `:8080/datasets`). All status comes from S3 directly.
23
+ 2. `cli.py::_write_dataset_artifacts` and
24
+ `dsimaging-store/controller/controller.py::write_dataset_artifacts`
25
+ re-implement the same scan / hash / manifest pipeline in parallel. They
26
+ already differ subtly (extra-metadata handling). This is a drift hazard.
27
+
28
+ ---
29
+
30
+ ## Priority 1 — missing core admin verbs
31
+
32
+ ### 1.1 `delete <dataset-id>`
33
+
34
+ Hard gap. Today the only way to drop a broken or obsolete dataset is
35
+ `mc rm --recursive`.
36
+
37
+ ```
38
+ dsimaging-admin delete <dataset-id> [--yes]
39
+ [--purge-versions]
40
+ [--opal-url ... --opal-project ... --opal-resource ...]
41
+ ```
42
+
43
+ - Refuse without `--yes`.
44
+ - By default, delete current objects only; prior versions stay (the bucket has
45
+ versioning on). `--purge-versions` issues `delete_object_versions` in pages
46
+ of 1000.
47
+ - If `--opal-*` is provided, also remove the Opal resource.
48
+
49
+ Effort: **low**. Pure `list_objects_v2` + `delete_objects` paginator work.
50
+
51
+ ### 1.2 `verify <dataset-id>`
52
+
53
+ Validate that the dataset on S3 still matches its
54
+ `content_hash_index.parquet`. The hash index is the whole reason content
55
+ hashes are computed; there is no command to check it.
56
+
57
+ ```
58
+ dsimaging-admin verify <dataset-id> [--sample-fraction 1.0]
59
+ [--output text|json]
60
+ [--quick]
61
+ ```
62
+
63
+ - Reads `indexes/content_hash_index.parquet` (and the masks index if present).
64
+ - Re-streams every object, recomputes sha256, reports
65
+ `missing` / `extra` / `mismatch` per sample.
66
+ - `--quick` compares S3 ETag against the recorded etag first; only falls back
67
+ to full hashing on mismatch. Caveat: ETag == sha256 only for single-part
68
+ uploads, so `--quick` is an heuristic, not a proof.
69
+ - Non-zero exit on drift; suitable for CI.
70
+
71
+ Effort: **medium**. DICOM series must hash with the same composition rule as
72
+ `controller.scan_s3_images` (sorted per-file sha256 fed back into a parent
73
+ sha256) — another reason to share code (see P4).
74
+
75
+ ### 1.3 `opal {list,register,unregister}` standalone
76
+
77
+ Today Opal registration is a side-effect of `publish`. Adding a fourth Opal
78
+ site, or replaying registration after a wipe, forces either a full re-upload
79
+ or hand-crafted Opal REST calls.
80
+
81
+ ```
82
+ dsimaging-admin opal list --opal-url ... [--project IMAGING]
83
+ dsimaging-admin opal register --dataset-id <id> --opal-url ... [--name ... --replace]
84
+ dsimaging-admin opal unregister --opal-url ... --project IMAGING --resource <name>
85
+ ```
86
+
87
+ `register` rebuilds the `imaging+dataset://` URL from the dataset's
88
+ manifest on S3, so it does not need access to the original local source.
89
+
90
+ Implementation: extract `_register_opal_resource` from `cli.py` into an
91
+ `opal.py` module and reuse from the new sub-commands and from `publish`.
92
+
93
+ Effort: **low**.
94
+
95
+ ---
96
+
97
+ ## Priority 2 — observability & drift detection
98
+
99
+ ### 2.1 `doctor` extended for the full module
100
+
101
+ The current `doctor` is bucket-scoped. Extend it to validate the deployment:
102
+
103
+ - Ping controller `http://<host>:8080/health`, surface `dirty_datasets` and
104
+ `last_errors`.
105
+ - Confirm the MinIO webhook is configured for `datasets/` (compare against
106
+ what the controller advertises).
107
+ - Compare CLI-side `list` against controller `/datasets` and flag any
108
+ divergence.
109
+ - Validate the init container ran (presence of `datasets/.keep`,
110
+ versioning enabled).
111
+
112
+ New flags:
113
+
114
+ ```
115
+ --controller-url http://127.0.0.1:8080 (default; from ~/.dsimaging.yaml)
116
+ --skip-controller (for remote / firewalled stores)
117
+ ```
118
+
119
+ ### 2.2 `list` enriched with controller state
120
+
121
+ When `--controller-url` is reachable, merge `dirty` and `last_error` into the
122
+ table. Same network call as the extended `doctor`; gracefully degrade to
123
+ S3-only when the controller is unreachable.
124
+
125
+ ### 2.3 `status <dataset-id>`
126
+
127
+ Drill-down for a single dataset:
128
+
129
+ - Object counts under `source/images/` and `source/masks/`.
130
+ - Manifest `schema_version` + last-modified.
131
+ - Row count of `content_hash_index.parquet` vs object count (cheap drift hint
132
+ short of a full `verify`).
133
+ - Controller `last_reconcile` / `last_error` if reachable.
134
+
135
+ ---
136
+
137
+ ## Priority 3 — robustness of `publish`
138
+
139
+ ### 3.1 Atomic publish via staging prefix
140
+
141
+ Today, a mid-upload failure leaves a partial set of objects under
142
+ `datasets/<id>/source/`. The controller fires on each PUT and writes a
143
+ partial manifest. Replace with a two-phase flow:
144
+
145
+ 1. Upload to `datasets/<id>/.staging-<uuid>/source/...`.
146
+ 2. On full success, server-side `CopyObject` into the canonical prefix and
147
+ delete the staging copy.
148
+ 3. Only then do the webhook events on the final prefix trigger the
149
+ controller; the controller never sees an in-progress dataset.
150
+
151
+ Trade-off: intra-bucket copy adds IO but no egress. Acceptable for the
152
+ target scale.
153
+
154
+ ### 3.2 Idempotent upload (skip-if-hash-matches)
155
+
156
+ For larger public datasets (LUNG1, other TCIA cohorts), re-running `publish`
157
+ should skip objects whose local sha256 already appears in
158
+ `content_hash_index.parquet`. Add `--no-skip` to opt out.
159
+
160
+ ### 3.3 `--dry-run`
161
+
162
+ Scan + report what would be uploaded, no S3 writes. Useful for sanity-checking
163
+ metadata CSV joins (`--metadata`) before paying the upload cost.
164
+
165
+ ---
166
+
167
+ ## Priority 4 — internal refactor (drift killer)
168
+
169
+ `cli.py::_write_dataset_artifacts` and
170
+ `dsimaging-store/controller/controller.py::write_dataset_artifacts` are
171
+ siblings that must stay in sync. They already diverge:
172
+
173
+ - The controller has no concept of `extra_metadata` provided from outside;
174
+ it only preserves whatever was already in `metadata/samples.parquet`.
175
+ - Mask handling code paths are similar but not identical.
176
+
177
+ Two viable options:
178
+
179
+ **Option A — shared core library (recommended).**
180
+ Extract `manifest`, `hashing`, `schema`, `s3_layout` into a small package
181
+ (`dsimaging-core` or similar) installed in both the CLI distribution and the
182
+ controller container. CLI keeps `cli` + `opal`; controller keeps the HTTP
183
+ layer + reconcile loop.
184
+
185
+ **Option B — controller as sole writer.**
186
+ CLI never writes indexes/manifests directly; after upload it issues
187
+ `POST /reconcile/<dataset-id>` and waits. Single source of truth, but the
188
+ CLI now hard-requires the controller stack — that breaks the use-case of
189
+ publishing into a vanilla S3 bucket without the `dsimaging-store` services
190
+ deployed.
191
+
192
+ Recommendation: **A**. Keep `publish` self-sufficient.
193
+
194
+ ---
195
+
196
+ ## Priority 5 — nice-to-haves
197
+
198
+ - `download <dataset-id> <dest>` — pull a dataset locally for inspection
199
+ / debugging.
200
+ - `copy <src-id> <dst-id>` — server-side intra-bucket copy for renames or
201
+ forks.
202
+ - Multi-profile config: `~/.dsimaging.yaml` with `profiles.<name>:` and a
203
+ `--profile <name>` flag (today only `default` is honoured).
204
+ - `--output json` on `list`, `verify`, `status`, `doctor` for scripting.
205
+ - Structured logging (`--log-format json`).
206
+ - Pre-publish DICOM sanity checks (consistent SeriesInstanceUID per folder,
207
+ monotonic InstanceNumber, single Modality value).
208
+
209
+ ---
210
+
211
+ ## Non-goals (explicit)
212
+
213
+ - Web UI. The MinIO console plus the CLI is enough for operator needs.
214
+ - Authentication / RBAC inside the CLI. Inherited from S3 and Opal endpoints.
215
+ - Image-level operations (DICOM de-identification, resampling, format
216
+ conversion). That is `dsImaging` / `dsRadiomics` territory, not admin.
217
+
218
+ ---
219
+
220
+ ## Open questions
221
+
222
+ - Should `delete` cascade to `dsHPC` artifacts that reference the dataset?
223
+ No back-reference index exists today; probably out of scope for the admin
224
+ CLI.
225
+ - Should `verify` write a tombstone (`indexes/last_verify.json`) on detected
226
+ drift so the controller can refuse to republish stale manifests?
227
+ - Minimal `release.yml` for PyPI Trusted Publishing from
228
+ `isglobal-brge/dsimaging-admin`? Tracked separately; only blocks future
229
+ cuts.
230
+
231
+ ---
232
+
233
+ ## Sequencing suggestion
234
+
235
+ **One full day of work**:
236
+
237
+ 1. Extract shared core (**P4 Option A**). Pays for everything below.
238
+ 2. `delete` + `opal *` standalone (**P1.1** + **P1.3**). Both small, both
239
+ unblock real operations.
240
+ 3. `verify` (**P1.2**). Turns the hash index from a hopeful artifact into a
241
+ real safety net.
242
+
243
+ **An afternoon**: skip P4, do P1.1 + P1.3 only. Defer the shared-core
244
+ refactor until the next time the manifest schema changes.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: dsimaging-admin
3
+ Version: 0.3.0
4
+ Summary: Admin CLI for managing medical imaging datasets in S3/MinIO for DataSHIELD
5
+ Project-URL: Homepage, https://github.com/isglobal-brge/dsimaging-admin
6
+ Project-URL: Repository, https://github.com/isglobal-brge/dsimaging-admin
7
+ Author-email: David Sarrat Gonzalez <david.sarrat@isglobal.org>, Juan R Gonzalez <juanr.gonzalez@isglobal.org>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: datashield,dicom,medical-imaging,minio,radiomics,s3
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Healthcare Industry
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: boto3>=1.28.0
19
+ Requires-Dist: click>=8.0.0
20
+ Requires-Dist: pyarrow>=14.0.0
21
+ Requires-Dist: pydicom>=3.0.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: requests>=2.31.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # dsimaging-admin
27
+
28
+ Admin CLI for creating and operating `dsimaging-store` deployments and the
29
+ medical imaging datasets stored in them.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install dsimaging-admin
35
+ ```
36
+
37
+ ## Create a store
38
+
39
+ `dsimaging-admin store init` writes a Docker Compose project for MinIO plus the
40
+ `dsimaging-store` controller.
41
+
42
+ ```bash
43
+ dsimaging-admin store init ./study-store \
44
+ --controller-image davidsarratgonzalez/dsimaging-store:latest
45
+ dsimaging-admin store up ./study-store
46
+ dsimaging-admin store doctor ./study-store
47
+ ```
48
+
49
+ For local controller development, build from a checked-out `dsimaging-store`
50
+ repo instead of using an image:
51
+
52
+ ```bash
53
+ dsimaging-admin store init ./study-store \
54
+ --store-source /path/to/dsimaging-store
55
+ dsimaging-admin store up ./study-store
56
+ ```
57
+
58
+ Use the generated connection details as a reusable CLI profile:
59
+
60
+ ```bash
61
+ dsimaging-admin init \
62
+ --endpoint http://127.0.0.1:9000 \
63
+ --controller-url http://127.0.0.1:8080 \
64
+ --bucket imaging-data
65
+ ```
66
+
67
+ ## Dataset operations
68
+
69
+ ```bash
70
+ # Publish with staging, publish lock, skip-if-hash-matches and DICOM checks.
71
+ dsimaging-admin publish \
72
+ --dataset-id study_ct_v1 \
73
+ --source /data/study_ct \
74
+ --metadata /data/study_ct/clinical.csv \
75
+ --modality ct
76
+
77
+ # Inspect and verify.
78
+ dsimaging-admin list
79
+ dsimaging-admin status study_ct_v1
80
+ dsimaging-admin verify study_ct_v1
81
+ dsimaging-admin doctor
82
+
83
+ # Rebuild artifacts from S3, copy, download or delete.
84
+ dsimaging-admin rescan study_ct_v1
85
+ dsimaging-admin copy study_ct_v1 study_ct_v2 --yes
86
+ dsimaging-admin download study_ct_v1 ./debug/study_ct_v1
87
+ dsimaging-admin delete study_ct_v1 --yes --purge-versions
88
+ ```
89
+
90
+ All reporting commands that are useful for automation support JSON output:
91
+
92
+ ```bash
93
+ dsimaging-admin list --output json
94
+ dsimaging-admin status study_ct_v1 --output json
95
+ dsimaging-admin verify study_ct_v1 --output json
96
+ dsimaging-admin doctor --output json
97
+ ```
98
+
99
+ ## What `publish` does
100
+
101
+ 1. Scans your local image directory and optional masks under `source/masks/`,
102
+ `masks/`, `source/labels/`, or `labels/`.
103
+ 2. Runs basic DICOM sanity checks for series UID, modality and instance order.
104
+ 3. Computes SHA-256 content hashes.
105
+ 4. Skips uploads that already match the current dataset hash indexes.
106
+ 5. Uploads through `datasets/<id>/.staging-*` and a `.publish-lock`, then copies
107
+ into `datasets/<id>/source/...`.
108
+ 6. Generates and uploads:
109
+ - `manifest.yaml`
110
+ - `indexes/content_hash_index.parquet`
111
+ - `indexes/masks_content_hash_index.parquet` when masks exist
112
+ - `metadata/sample_manifests.parquet`
113
+ - `metadata/samples.parquet`
114
+
115
+ Use `--dry-run` to scan and show the upload plan without S3 writes, `--no-skip`
116
+ to force uploads, or `--no-atomic` to disable staging.
117
+
118
+ ## Configuration
119
+
120
+ `~/.dsimaging.yaml` supports multiple profiles:
121
+
122
+ ```yaml
123
+ default_profile: default
124
+ profiles:
125
+ default:
126
+ endpoint: http://127.0.0.1:9000
127
+ controller_url: http://127.0.0.1:8080
128
+ bucket: imaging-data
129
+ access_key: minioadmin
130
+ secret_key: minioadmin123
131
+ region: ""
132
+ ```
133
+
134
+ Environment variables override profile values:
135
+
136
+ | Variable | Default | Description |
137
+ |---|---|---|
138
+ | `DSIMAGING_PROFILE` | `default` | Config profile |
139
+ | `DSIMAGING_ENDPOINT` | `http://127.0.0.1:9000` | S3/MinIO endpoint |
140
+ | `DSIMAGING_CONTROLLER_URL` | (empty) | dsimaging-store controller URL |
141
+ | `DSIMAGING_ACCESS_KEY` | `minioadmin` | S3 access key |
142
+ | `DSIMAGING_SECRET_KEY` | `minioadmin123` | S3 secret key |
143
+ | `DSIMAGING_BUCKET` | `imaging-data` | Bucket name |
144
+ | `DSIMAGING_REGION` | (empty) | S3 region |
145
+
146
+ ## Dataset layout in S3
147
+
148
+ ```text
149
+ s3://<bucket>/datasets/<dataset_id>/
150
+ manifest.yaml
151
+ metadata/
152
+ samples.parquet
153
+ sample_manifests.parquet
154
+ indexes/
155
+ content_hash_index.parquet
156
+ masks_content_hash_index.parquet
157
+ source/
158
+ images/
159
+ masks/
160
+ derived/
161
+ qc/
162
+ ```
163
+
164
+ Store creation and dataset management only target dsimaging-store deployments.
@@ -0,0 +1,139 @@
1
+ # dsimaging-admin
2
+
3
+ Admin CLI for creating and operating `dsimaging-store` deployments and the
4
+ medical imaging datasets stored in them.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install dsimaging-admin
10
+ ```
11
+
12
+ ## Create a store
13
+
14
+ `dsimaging-admin store init` writes a Docker Compose project for MinIO plus the
15
+ `dsimaging-store` controller.
16
+
17
+ ```bash
18
+ dsimaging-admin store init ./study-store \
19
+ --controller-image davidsarratgonzalez/dsimaging-store:latest
20
+ dsimaging-admin store up ./study-store
21
+ dsimaging-admin store doctor ./study-store
22
+ ```
23
+
24
+ For local controller development, build from a checked-out `dsimaging-store`
25
+ repo instead of using an image:
26
+
27
+ ```bash
28
+ dsimaging-admin store init ./study-store \
29
+ --store-source /path/to/dsimaging-store
30
+ dsimaging-admin store up ./study-store
31
+ ```
32
+
33
+ Use the generated connection details as a reusable CLI profile:
34
+
35
+ ```bash
36
+ dsimaging-admin init \
37
+ --endpoint http://127.0.0.1:9000 \
38
+ --controller-url http://127.0.0.1:8080 \
39
+ --bucket imaging-data
40
+ ```
41
+
42
+ ## Dataset operations
43
+
44
+ ```bash
45
+ # Publish with staging, publish lock, skip-if-hash-matches and DICOM checks.
46
+ dsimaging-admin publish \
47
+ --dataset-id study_ct_v1 \
48
+ --source /data/study_ct \
49
+ --metadata /data/study_ct/clinical.csv \
50
+ --modality ct
51
+
52
+ # Inspect and verify.
53
+ dsimaging-admin list
54
+ dsimaging-admin status study_ct_v1
55
+ dsimaging-admin verify study_ct_v1
56
+ dsimaging-admin doctor
57
+
58
+ # Rebuild artifacts from S3, copy, download or delete.
59
+ dsimaging-admin rescan study_ct_v1
60
+ dsimaging-admin copy study_ct_v1 study_ct_v2 --yes
61
+ dsimaging-admin download study_ct_v1 ./debug/study_ct_v1
62
+ dsimaging-admin delete study_ct_v1 --yes --purge-versions
63
+ ```
64
+
65
+ All reporting commands that are useful for automation support JSON output:
66
+
67
+ ```bash
68
+ dsimaging-admin list --output json
69
+ dsimaging-admin status study_ct_v1 --output json
70
+ dsimaging-admin verify study_ct_v1 --output json
71
+ dsimaging-admin doctor --output json
72
+ ```
73
+
74
+ ## What `publish` does
75
+
76
+ 1. Scans your local image directory and optional masks under `source/masks/`,
77
+ `masks/`, `source/labels/`, or `labels/`.
78
+ 2. Runs basic DICOM sanity checks for series UID, modality and instance order.
79
+ 3. Computes SHA-256 content hashes.
80
+ 4. Skips uploads that already match the current dataset hash indexes.
81
+ 5. Uploads through `datasets/<id>/.staging-*` and a `.publish-lock`, then copies
82
+ into `datasets/<id>/source/...`.
83
+ 6. Generates and uploads:
84
+ - `manifest.yaml`
85
+ - `indexes/content_hash_index.parquet`
86
+ - `indexes/masks_content_hash_index.parquet` when masks exist
87
+ - `metadata/sample_manifests.parquet`
88
+ - `metadata/samples.parquet`
89
+
90
+ Use `--dry-run` to scan and show the upload plan without S3 writes, `--no-skip`
91
+ to force uploads, or `--no-atomic` to disable staging.
92
+
93
+ ## Configuration
94
+
95
+ `~/.dsimaging.yaml` supports multiple profiles:
96
+
97
+ ```yaml
98
+ default_profile: default
99
+ profiles:
100
+ default:
101
+ endpoint: http://127.0.0.1:9000
102
+ controller_url: http://127.0.0.1:8080
103
+ bucket: imaging-data
104
+ access_key: minioadmin
105
+ secret_key: minioadmin123
106
+ region: ""
107
+ ```
108
+
109
+ Environment variables override profile values:
110
+
111
+ | Variable | Default | Description |
112
+ |---|---|---|
113
+ | `DSIMAGING_PROFILE` | `default` | Config profile |
114
+ | `DSIMAGING_ENDPOINT` | `http://127.0.0.1:9000` | S3/MinIO endpoint |
115
+ | `DSIMAGING_CONTROLLER_URL` | (empty) | dsimaging-store controller URL |
116
+ | `DSIMAGING_ACCESS_KEY` | `minioadmin` | S3 access key |
117
+ | `DSIMAGING_SECRET_KEY` | `minioadmin123` | S3 secret key |
118
+ | `DSIMAGING_BUCKET` | `imaging-data` | Bucket name |
119
+ | `DSIMAGING_REGION` | (empty) | S3 region |
120
+
121
+ ## Dataset layout in S3
122
+
123
+ ```text
124
+ s3://<bucket>/datasets/<dataset_id>/
125
+ manifest.yaml
126
+ metadata/
127
+ samples.parquet
128
+ sample_manifests.parquet
129
+ indexes/
130
+ content_hash_index.parquet
131
+ masks_content_hash_index.parquet
132
+ source/
133
+ images/
134
+ masks/
135
+ derived/
136
+ qc/
137
+ ```
138
+
139
+ Store creation and dataset management only target dsimaging-store deployments.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dsimaging-admin"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Admin CLI for managing medical imaging datasets in S3/MinIO for DataSHIELD"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,6 +28,7 @@ dependencies = [
28
28
  "pyyaml>=6.0",
29
29
  "click>=8.0.0",
30
30
  "requests>=2.31.0",
31
+ "pydicom>=3.0.0",
31
32
  ]
32
33
 
33
34
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """dsimaging-admin: Admin CLI for managing medical imaging datasets in S3/MinIO."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"