routes
HTTP routes for the patient-data API.
Every parameter states its description and its constraints on the Query/Path
marker, not only in the handler docstring. FastAPI builds the OpenAPI document from
the markers and ignores a docstring's Args: section, so a description left only
in the docstring reaches no consumer of the document — and a constraint enforced
only inside the handler documents the endpoint as accepting values it rejects.
Module
Functions
get_eligibility_counts
def get_eligibility_counts( trial_ids: "Annotated[list[str], Query(min_length=1, description='Trial (project) IDs, repeated once per ID (`?trial_ids=t1&trial_ids=t2`). At least one non-blank ID is required. A trial with no published `patient_eligibility` partition comes back with all three counts zeroed rather than being omitted.'), AfterValidator(_require_non_empty_ids)]", repository: PatientDataRepository = Depends(dependency=<function get_repository>, use_cache=True, scope=None), user: AuthenticatedUser = Depends(dependency=<function verify_jwt>, use_cache=True, scope=None),) ‑> dict[str, EligibilityCounts]:Return eligibility counts for the requested trials.
Arguments
trial_ids: Trial (project) IDs, one per repeatedtrial_idsquery param (e.g.?trial_ids=t1&trial_ids=t2); at least one required.repository: The patient-data repository.user: The authenticated caller.
Returns A mapping of each requested trial ID to its status counts.
get_patient
def get_patient( bitfount_patient_id: BitfountPatientIdPath, repository: PatientDataRepository = Depends(dependency=<function get_repository>, use_cache=True, scope=None), user: AuthenticatedUser = Depends(dependency=<function verify_jwt>, use_cache=True, scope=None),) ‑> PatientSummary:Return a single patient by ID.
Arguments
bitfount_patient_id: The Bitfount patient ID.repository: The patient-data repository.user: The authenticated caller.
Returns The patient summary.
Raises
HTTPException: 404 if the patient is not found.
get_patient_eligibility_evidence
def get_patient_eligibility_evidence( bitfount_patient_id: BitfountPatientIdPath, project_id: ProjectIdPath, repository: PatientDataRepository = Depends(dependency=<function get_repository>, use_cache=True, scope=None), user: AuthenticatedUser = Depends(dependency=<function verify_jwt>, use_cache=True, scope=None),) ‑> EligibilityEvidence:Return eligibility evidence for a patient on a specific trial.
Arguments
bitfount_patient_id: The Bitfount patient ID.project_id: The trial (project) ID.repository: The patient-data repository.user: The authenticated caller.
Returns The eligibility evidence.
Raises
HTTPException: 404 if no evidence exists for the pair.
get_scan_image
def get_scan_image( scan_id: "Annotated[str | None, Query(description='The scan identifier to resolve. Supply exactly one of `scan_id` or `path`; supplying both, or neither, is a `400`.')]" = None, path: Annotated[str | None, Query(description="An explicit file path, which must resolve inside the pod\'s configured allowlist. Supply exactly one of `scan_id` or `path`.")] = None, modality: Annotated[str | None, Query(description="Series selector for a multi-series file (e.g. `.e2e`). Matched case-insensitively against any of three spellings: the modality group (`OCT`/`SLO`, as `manifest.json` reports it and as a task filter spells it), the finer series description (`manifest.json`\'s `series_modality` — `slo - red`), or the parser\'s short code (`SLO_R`). Prefer the group. Ignored on the DICOM path, which has a single series.", examples=[\'OCT\', \'SLO\', \'slo - red\', \'SLO_R\'])] = None, laterality: "Annotated[str | None, Query(pattern='^[LRlr]$', description='Series selector, `L` or `R` (case-insensitive). A series whose own laterality is unknown is not excluded by this filter — an absent value cannot be disproven against the requested side. Only a known, differing laterality excludes a series.')]" = None, width: Annotated[int | None, Query(ge=1, description="Output width in px; defaults to the pod\'s configured width. Each frame is clamped to its own source width, so a value above that does not upscale — `manifest.json` reports the width actually encoded alongside the source width.")] = None, refresh: "Annotated[bool, Query(description='Re-render the archive and replace the cached copy instead of serving it. The response is identical either way; this only decides whether the cache is trusted.')]" = False, include_segmentation_masks: "Annotated[bool, Query(description='Also carry one pre-rendered mask PNG per segmentation class, under `masks/`, rasterised to the dimensions the frames were served at. Independent of `include_segmentation_vectors`: the two outputs cost very differently, so asking for one never pays for the other.')]" = False, include_segmentation_vectors: Annotated[bool, Query(description="Also carry each class\'s raw drawable instances in the served segmentation index, for a client that renders the overlays itself. They stay in the model\'s own coordinate space and ship with per-source `scale_x`/`scale_y` to scale by.")] = False, segmentation_classes: "Annotated[list[str] | None, Query(description='Class-name filter, one name per repeated `segmentation_classes` parameter (e.g. `?segmentation_classes=a&segmentation_classes=b`). It narrows whichever outputs are selected and is not itself an opt-in: supplying it with neither flag set is a `400`. A name no model produced yields nothing for it rather than an error, since which classes exist depends on the model that ran.'), AfterValidator(_clean_segmentation_classes)]" = None, repository: PatientDataRepository = Depends(dependency=<function get_repository>, use_cache=True, scope=None), user: AuthenticatedUser = Depends(dependency=<function verify_jwt>, use_cache=True, scope=None),) ‑> starlette.responses.Response:Return a scan's frames as a ZIP of scaled WebP images.
Arguments
scan_id: The scan identifier (exactly one of scan_id/path).path: An explicit, allowlisted file path (exactly one of scan_id/path).modality: Optional series selector for multi-series files.laterality: Optional "L"/"R" series selector.width: Optional output width (px); defaults to the configured width.refresh: When true, bypass and overwrite the cached archive.include_segmentation_masks: When true, the archive also carries one pre-rendered mask PNG per segmentation class. Absent or false means no mask PNGs. Independent ofinclude_segmentation_vectors: the two outputs cost very differently, so selecting one never pays for the other.include_segmentation_vectors: When true, the served segmentation index also carries each class's raw drawable instances, for a client that renders overlays itself. Absent or false means no instances.segmentation_classes: Optional class-name filter, one per repeatedsegmentation_classesquery param (e.g.?segmentation_classes=a&segmentation_classes=b). It narrows whichever outputs are selected and is not itself an opt-in — supplying it with neither flag set is a 400. A name that matches no class the model produced simply yields nothing for it; which classes exist depends on the model that ran, so an unrecognised name is never an error.repository: The data repository, which resolves the scan and renders the image via its composed scan-image service.user: The authenticated caller.
Returns
A application/zip response of frame_NNN.webp files plus
manifest.json. Selecting either segmentation output also carries a
segmentations.json index: it lists each class's mask PNG path under
masks/ when include_segmentation_masks is true, and each class's raw
instances when include_segmentation_vectors is true. A scan with no
segmentation available gets the index with every frame's sources
empty, rather than an error or a missing index — a caller reads
availability from sources, not from whether the file is present. A
failure building the overlays does drop the index, since the images must
never be lost to an overlay.
Raises
HTTPException: 400 for bad requests, includingsegmentation_classessupplied with neither segmentation output selected; 404 when the scan is not found or the scan-image feature is not enabled.
health
def health() ‑> HealthStatus:Report liveness for connection checks.
Returns
A fixed {"status": "ok"} payload.
list_patients
def list_patients( search: str | None = None, eligible_trials: Annotated[list[str] | None, Query(description="Zero or more trial (project) IDs; return only patients eligible for **any** of them. Each summary\'s `eligible_trials` still lists every trial the patient is eligible for, unaffected by this filter.")] = None, page: Annotated[int, Query(ge=1)] = 1, page_size: Annotated[int, Query(ge=1, le=200)] = 50, repository: PatientDataRepository = Depends(dependency=<function get_repository>, use_cache=True, scope=None), user: AuthenticatedUser = Depends(dependency=<function verify_jwt>, use_cache=True, scope=None),) ‑> PatientList:List patients, filtered and searched server-side, one page at a time.
Arguments
search: Case-insensitive substring matched against a patient's name or EHR id.eligible_trials: Zero or more trial (project) IDs, one per repeatedeligible_trialsquery param (e.g.?eligible_trials=t1&eligible_trials=t2); restricts to patients eligible for any of them. Blank values are ignored.page: 1-indexed page number.page_size: Items per page (1–200, default 50).repository: The patient-data repository.user: The authenticated caller.
Returns
A PatientList envelope: the requested page in items, and the count
of all matching patients (before pagination) in total.