Spleen data-enrichment (labels) depends on a private repo — can't run the spleen e2e outside the org or in CI - #955
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
27d1b57 to
14078c9
Compare
Data enrichment — adding labels that FLIP cannot pull from PACS — was documented only in the abstract and tooled only in a private repo. This adds the tooling in-tree and a user guide covering it end to end. Closes #776. flip.xnat (new, flip-utils): - XnatClient over requests (no new dependencies; requests is already core), mirroring the REST shapes imaging-api already uses. Resolves the XNAT project by secondary_ID, lists scans, uploads scan resource files. - upload_enrichment_files(): manifest-driven and project-agnostic. Derives each target filename from the image already in the scan's resource (input_ -> label_), which is the pairing the apps rely on, rather than guessing it. Skip-if-exists by default; refuses equal rename prefixes, which would overwrite the image. - flip-xnat console script, also runnable as python -m flip.xnat. - Runs on the developer's workstation inside the Trust network, never in the FL client, which holds no XNAT credentials. Spleen tutorial (removes the private-repo dependency): - utils/upload_spleen_labels_to_xnat.py replaces the uploader that lived in londonaicentre/flip_project_spleen_segmentation. The accession-to-MSD-case mapping is fetched at run time from the public aicentreflip/trust-data OMOP export, which reproduces the private mapping exactly (41/41 rows) and additionally carries source_trust, so each label is uploaded only to the Trust that holds that study instead of attempting both. Nothing is checked in and no private artefact is needed. - upload-spleen-labels target in both backend Makefiles; enrichment is a one-time, backend-agnostic step, so Flower delegates to the single copy of the script rather than duplicating it. - flip-api's e2e_smoke_spleen targets and the demo-video docs repoint at it. Fail loudly: - The five spleen training/evaluation entry points now raise a message naming the missing label_ sibling and the image count, instead of letting an empty dataset surface as torch's num_samples=0 after a full pull cycle. Docs: - New user guide, docs/source/user-guides/user-data-enrichment.rst, with both the manual XNAT-UI route and the scripted one. - It leads with which route a label should take: labels already in OMOP (a lab result, a coded report finding) belong in the cohort query and need no enrichment at all — as the chest X-ray tutorial demonstrates. Enrichment is for labels with nowhere to live in OMOP, chiefly segmentation masks. - Cross-links from user-common, flip-workflow and component-xnat; a flip.xnat section in the flip-utils overview; both spleen READMEs. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
14078c9 to
c53ff7b
Compare
garciadias
left a comment
There was a problem hiding this comment.
This is a substantial and largely well-built replacement. The contract from the e2e-smoke section of the instructions file is honoured on all four points I checked: enrichment runs after the pull and after DICOM-to-NIfTI conversion, it resolves each trust's XNAT project by secondary_ID, and it writes each label into the scan's existing NIFTI resource with input_ renamed to label_. Apache headers are present on all eleven new source files, there are zero new dependencies (only [project.scripts]), so no cooldown or lock concern arises, the new Make targets are documented, and the AGENTS mirror is exact. The CLAUDE.md text about the private repo, .xnat1.cfg/.xnat2.cfg, and "tracked in FLIP#776" is all correctly removed.
I also want to record two things explicitly, since both were prominent risks going in. There are no hardcoded credentials or hostnames anywhere in this change — every example is a placeholder. And the labels are deliberately not republished, so the MSD CC-BY-SA attribution obligations are unchanged; only the accession-to-case mapping is new.
Both earlier review points are properly resolved at this head. not_found_ok with 404-only handling is in place with all four claimed tests, and I swept the rest of the enrichment path for other swallowed exceptions and found none. The per-scan versus per-accession decision is sound as argued — the contract fix is the right call over deduplication, and the rendered output is unambiguous. Neither needs revisiting.
Three things should change before this merges.
Enrichment only covers one trust. e2e_smoke_spleen invokes the command once, but the new script talks to exactly one XNAT per run, whereas the private script it replaces was multi-trust in a single invocation. On the standard two-trust dev roster the second trust gets no labels and its client dies on the new zero-pairs guard. The user guide already says to run it once per trust, and the PR's own verification table drove GSTT and KCH separately, so this looks like an oversight — but the instructions file currently reads as though the target is complete, which will cost someone a full pull cycle.
An all-skipped run still exits 0. ok is defined as "nothing failed", so a run that resolved zero destinations reports success and returns 0 from both entry points. run_data_enrichment only raises on a non-zero exit, so the smoke treats a no-op enrichment as done and proceeds into training. The loud, actionable summary genuinely fixes the interactive case — this is the scripted case, where the signal the automation consumes is unchanged. Since removing that opaque num_samples=0 failure is the point of the issue, closing the automated half seems worth doing here.
The overwrite flag never reaches the request. The URL always carries just inbody=true; overwrite only selects between two error messages. Either --overwrite cannot overwrite, or overwrite=False protects nothing — which of the two depends on how XNAT responds to an existing file, and imaging-api's own pre-check suggests the latter. The product path is safe today because the enrichment layer pre-checks, so this is about the public client method and its docstring promise.
The five minor comments cover the mutable main revision with no checksum or cache on the mapping fetch, the partial-label-set case passing quietly when NUM_CASES is 10 against a 41-accession mapping, the --trust choices being pinned to the two-trust mock roster, an unescaped manifest target_filename reaching URL construction, and the new XNAT_HOST/XNAT_USER/XNAT_PASS triple not lining up with the names the kit files already use.
Two notes that are not change requests:
On closing #776. No CI workflow is added, and none is claimed, so this is not a checklist mismatch — but the "in CI" half of the issue is enabled rather than delivered, since a run still needs a live stack, XNAT credentials and egress to huggingface.co. Worth deciding whether #776 closes here or keeps a CI-enablement tail.
One thing I could not verify. Whether the Flower delegation's data/spleen/accession-resources tree actually uses the subject_N/scans/ names that build_manifest requires. The dataset is not local, and the verification in the description covers the NVFLARE labels directory and argument expansion. Could you confirm the Flower path end to end, or point me at a run that did?
…oad-data-enrichment
Review of #955 found three blocking gaps and five smaller ones. Addressed here. Enrichment covered one trust. The uploader talked to a single XNAT per run while e2e_smoke_spleen invoked it once, so the second trust got no labels and its FL client died at the new zero-pairs guard minutes into training. --credentials-file is now repeatable and --xnat-url added, so one invocation covers the roster; the manifest is self-selecting because an accession exists at exactly one trust. A run that resolved zero destinations exited 0. `ok` means "nothing failed", which is true of a run where every scan was skipped — the broken-Container-Service case this work exists to surface. `ok` is unchanged; a separate `resolved_any` drives a shared EnrichmentReport.exit_code(), with --allow-no-op for a genuinely empty run. The overwrite flag never reached the request. XNAT has no overwrite parameter and its PUT replaces silently, so overwrite=False protected nothing; on a 409 both branches raised, making --overwrite unable to overwrite. Now pre-checked in the client, mirroring imaging-api. Also: percent-encode caller-supplied URL segments and reject non-bare manifest target_filenames (a "?" or "#" silently dropped inbody=true); make the HF mapping revision overridable and cache it on disk; report coverage with the correct post-filter denominator and name the missing cases; validate --trust against the fetched mapping instead of hardcoded choices; document the kit-name to XNAT_HOST correspondence and that --trust is the OMOP partition, not the FL kit slot. Two defects found while verifying, not in the review: a command-line EXTRA_ARGS beat the target-specific one and silently dropped enrichment from e2e_smoke_spleen (now a separate ENRICHMENT_ARGS), and cli.py had no tests at all (new test_cli.py takes it 0% -> 97% and pins the exit contract). Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
|
Thanks for the depth here — the three blocking items were all real, and two turned out worse than the review had them. All eight threads are addressed in a12f9e5 and answered individually; this covers the two questions and the places I did not do what was suggested. The Flower path you could not verify — confirmed, with a caveat
But checking it surfaced something else: that tree holds 6 of 41 cases (3 per trust), so the Flower delegation silently produced ~14% enrichment, and the error text it emitted told the user to run #776Keeping Where I did not do what was suggested
Two defects found while verifying, not in the review
Verification
Behaviour checked directly rather than only by unit test: the mapping cache makes one network call for two reads and re-fetches on a version bump; |
|
Live verification of the new code, run against the dev stack. CI is green on a12f9e5 (34 pass, 1 skip — the PR-to- One caveat up front: I could not run the spleen target with its own roster defaults. Multi-trust in one invocation — two Both servers visited; target names derived from the real converted images in XNAT, not from the local filename; the trust that does not hold the project soft-skipped with Exit codes, same live servers:
The second row is the one that mattered: that run prints Also confirmed the Everything was Follow-up filed as #998 for the CI-enablement tail of #776, and it also picks up the lexicographic |
Codecov flagged 10 uncovered lines in flip.xnat. Covering them found two real defects rather than just moving a number. Network failures were reported as local-file errors. requests.RequestException subclasses OSError, so `except OSError` listed first caught every transport failure and raised "Could not read <local file>" — sending the operator to inspect a file that is perfectly fine, while the actual cause was the network. That also made the RequestException branch dead code, which is exactly why coverage flagged it. Handlers reordered, specific before general. The manifest bare-filename guard missed "?" and "#" — the very characters that motivated it. Both are legal in POSIX filenames, so `Path(name).name` accepts them, and only the client-side quoting was stopping them severing the URL. Rejection now covers path separators, "." / "..", and the URL-significant characters, so the manifest layer gives the clear error it promised. flip.xnat is now at 100% line coverage: added tests for the unreadable-file and transport-failure upload paths, the manifest validation (four rejection cases plus the accepted one), the failed-upload and missing-project-id exit codes, and `python -m flip.xnat` propagating main()'s exit code via runpy. FakeSession gains put_error to mirror its existing get_error. The one remaining exclusion is cli.py's `if __name__ == "__main__"` guard, unreachable once __main__.py covers the same call. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
|
Addressed the Codecov patch-coverage report in 6c90374. Covering those 10 lines turned up two real defects, so this is worth more than the number moving. Network failures were reported as local-file errors. My own bare-filename guard missed
The single remaining exclusion is
|
garciadias
left a comment
There was a problem hiding this comment.
Good in-tree replacement for the private-repo label upload. One correctness gap blocks merge: with a multi-trust roster where no server resolved the project, run_enrichment exits 0 silently, defeating the fail-loud goal. Please handle the all-missing case and add a test.
…un-pulled Trusts as uncovered With two or more servers a per-server XnatProjectNotFound is a legitimate skip, so a roster where *no* server had pulled the project produced no summary at all: `requested` fell back to 0 and the no-op guard never fired, exiting 0 on the exact silent no-labels case the guard exists for. `exit_code()` now fails when there are outcomes but no summaries — the only way to reach that is every server erroring, fatal ones being caught already — regardless of `--allow-no-op`, since a project existing at no Trust means the image pull never ran. `fully_covered` had the mirror-image blind spot: an un-pulled Trust contributes no scans to the denominator, so one fully-enriched Trust vouched for the whole roster. It now also requires every visited server to have produced a summary. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
…-pairs guard The other three spleen entry points (both Flower apps and the NVFLARE evaluator) raise RuntimeError for the same "no image/label pairs" condition; the NVFLARE trainer raised ValueError. Nothing in-tree catches either type on this path, so this is consistency across the four guards rather than a behaviour change. The ValueError at the missing-accession_id column check stays — that one genuinely is a bad-value condition. Signed-off-by: at24_bioeng625-pc <alexandre.triay_bagur@kcl.ac.uk>
Description
Data enrichment — adding the labels FLIP cannot pull from PACS — was documented only in the abstract and tooled only in a private repo (
londonaicentre/flip_project_spleen_segmentation, itself depending on the private, unlicensedlondonaicentre/xnat_utils). This ships the tooling in-tree and documents the stage end to end.No private repo or private package is required any more, and no data artefact is committed to do it.
What's here
flip.xnat— new subpackage inflip-utilsXnatClientbuilt onrequests(zero new dependencies —requestsis already a core dep), mirroring the REST shapesimaging-apialready uses. Resolves the XNAT project bysecondary_ID, lists scans, uploads scan resource files.upload_enrichment_files()— manifest-driven (accession_id,file_path[,target_filename]) and project-agnostic. It derives each target filename from the image already in the scan's resource (input_→label_), which is the pairing the apps rely on, rather than guessing it. Skip-if-exists by default; refuses equal rename prefixes, which would overwrite the image.flip-xnatconsole script, also runnable aspython -m flip.xnat.flipships inside every FL image.Spleen tutorial — the private-repo replacement
…/3d_spleen_segmentation/utils/upload_spleen_labels_to_xnat.py, alongside the existing spleen data helpers.aicentreflip/trust-dataOMOP export, not committed. Verified it reproduces the privatetables/dicom_metadata.csvjoin exactly (41/41 rows, identical case for every accession) and additionally carriessource_trust— 21 trust1 / 20 trust2, matching the published Orthanc volumes with zero misplacements — so each label is uploaded only to the Trust that actually holds that study, rather than attempting both.upload-spleen-labelstarget in both backend Makefiles. Enrichment is a one-time, backend-agnostic step (the labels live in XNAT; both backends read them), so Flower delegates to the single copy of the script with its own--labels-dirrather than duplicating it.flip-api'se2e_smoke_spleen/e2e_smoke_spleen_evaluationtargets and the demo-video docs repoint at it.Fail loudly
The five spleen train/eval entry points now raise
No image/label pairs found: N image(s) …, none with a matching label_*.nii.gznaming the likely cause, instead of letting an empty dataset surface as torch's opaquenum_samples=0after a full pull cycle.Documentation
docs/source/user-guides/user-data-enrichment.rst, covering both the manual XNAT-UI route and the scripted one, the ordering constraint, the placement contract, and troubleshooting.Effusion/Edema/Normalas dataframe columns. Enrichment is for labels with nowhere to live in OMOP, chiefly segmentation masks.user-common,flip-workflowandcomponent-xnat; aflip.xnatsection in theflip-utilsoverview (also the PyPI long description); both spleen tutorial READMEs.Note for reviewers
The accession mapping is read from the OMOP CSV export for the data version this checkout deploys — the URL is built from
trust/omop-db/.data_version(currently20260729), so the mapping and the pgdata the trusts are seeded from share a single pin and cannot drift apart. If that file is missing or empty the script stops rather than guessing a version and silently fetching a mapping for different mock data.Linked Issues
Closes #776
Checklist
Type of Change
make -C docs/ docs.Verification performed
flip-utils: 729 tests pass (688 pre-existing + 41 new forflip.xnat), ruff clean,flip/xnatmypy-clean (the 6 reported errors are the pre-existingflip/corebaseline, unchanged).make -C docs docs— build succeeded, no new warnings; the new page registers in the User guides toctree.fl-tutorialslint unchanged at 18 pre-existing errors (verified the one in a file I touched,client.py, exists ondevelop);scripts/check_tutorial_sync.shgreen.flip-utils, fetches the mapping from HF (41 accessions, 21/20 split), pairs labels against the real MSD download, and stops at the credentials check. Missing-argument guard fires on both backends including through the Flower delegation.$FLIP_PROJECT_IDescaping through the new Makefile indirection confirmed by inspecting the expanded recipe.Verified live against the dev trust stack. Brought up GSTT + KCH, created a spleen project (
cf497b94-…), waited for the pull, then exercised the tool end to end:resolve_project_by_flip_project_idsecondary_IDon both trustslist_scans/list_resource_fileslabel_spleen_63.nii.gz→label_CT_Spleen_20001201230811_1871960997.nii.gz, derived from the converted image in XNAT, not the local filenamesource_trustpredictsNIFTIresource now holdsinput_CT_Spleen_….nii.gzandlabel_CT_Spleen_….nii.gzThe KCH shortfall is a trust-side pull stall, not this change: imaging-api reports
Processing=3, Successful=17, Failed=0on a freshly-booted XNAT (the known directArchive/DQR stall), Orthanc logs no C-STORE aborts, and a permissions fault would have failed all 20 rather than 3. The tool reports those three honestly as skips rather than failing or inventing a match.Also confirmed live: a DICOM resource (no
input_prefix) derives no target name, so it is skipped rather than risking an overwrite of imaging data.Update — merged
develop(5772968)Merged 129 commits of
develop, chiefly the client-API migration (#983) and Flower differential privacy (#833). What that changes about the diff above:#983deleted two of them outright (3d_spleen_segmentation/client.py,3d_spleen_segmentation_evaluation_client_api/) and its rewritten3d_spleen_segmentation/app_files/trainer.pyalready raises on an empty datalist — so that entry point is covered ondevelopand needs nothing here. The remaining three (both Flowerdata_loading.pyfiles and the NVFLARE3d_spleen_segmentation_evaluationevaluator) carry the change; the evaluator edit landed byte-identically on the file that replaced the deleted_client_apicopy.develop's trainer raisesValueErrorwith its own phrasing, this PR's three raiseRuntimeErrorand point at the new Data Enrichment guide. Worth unifying, but deliberately left alone here rather than rewriting merged code in a merge commit.make -C fl-tutorials test-templateis gone — removed ondevelop. The three Makefiles keep that removal and addupload-spleen-labels.e2e_smoke_spleenuploads the Flower tutorial paths (develop's change) while the enrichment step runs out of the NVFLARE tree (backend-agnostic, this PR's design).CLAUDE.md/AGENTS.mdnow state both.develop's simulator + differential-privacy sections, this PR's data-enrichment section).Verification re-run post-merge
fl-tutorials: ruff clean (the 18 pre-existing errors noted above were fixed ondevelop), 90 passed / 12 skipped.flip-utils: ruff clean, 748 passed (46 inflip/xnat),uv lock --checkin sync.scripts/check_tutorial_sync.shgreen;make -C docs docsbuild succeeded with 1 warning, pre-existing ondevelop(component-fl-nodes.rst, untouched here).upload-spleen-labelsexpands and its missing-argument guard fires on both backends, including through the Flower delegation.mainguard, correct for adevelop-targeted PR).Update — review round 2 (a12f9e5)
garciadias's review found three blocking gaps and five smaller ones. All eight are addressed; each thread carries its own reply. Two turned out worse than the review had them, and verifying them surfaced two further defects.The three blocking ones
e2e_smoke_spleeninvoked it once, so the second trust got no labels and its client died at the new zero-pairs guard minutes into training — silently, becauseskipped_no_scanis notfailed, so the step exited 0.--credentials-fileis now repeatable and--xnat-urladded, so one invocation covers the roster;flip-api/MakefilegainsSPLEEN_XNAT_URLS(both dev trusts, credentials from the environment — no secrets in the Makefile). The manifest is self-selecting: an accession exists at exactly one trust, and the others report it as no matching scan. A trust that never pulled the project is a soft skip with a roster, still fatal alone.ok("nothing failed") is unchanged — it is a public property with an accurate name. A newresolved_anydrives a sharedEnrichmentReport.exit_code(), aggregate rather than per-server.skipped_no_resourcedeliberately does not count as resolved, so the ran-before-conversion case now fails too.--allow-no-opopts out;--dry-runis not special-cased, so a rehearsal predicts the real exit code.overwritenever reached the request — and worse than reported: on a 409 both branches raised, so--overwritecould not overwrite, it only made the error opaque. XNAT has no overwrite parameter, so it is now pre-checked in the client, mirroringimaging_api/services/upload.py:285-287.The five smaller ones
target_filenameis rejected at parse time unless bare, and percent-encoded at the point of use (verified againstrequests2.34.2:parse_urlsplits on?/#before quoting, so a#silently droppedinbody=true);HF_TRUST_DATA_REVISIONis env-overridable, followingupdate_omop_data.sh:38, and the mapping is cached on disk so a re-run needs no huggingface.co egress; coverage is reported with the correct post-filter denominator, at WARNING, naming the missing cases;--trustis validated against the fetched mapping instead of hardcodedchoices; and the kit-name →XNAT_HOSTcorrespondence is documented.Two suggestions not taken, argued in-thread: no sha256 on the mapping (no download in this repo is checksummed, and three other readers hit the same dataset at
main— worth doing across all four as its own change), and coverage warns rather than fails by default (fail-by-default would breake2e_smoke_spleen, which sets noNUM_CASES, and is unsatisfiable through the Flower delegation;--require-full-coverageopts in).Found while verifying, not in the review
EXTRA_ARGSsilently erased enrichment. A command-line variable beats a target-specific one, somake -C flip-api e2e_smoke_spleen EXTRA_ARGS="--project-id <uuid>"— the documented fast-iterate flow — ran with no enrichment and died at the zero-pairs guard. Enrichment flags now ride in a separateENRICHMENT_ARGSthat composes.cli.pyhad no tests at all (0%; codecov's 39 missing lines), so no exit code in the subpackage was pinned. Newtest_cli.py, 13 cases,cli.py→ 97%.NUM_CASESknob, so its remediation message pointed at a variable that does nothing there. The text now names both backends unconditionally, and the Flower README says so.Verification
flip-utils776 passed (748 → +28), ruff clean,flip/xnatmypy-clean.fl-tutorials90 passed / 12 skipped, ruff clean,check_tutorial_sync.shgreen.make -C docs docssucceeded with no Sphinx warnings.uv lock --checkin sync.developmerged.Checked directly, not only by unit test: the cache makes one network call for two reads and re-fetches on a version bump;
HF_TRUST_DATA_REVISIONreaches the URL;--trust 7is rejected naming the trusts present;--xnat-urlwithout credentials fails clearly; andmake -n e2e_smoke_spleen EXTRA_ARGS="--project-id 1234"expands with both--xnat-urlflags and the--project-id. A two-trust live dry run against the dev stack is the remaining check before merge.Update — review round 3 (ee74d1c, 4dc7c94)
Three threads, all accepted.
XnatProjectNotFoundis a legitimate skip, so when every server raised it noEnrichmentSummarywas produced at all:requestedfell back to 0 and the no-op guard never fired — the exact silent no-labels case round 2 added it for.exit_code()now fails when there are outcomes but no summaries (fatal ones are caught above it, so the only way to reach that state is every server erroring), and does so regardless of--allow-no-op: a project existing at no trust means the image pull never ran there. Covered at library level and through the real CLI with--allow-no-opset.--require-full-coveragewas blind to an un-pulled trust. Its denominator sums only the summaries that exist, so an un-pulled trust contributed zero scans and one fully-enriched trust vouched for the whole roster.fully_coverednow also requires every visited server to have produced a summary. Covered by a one-holder/one-absent roster assertingexit_code() == 0butexit_code(require_full_coverage=True) == 1.ValueErrorwhere the two Flower apps and the NVFLARE evaluator raiseRuntimeError; it now raisesRuntimeErrortoo. ThatValueErrorwas pre-existing code from f651b9b, not one of this PR's guards, and nothing in-tree catches either type on that path — so this is consistency, not a behaviour fix. The missing-accession_id-columnValueErrorin the same file is left alone; that one genuinely is a bad-value condition.The
--allow-no-opand--require-full-coveragerows in the user guide, its "resolves nothing" note, itssecondary_IDtroubleshooting row, and theflip-utilsoverview all now state these contracts.Verification
flip-utils789 passed (776 → +13, of which +2 for these fixes and the rest from the round-2 follow-up commit), ruff clean,flip/xnatmypy-clean (the 60 reported errors are the pre-existingflip/core,flip/flowerandflip/nvflarebaseline — none inflip/xnat).make -C fl-tutorials test: ruff clean, 90 passed / 12 skipped. Both edited.rstfiles parse without error.