Skip to content

Add include/exclude mode to mass transfer filters#336

Open
NumericalAdvantage wants to merge 2 commits intomainfrom
mass-transfer-exclude-filters
Open

Add include/exclude mode to mass transfer filters#336
NumericalAdvantage wants to merge 2 commits intomainfrom
mass-transfer-exclude-filters

Conversation

@NumericalAdvantage
Copy link
Copy Markdown
Collaborator

@NumericalAdvantage NumericalAdvantage commented Apr 23, 2026

Summary

  • Each entry in filters_json can now set mode: "include" | "exclude" (default include). A series is kept if it matches at least one include filter and no exclude filter.
  • Exclude matching reuses the existing per-criterion logic via a new _series_matches_filter helper and does not issue its own C-FIND queries — all checks run client-side against the DiscoveredSeries already collected by the include pass, so excludes add zero PACS round-trips.
  • The old series-level pruning branches inside _discover_series are replaced by a single call to the helper, which is also used for the post-filter that applies excludes.

Validation

  • At least one filter must have mode=include (an all-excludes job is rejected).
  • An exclude filter must specify at least one criterion (an empty one would match everything).
  • apply_institution_on_study is documented as include-only and silently ignored on excludes; institution is always evaluated at series level on the DiscoveredSeries.

Why this approach

C-FIND has no anti-match operator, so any exclusion logic has to run client-side. DiscoveredSeries already carries every field the existing filter criteria reference (modality, institution, descriptions, series number, age from patient_birth_date+study_datetime, instance count), so excludes can be implemented as a pure post-filter without new queries, without a new schema change, and without touching the transfer phase.

Test plan

  • ruff check passes
  • pyright passes
  • Unit tests added for _series_matches_filter (modality, institution, series number, age strict vs permissive, check_institution=False, min_instances)
  • Integration tests added for _discover_series exclude behavior (single-exclude removes, multi-exclude OR semantics, exclude-by-institution, and a call-count assertion that excludes issue no extra C-FINDs)
  • Form validation tests added for exclude-accepted, all-excludes-rejected, empty-exclude-rejected, unknown-mode-rejected
  • Existing _discover_series tests still pass (the refactor is behavior-preserving for include-only filters)
  • Manual: load a job with an exclude filter in the UI and verify the discovery result in the job detail

Summary by CodeRabbit

Release Notes

  • New Features
    • Added filter modes ("include" and "exclude") to control series matching behavior—include filters match series, exclude filters remove matched series from results.
    • Exclude filters now require at least one matching criterion (modality, institution, description, series number, or age/instance bounds) to prevent misconfiguration.
    • Clarified filter application order: include filters applied first, then exclude filters remove matching results.

Each filter in filters_json now supports an optional mode field
("include" or "exclude", default "include"). After the include
discovery loop builds its DiscoveredSeries set, any series that
matches at least one exclude filter is dropped.

Exclude filters reuse the existing per-criterion matching logic
via a new _series_matches_filter helper and do not issue their
own C-FIND queries — all criteria are evaluated client-side
against the DiscoveredSeries already in memory.

Validation rejects jobs with no include filter and exclude
filters with no criteria. apply_institution_on_study is
documented as include-only and silently ignored on excludes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@NumericalAdvantage has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 51 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 145ce97a-fe6a-4fcb-b453-151f8fdd137e

📥 Commits

Reviewing files that changed from the base of the PR and between 65fc462 and bc8028b.

📒 Files selected for processing (2)
  • adit/mass_transfer/processors.py
  • adit/mass_transfer/tests/test_processor.py
📝 Walkthrough

Walkthrough

The changes introduce include and exclude filter modes to mass transfer functionality. The FilterSchema now supports a mode field (include/exclude, defaulting to include) with validation requiring exclude filters to have at least one matching criterion. The FilterSpec and discovery logic are updated to apply include filters first, then remove series matching any exclude filters. Help text and examples are updated to reflect these new semantics.

Changes

Cohort / File(s) Summary
Filter Schema Definition
adit/mass_transfer/forms.py
Added mode field (include/exclude) to FilterSchema with post-validation rule ensuring exclude filters contain at least one matching criterion (modality, institution/study/series descriptions, series number, or age/count bounds). Updated UI example JSON and help text to document include/exclude semantics and apply_institution_on_study behavior.
Filter Processing Logic
adit/mass_transfer/processors.py
Added mode field to FilterSpec dataclass. Introduced _series_matches_filter function centralizing series-level matching for modality, institution (conditionally), study/series description, series number, patient age bounds, and minimum instance count. Refactored _discover_series to apply include filters during study queries, evaluate each series against include specs, then post-filter by removing series matched by any exclude specs.
Form and Processor Tests
adit/mass_transfer/tests/test_forms.py, adit/mass_transfer/tests/test_processor.py
Added tests validating FilterSchema mode field behavior including exclusion of all-exclude filter lists and empty exclude filters. Added unit tests for FilterSpec.from_dict mode defaulting, exclude-mode series filtering, institution-based exclusion, age filtering with birth date handling, series description/modality matching, and verification that exclude filters do not trigger additional PACS queries.

Sequence Diagram

sequenceDiagram
    actor User
    participant Form as FilterSchema<br/>(Validation)
    participant Processor as _discover_series<br/>(Discovery)
    participant PacsDB as PACS Database
    participant Matcher as _series_matches_filter<br/>(Matching)

    User->>Form: Submit filters with modes
    Form->>Form: Validate mode field<br/>(include/exclude)
    Form->>Form: Check exclude filters<br/>have criteria
    Form->>Processor: Pass validated FilterSpecs
    
    Processor->>PacsDB: Query studies using<br/>include-mode filters only
    PacsDB-->>Processor: Return studies & series
    
    loop For each series discovered
        Processor->>Matcher: Check against<br/>include specs
        alt Matches include spec
            Matcher-->>Processor: Include series
        else Does not match
            Matcher-->>Processor: Skip series
        end
    end
    
    loop Post-filter against exclude specs
        Processor->>Matcher: Check against<br/>exclude specs
        alt Matches exclude spec
            Matcher-->>Processor: Mark for removal
            Processor->>Processor: Remove matched series
        end
    end
    
    Processor-->>User: Return filtered series
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

A rabbit hops through filters bright,
Include and exclude modes take flight,
First we gather what we seek,
Then banish those deemed too meek,
PACS queries reduced, logic sleek! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding include/exclude mode functionality to mass transfer filters, which is the primary focus of the changeset across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mass-transfer-exclude-filters

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces "exclude" filters to the mass transfer system, allowing users to filter out specific DICOM series after an initial inclusion. Key changes include updates to the FilterSchema and FilterSpec models to support a mode field, new validation logic to ensure at least one include filter is present, and the centralization of filtering logic into a _series_matches_filter helper function. Feedback was provided to use a Literal type for the mode field in the processor to improve type safety and maintain consistency with the form schema.

Comment thread adit/mass_transfer/processors.py Outdated
Built from a plain dict from the job's filters_json field.
"""

mode: str = "include"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better type safety and consistency with the FilterSchema defined in forms.py, consider using a Literal for the mode field. This helps static analysis tools like pyright (which is used in this repository) to catch invalid values.

Suggested change
mode: str = "include"
mode: Literal["include", "exclude"] = "include"

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@adit/mass_transfer/processors.py`:
- Around line 796-801: Add a processor-side guard in _discover_series() to
enforce the include-filter invariant: after computing include_filters = [mf for
mf in filters if mf.mode != "exclude"] and exclude_filters = [...], check that
include_filters is not empty (i.e., filters_json isn't all-exclude) and if it
is, raise a clear exception (e.g., ValidationError or ValueError) with a message
referencing filters_json/filters so the job fails fast instead of returning no
series; this complements clean_filters_json() and prevents jobs created outside
the form from silently succeeding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8029f450-d991-40ff-a286-ef58422292f7

📥 Commits

Reviewing files that changed from the base of the PR and between 0f67ddd and 65fc462.

📒 Files selected for processing (4)
  • adit/mass_transfer/forms.py
  • adit/mass_transfer/processors.py
  • adit/mass_transfer/tests/test_forms.py
  • adit/mass_transfer/tests/test_processor.py

Comment thread adit/mass_transfer/processors.py
- Type FilterSpec.mode as Literal["include", "exclude"] to match
  FilterSchema and let pyright catch invalid values.
- Validate mode in FilterSpec.from_dict so jobs persisted outside
  the form (e.g. admin, direct ORM) can't smuggle in an unknown
  mode string.
- Add a _discover_series guard that raises DicomError when no
  include filter is present, turning a silent empty-result into
  a loud failure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant