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 publishes each marker as the parameter's own description, so that is the only spelling a consumer of the document sees against the parameter — and a constraint enforced only inside the handler documents the endpoint as accepting values it rejects.

A handler docstring is not a second place to document a parameter. FastAPI takes the whole docstring as the operation's description, so an Args: block is published verbatim, including the entries for Depends-injected arguments a client never sends. build_openapi trims each description to its caller-facing prose for that reason; see _endpoint_description.

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,    project_id: LastAnalysedScopeQuery = 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),) ‑> PatientSummary:

Return a single patient by ID.

Arguments

  • bitfount_patient_id: The Bitfount patient ID.
  • project_id: Scopes last_analysed_image_date to one trial.
  • 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_patient_scan_evidence​

def list_patient_scan_evidence(    bitfount_patient_id: BitfountPatientIdPath,    project_id: ProjectIdPath,    page: Annotated[int, Query(ge=1, description=_PAGE_DESCRIPTION)] = 1,    page_size: Annotated[int, Query(ge=1, le=200, description=_PAGE_SIZE_DESCRIPTION)] = 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),) ‑> ScanEvidenceList:

Return each scan's own eligibility verdict and evidence for a trial.

The sibling evidence endpoint serves the patient rollup, which carries the criteria of one determined_by_scan and nothing from the other scans. This serves every scan the trial evaluated, so a caller can show a measured eye beside an unmeasurable one and see which scan decided the verdict.

Unlike that endpoint this does not 404 on an unknown pair: a patient with no scans in the trial's published partition is an empty page, not an error, which is the same thing an out-of-range page returns.

Arguments

  • bitfount_patient_id: The Bitfount patient ID.
  • project_id: The trial (project) ID.
  • 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 ScanEvidenceList envelope: the requested page in items, and the count of the patient's scans for the trial in total.

list_patients​

def list_patients(    search: Annotated[str | None, Query(description="Case-insensitive substring matched against a patient\'s name or EHR ID. Omitted or blank matches everyone.")] = None,    trial: Annotated[str | None, Query(description="One trial (project) ID; return that trial\'s cohort — every patient it evaluated, whatever the verdict. Narrow to particular verdicts with `statuses`. Single-valued, unlike `eligible_trials`: repeating it is a 400, not a union. Each summary\'s `trial_statuses` still reports every trial the patient has a verdict for, unaffected by this filter.")] = None,    statuses: "Annotated[list[EligibilityStatus] | None, Query(description='Which verdicts `trial` (or `eligible_trials`) admits, repeated once per value (`?statuses=ineligible&statuses=unknown`). Omitted, every verdict is admitted. Ignored when neither trial filter is given.')]" = None,    eligible_trials: "Annotated[list[str] | None, Query(description='Zero or more trial (project) IDs, repeated once per ID; return only patients eligible for **any** of them. The multi-trial filter: `trial` takes one trial and reads any verdict, this takes several and reads only `eligible`, so neither subsumes the other. Pass one or the other, never both.')]" = None,    project_id: LastAnalysedScopeQuery = None,    page: Annotated[int, Query(ge=1, description=_PAGE_DESCRIPTION)] = 1,    page_size: Annotated[int, Query(ge=1, le=200, description=_PAGE_SIZE_DESCRIPTION)] = 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.
  • trial: One trial (project) ID whose cohort to return — every patient the trial evaluated, narrowed by statuses. Blank is ignored.
  • statuses: Which verdicts the trial filter admits, one per repeated statuses query param (e.g. ?statuses=ineligible&statuses=unknown). Omitted, all three are admitted.
  • eligible_trials: Zero or more trial (project) IDs; restricts to patients eligible for any of them (union). Blank values are ignored. Reads only the eligible verdict, whatever statuses says.
  • project_id: Scopes last_analysed_image_date to one trial.
  • 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.

Raises

  • HTTPException: 400 if both trial and eligible_trials are given.