Skip to main content

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 repeated trial_ids query 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 of include_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 repeated segmentation_classes query 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, including segmentation_classes supplied 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 repeated eligible_trials query 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.