models
Pydantic request/response models for the patient-data API.
These models are the single source of truth for the served OpenAPI document:
create_app publishes app.openapi() at /openapi.json and /openapi.yaml,
and dev-scripts/dump_patient_api_openapi.py writes the same document to
openapi.yaml beside this module. Anything a consumer needs to know about a
response shape therefore belongs in the annotations, the docstrings, or the
json_schema_extra examples here — not in a separate hand-written spec, which
would drift.
The evidence blob is where that matters most. EligibilityCriterion is
structurally uniform, but its entries fall into families that differ in which
fields are populated; each family is declared below as a narrowing subclass
(CRITERION_VARIANTS) so the families reach the OpenAPI document as named
schemas rather than living only in prose.
Classes
AppointmentCriterion
class AppointmentCriterion(**data: Any):Variant: appointment history / current patient.
Patient-grain, EHR-provenance MethodFilters reading the appointment and
encounter columns together, so value is always null.
-
Current patientrecords{"current_patient_months": {"lte": <months>}}, the recency window it enforces. The filter compares appointment dates against a cutoff derived from that window rather than comparing a month count, so read the pair as the configured window, not as a comparison reproducible fromvalue. -
Appointment historyrecords an empty operator map: it has two operands (years,min_per_year) under one config field, and the map holds one operand per operator token. Read both from the task config. -
Appointment history(appointment_history) — the history spansyearsyears and each of those years holds at leastmin_per_yearappointments. One criterion, not two. -
Current patient(current_patient_months) — an appointment within the last N months.
Both are unknown — never fail — when the ehr_query step ran without
fetch_appointments, or when the appointment records were only partly
readable.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
config_fields : dict[typing.Literal['appointment_history', 'current_patient_months'], dict[CriterionOperator, bool | int | float | str | list[typing.Any] | None]]
- static
family : Literal[<CriterionFamily.APPOINTMENT: 'appointment'>]
- static
grain : Literal[<CriterionGrain.PATIENT: 'patient'>]
- static
model_config
- static
output_field : Literal['Appointment history', 'Current patient']
- static
provenance : Literal[<CriterionProvenance.EHR: 'ehr'>, <CriterionProvenance.UNKNOWN: 'unknown'>]
- static
unit : None
- static
value : None
AuthenticatedUser
class AuthenticatedUser(**data: Any):Authenticated caller identity produced by the auth dependency.
Arguments
username: The verified username of the authenticated caller, matching the user the pod is logged in as.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
CodeListCriterion
class CodeListCriterion(**data: Any):Variant: an EHR code list (conditions / procedures).
A MethodFilter outcome whose value is its CodeMatchEvidence — the codes
that actually drove this patient's verdict — rather than a scalar. null when
the code column was absent, since nothing was inspected; an empty payload would
imply the codes were checked and found wanting.
matched_codes holds every code that qualified, ascending by code, each
tagged with the scope that qualified it. A code that matched but decided nothing
is absent: one ruled out by its recency window, and one whose inferred
laterality disagreed with the scan eye. pending_laterality_review and
pending_date_review appear only when matched_codes is empty — once something
qualified, the criterion is settled and a code awaiting review is moot.
The config field records the whole configured code list as its operand, under
the code_match operator: {"conditions_inclusion": {"code_match": ["H35.31", "H35.3*"]}}. Entries are literal codes and regex patterns, so the pair tells a
consumer what was checked.
code_match rather than in/nin deliberately. Those tokens promise the
verdict is reproducible by testing the recorded operand for membership, and it
is not: the real predicate matches literal-or-regex against two separately
configured lists (any-eye codes short-circuit; study-eye codes qualify only when
the scan laterality is known and the code's inferred laterality includes it),
with a third config input deciding the verdict when a study-eye code matches but
its own laterality is indeterminate. The operand says what was checked; the
token stops a consumer claiming how.
fail on an inclusion list means no qualifying code was found; on an
exclusion list it means an excluded code matched. unknown means the code
column was absent or unevaluable (e.g. the EHR step did not run), never that
the patient has no codes.
A study-eye variant (conditions_inclusion_study_eye,
conditions_exclusion_study_eye, procedures_exclusion_study_eye) folds into
this same criterion under its non-suffixed config key, so it is not separately
visible — and since an outcome carries one config field, the recorded operand is
the any-eye list alone; those study-eye codes are not keyed anywhere in the
evidence. Neither are eligible_on_*_codes_lat_unknown or
code_exclusion_time_windows, which modify this filter rather than producing
their own criterion.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
config_fields : dict[typing.Literal['conditions_inclusion', 'conditions_exclusion', 'procedures_inclusion', 'procedures_exclusion'], dict[CriterionOperator, bool | int | float | str | list[typing.Any] | None]]
- static
family : Literal[<CriterionFamily.CODE_LIST: 'code_list'>]
- static
grain : Literal[<CriterionGrain.PATIENT: 'patient'>]
- static
model_config
- static
output_field : Literal['Conditions inclusion', 'Conditions exclusion', 'Procedures inclusion', 'Procedures exclusion']
- static
provenance : Literal[<CriterionProvenance.EHR: 'ehr'>, <CriterionProvenance.SUPPLIED: 'supplied'>, <CriterionProvenance.UNKNOWN: 'unknown'>]
- static
unit : None
- static
value : CodeMatchEvidence | None
CompoundScanCriterion
class CompoundScanCriterion(**data: Any):Variant: a compound scan rule (drusen OR-signal, N-scan run).
A scan-grain MethodFilter whose verdict spans several columns, so it has no
single value.
The two members differ in whether their bound is recorded:
-
Drusen inclusionrecords{"drusen_threshold": {"gte": <threshold>}}— the filter tests each drusen probability against exactly that threshold. -
A
<Display Name> N-scancriterion records an empty operator map. Its bound cannot be stated as one comparison: the filter compares a scan count againstceil(typical_width_micrometers / slice_thickness), so the configured width is in different units from the quantity compared, and the threshold applied moves per row with the scan's slice thickness. Read the width from the task config; do not read the empty map as "unbounded". -
Drusen inclusion— passes when any of the hard/soft/confluent drusen probabilities is at or abovedrusen_threshold. The per-subtype thresholds are separateScanMetricCriterionentries on their own probability columns, not part of this one. -
<Display Name> N-scan— fails when the biomarker spans a long enough consecutive-B-scan run. One per configured N-scan width, overDiffuse Edema,Epiretinal Fibrosis,Intraretinal fluid (IRF),Subretinal Fluid (SRF),Subretinal Hyperreflective Material (SHRM)andDiabetic Macular Edema.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
family : Literal[<CriterionFamily.COMPOUND_SCAN: 'compound_scan'>]
- static
grain : Literal[<CriterionGrain.SCAN: 'scan'>]
- static
model_config
- static
provenance : Literal[<CriterionProvenance.SCAN: 'scan'>, <CriterionProvenance.UNKNOWN: 'unknown'>]
- static
unit : None
- static
value : None
EligibilityCounts
class EligibilityCounts(**data: Any):Per-trial patient counts by eligibility status.
The three buckets partition the trial's patient_eligibility rows, so they
sum to that trial's cohort size — not to the pod's total patient count, since
a patient with no rollup row for the trial is in no bucket.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
EligibilityCriterion
class EligibilityCriterion(**data: Any):One criterion's served evidence entry (mirrors stored CriterionEvidence).
The value type of evidence.criteria, and the schema every entry validates
against. Entries are structurally uniform but fall into families, each published
as a narrowing schema of this one and listed under this schema's x-variants;
x-family-variants maps a family tag to the schema documenting it.
Classify an entry by family. It is the wire tag naming which variant
applies, declared by the criterion's producer rather than inferred, so a
consumer does not have to pattern-match output_field spellings or guess from
whether value is None. It is not a strict discriminator: a criterion
produced by a hand-authored task-config ColumnFilter, and any row written
before the tag was recorded, carry family: null. Handle that case; for those
rows only, fall back to output_field and config_fields.
Arguments
status: The criterion state.nais in the vocabulary but is never written into a stored evidence map, so expect onlypass/fail/unknown.output_field: The criterion's logical field name, and its key inevidence.criteriatoo (the two always agree). Either a produced column name (total_ga_area,Age (yrs),cst_mean_um) or a human-readable filter name for a compound criterion (Conditions inclusion,Drusen inclusion,Subretinal Fluid (SRF) N-scan).value: The observed value, orNoneif missing/not applicable. A scalar for the column-backed families, and aCodeMatchEvidenceobject for a code-list criterion — the codes that drove this patient's verdict. One field for every shape of observed value; readfamilyto know which to expect. The configured code list is served as well, but as the operand underconfig_fields, not here.
None is four distinct situations, indistinguishable from the value alone — use family and status to tell them apart: a non-code-list MethodFilter criterion, an absent target column, a NaN/NaT cell, and a non-finite float mapped to null on write (NaN and inf are not valid JSON).
config_fields:{config_field: {operator: operand}}for the config input(s) that produced this criterion. Only bounds in force for the run appear. An empty operator map means "no comparison recorded", not "no comparison made"; exactly two criteria carry one — the N-scan runs andappointment_history, neither of which can state its bound as a single comparison (seeCompoundScanCriterionandAppointmentCriterion). The whole map is empty for a criterion with no mapped config field and for a legacy row written before the field existed.grain: The cardinality axis (scan/patient), orNonefor a legacy row written before provenance tagging.provenance: The source axis (scan/ehr), orNonefor a legacy row.unit: Thevalue's display unit (e.g."mm²","nL"), orNonewhen the criterion is unitless, its column carries no catalogued unit, or the row was written before units were recorded.family: The criterion's semantic family — the tag naming which variant below applies.Nonefor a criterion built from a hand-authored task-configColumnFilter, and for a row written before the tag existed.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Subclasses
Variables
- static
config_fields : dict[str, dict[CriterionOperator, bool | int | float | str | list[typing.Any] | None]]
- static
family : CriterionFamily | None
- static
grain : CriterionGrain | None
- static
model_config
- static
output_field : str
- static
provenance : CriterionProvenance | None
- static
status : CriterionState
- static
unit : str | None
- static
value : bool | int | float | str | list[typing.Any] | dict[str, typing.Any] | None
EligibilityEvidence
class EligibilityEvidence(**data: Any):Eligibility evidence for a patient on a specific trial.
Mirrors the patient_eligibility row: status and determined_by_scan are
the row's own columns, and evidence is a JSON object holding the
per-criterion calculated values under a criteria sub-map.
Arguments
bitfount_patient_id: The Bitfount patient ID.project_id: The trial (project) ID.status: Overall eligibility status for the patient on this trial.determined_by_scan: Thescan_idof the scan that decided the verdict, chosen deterministically (most recentstudy_date, thenscan_id), orNone— e.g. a patient with no qualifying scan, which is also why the evidence map can then hold patient-grain criteria only.evidence: Typed object of per-criterion calculated evidence underevidence.criteria, each entry anEligibilityCriterioncarrying aconfig_fieldsmap —{config_field: {operator: operand}}for the criteria-matching config input(s) that produced it (operatorsgt/gte/lt/lte/eq/ne, plusin/ninwhose operand is a list of candidate values; every typedMethodFiltercriterion carries{config_field: {}}) — which makes the evidence self-describing and is whatevidence_mappingis built from. Each entry also carriesgrain/provenance, which areNonefor a legacy row written before provenance tagging, andunit(the value's unit,Nonewhen unitless or written before units were recorded).evidence_mapping: Convenience map from each criteria-matching config input field (e.g.total_ga_area_lower_bound) to the criterion key withinevidence.criteriait relates to, so a caller can resolve an input field to its evidence entry without scanning. Rebuilt from each criterion'sconfig_fields. A range criterion's lower + upper bound both map to the one merged criterion. Only fields whose criterion is present in this evidence are included. Not injective in one case: every generic criterion shares thecolumn_criteriakey, so with several configured this names only one of them.ehr_retrieved_at: When this patient's EHR data underlying this trial's verdict was retrieved — the servedpatient_level_eligibilityrow's own stored retrieval time, not recomputed at read time. Under best-effort EHR serving, a run that could not reach the EHR serves each patient's last stored row rather than aborting, so a verdict can be computed from EHR data that is now stale; this is how old it is.Noneis several situations a consumer must not conflate: a row written before this field was recorded, a patient with no EHR row at all (e.g. a scan-only trial, or a patient the EHR leg never evaluated), and a patient whose EHR lookup was attempted and retrieved nothing (no EHR data, so no retrieval time to report) — all serveNone, and none of them means the others.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
bitfount_patient_id : str
- static
determined_by_scan : str | None
- static
ehr_retrieved_at : datetime.datetime | None
- static
evidence : EligibilityEvidenceBody
- static
evidence_mapping : dict[str, str]
- static
model_config
- static
project_id : str
- static
status : bitfount.cache.types._eligibility_shared.EligibilityStatus
EligibilityEvidenceBody
class EligibilityEvidenceBody(**data: Any):The evidence object: per-criterion entries under criteria.
criteria is keyed by output_field, and each entry repeats its own key in
output_field. Only criteria in force for the run appear, so the absence of a
key means "not configured", never "passed". The key set is open — a generic
column_criteria contributes an arbitrary column name.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
ErrorDetail
class ErrorDetail(**data: Any):FastAPI's HTTPException response body.
Arguments
detail: The failure reason. Never carries a filesystem path or a file basename, either of which can itself encode PHI.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
GenericColumnCriterion
class GenericColumnCriterion(**data: Any):Variant: a generic column_criteria scalar comparison.
The escape hatch — a config-declared comparison against any produced column
by name. output_field is that column name, so it is open-ended: do not
enumerate it client-side.
This is the only variant whose value can be a bool or a str, and the only
one whose axes are genuinely unconstrained: grain is resolved from the field
catalog (or the column's provenance, else defaults to scan), and provenance
from the column's source.
Every generic criterion shares the single column_criteria config key, with
two consequences worth coding against:
evidence_mapping["column_criteria"]can name only one criterion. With several generic criteria configured, the rest are reachable only byoutput_field.- Two generic criteria on one column merge into one entry, and if they also share an operator token the evidence keeps only the last threshold (the pod logs a warning).
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Subclasses
Variables
- static
config_fields : dict[typing.Literal['column_criteria'], dict[CriterionOperator, bool | int | float | str | list[typing.Any] | None]]
- static
family : Literal[<CriterionFamily.GENERIC_COLUMN: 'generic_column'>]
- static
model_config
GenericMethodCriterion
class GenericMethodCriterion(**data: Any):Variant: a generic column_criteria glob / collection-membership test.
A generic criterion that could not be a vectorised column comparison, so it
ran as a MethodFilter and carries value: null even though it targets a
single column. Three routes land here:
- a
strcolumn whose criterion value contains a*wildcard (per-row glob matching), - a
collection-dtype column (Previous Appointments Info,Previous Encounters Info), where membership is tested inside the cell rather than against it, and - an operator illegal for the column's catalogued dtype. That criterion is
deliberately not dropped — a dropped criterion would leave the row falsely
unconstrained — but degrades to
unknownfor every row.
Conditions and Procedures are collection-dtype too but are not a
route here: a generic membership criterion on an EHR code column is refused
outright and produces no criterion at all, because those cells hold code
objects rather than bare values. Use the typed
conditions_/procedures_ code lists, which produce a CodeListCriterion.
All three routes need the column's catalogued dtype, so a criterion on an uncatalogued column cannot reach the collection or illegal-operator routes — only the glob route, which is decided by the criterion value alone.
Unlike the other MethodFilter variants, config_fields.column_criteria does
carry an operator token and operand here, the generic criterion knowing its
own operator. For in/not in that operand is the whole candidate list.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
family : Literal[<CriterionFamily.GENERIC_METHOD: 'generic_method'>]
- static
model_config
- static
value : None
HealthStatus
class HealthStatus(**data: Any):Liveness payload for the unauthenticated /health endpoint.
Arguments
status: Always"ok"when the API process is serving requests.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
LegacyCriterion
class LegacyCriterion(**data: Any):Variant: a row written before provenance/unit tagging.
grain, provenance and unit are null, and config_fields is empty —
which also makes the row's evidence_mapping empty, since that is rebuilt
purely from config_fields. A client must not require any of these fields to
be populated.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
PatientList
class PatientList(**data: Any):Envelope for the patient list endpoint.
The endpoint is paginated server-side: items holds the requested page,
and total is the count of all patients matching the search/filter
before pagination — so a client can render "page X of Y".
Arguments
items: The patient summaries on the requested page.total: The number of matching patients before pagination.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
PatientMetricCriterion
class PatientMetricCriterion(**data: Any):Variant: a numeric comparison against a patient-grain column.
The patient-grain counterpart to ScanMetricCriterion: same ColumnFilter
shape and same config_fields structure, one entry per applied bound, but
evaluated once per patient rather than once per scan.
Today its only member is Age (yrs), which is why output_field pins that
one literal — the family is named for the kind of criterion rather than the
field, so a second patient-grain numeric criterion widens the literal instead
of needing a new family.
Age is also the one criterion that routinely crosses the two axes, which is
why provenance is left unpinned here: ehr when age came from an EHR
date_of_birth, scan when it was computed from a DICOM birth date. Always
unitless.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
family : Literal[<CriterionFamily.PATIENT_METRIC: 'patient_metric'>]
- static
grain : Literal[<CriterionGrain.PATIENT: 'patient'>]
- static
model_config
- static
output_field : Literal['Age (yrs)']
- static
unit : None
- static
value : int | None
PatientSummary
class PatientSummary(**data: Any):Summary view of a patient for list and detail endpoints.
Both endpoints serve this same shape.
Arguments
bitfount_patient_id: The Bitfount patient ID.ehr_patient_id: The EHR patient ID, if known.name: The patient's display name, as stored. Names are reduced to"Given Family"when the record is written, whether they came from the EHR or from a scan, so vendor-specific forms such as DICOM'sFAMILY^GIVEN^MIDDLEdo not reach this field. A name that could not be parsed is stored — and served — unchanged. Not normalised here.eligible_trials: All trial (project) IDs the patient is eligible for, ascending. Unaffected by the list endpoint'seligible_trialfilter.mrns: The patient's medical record number(s), if known.last_analysed_image_date: When the patient's most recently analysed image was processed, orNoneif no image has been analysed for them yet. An aware UTC timestamp — the maximumprocessed_atover the patient's scan rows, so it is a processing time, not a capture time.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
bitfount_patient_id : str
- static
ehr_patient_id : str | None
- static
eligible_trials : list[str]
- static
last_analysed_image_date : datetime.datetime | None
- static
model_config
- static
mrns : list[str]
- static
name : str
ScanImageFrame
class ScanImageFrame(**data: Any):One frame's entry in a scan-image archive's manifest.json.
Arguments
index: The frame's position in the archive, from 0.filename: The frame's archive member name (frame_NNN.webp).modality: The frame's modality in the SDK's vocabulary — the same two values a task filter or a datasource takes.nullwhen the source states none (a DICOM carrying no acquisition-device-type tag) or states something that is neither an OCT nor an SLO: a colour photo, an angiography series, an encapsulated PDF. Readseries_modalityto tell those apart, and switch on this field rather than that one.series_modality: The source's own, finer modality label — the lower-casedprivate_eyedescription, sooctbut alsoslo - red,colour photo,red-free (cross-polarized). Carries whatmodality's normalisation drops: which SLO channel a frame came from, and what anull-modality series actually is.nullon the DICOM path, whose tag is already in the SDK vocabulary and has no finer label behind it. Open-ended, and a vendored parser's spelling rather than this API's — do not switch on it.laterality: The series' detected laterality, ornullwhen unknown — which is every DICOM-path frame, since the requestedlateralityfilter is never echoed back as detected metadata.B(both eyes) andU(explicitly unknown) are possible alongsideL/R.source_width: The frame's width before scaling, in px.source_height: The frame's height before scaling, in px.width: The encoded width, in px — the requested width clamped tosource_width.height: The encoded height, in px.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
filename : str
- static
height : int
- static
index : int
- static
laterality : Optional[Literal['L', 'R', 'B', 'U']]
- static
modality : Literal['OCT', 'SLO', None]
- static
model_config
- static
series_modality : str | None
- static
source_height : int
- static
source_width : int
- static
width : int
ScanImageManifest
class ScanImageManifest(**data: Any):The manifest.json inside a GET /scans/image archive.
Documented for consumers of the archive; never served as a response body.
Arguments
format: The frame encoding — alwayswebp.quality: The WebP quality the frames were encoded at (0-100).acquisition_datetime: When the series was acquired, ornullwhen the file states nothing — a normal outcome, not an error, so a consumer must render the absence rather than assume a date. Read from the source's own metadata: on the DICOM path the first ofAcquisitionDateTime,AcquisitionDate(+AcquisitionTime),ContentDate(+ContentTime) orStudyDate(+StudyTime) that states a value; on the native-vendor path the exam'sscan_datetime, falling back to the earliest B-scan capture time. A date whose paired time is absent reads as midnight. Carries a UTC offset only where the source stated one — a DICOM date/time pair states no zone, and one is not invented.frames: One entry perframe_NNN.webpmember, in archive order.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
acquisition_datetime : datetime.datetime | None
- static
format : Literal['webp']
- static
frames : list[ScanImageFrame]
- static
model_config
- static
quality : int
ScanMetricCriterion
class ScanMetricCriterion(**data: Any):Variant: a numeric comparison against a produced scan-metric column.
A ColumnFilter outcome. grain is scan, value is numeric, and there is
one config_fields key per applied bound — a two-sided range merges into this
single entry.
output_field is one of:
- GA:
total_ga_area(mm²),largest_lesion_size(mm²),distance_from_fovea_centre(mm),max_cnv_probability(unitless). - Biomarker areas:
<label>_area(mm²), over the area-eligible segmentation labels. - Biomarker probabilities:
max_<label>_probability(unitless), over the segmentation labels plus the model classification pathologies (wet_amd,dry_amd,geographic_atrophy, ...). - CST/GCC:
cst_mean_um(µm),gcc_mean_superior,gcc_mean_inferior(unitless — the algorithm does not document a unit). - Fluid volumes (
nL):fluid_subretinal_fluid_volume,fluid_intraretinal_cystoid_fluid_volume,fluid_serous_rpe_detachment_volume,fluid_total_fluid_volume.
Bound directions, for reading config_fields: GA area and lesion size are
exclusive (gt/lt); CNV is lt; fovea distance and every CST/GCC/fluid
range are inclusive (gte/lte); the lte biomarker thresholds
(hypertransmission, neurosensory retina atrophy, hard exudates, wet AMD) are
exclusionary; the drusen thresholds are gte inclusion signals; and
exclude_foveal_ga records {"ne": 0.0} on distance_from_fovea_centre.
provenance is unknown when the run built the criterion without a
datasource context, or the target column was absent from the provenance map.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be
validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Ancestors
Variables
- static
family : Literal[<CriterionFamily.SCAN_METRIC: 'scan_metric'>]
- static
grain : Literal[<CriterionGrain.SCAN: 'scan'>]
- static
model_config
- static
provenance : Literal[<CriterionProvenance.SCAN: 'scan'>, <CriterionProvenance.UNKNOWN: 'unknown'>]
- static
value : int | float | None
ScanSegmentationIndex
class ScanSegmentationIndex(**data: Any):The segmentations.json inside a GET /scans/image archive.
Present when either include_segmentation_masks or
include_segmentation_vectors is set; absent from the archive entirely when
neither is. Documented for consumers of the archive; never served as a
response body, and — like the frames and the manifest beside it — served
compactly, since it carries one entry per class per frame.
Arguments
frames: One entry per served frame, in frame order.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
SegmentationClass
class SegmentationClass(**data: Any):One segmentation class on one frame, from one model.
Carries mask, instances, or both — never neither, since a class present in
neither requested output is omitted from classes entirely. Each key is
absent, not null, when its layer was not requested.
Arguments
name: The normalised class name (every non-word character replaced with an underscore, then lower-cased), and the stable key to match state and filters on. It is also whatsegmentation_classesfilters against.label: The human-readable name to render, falling back tonamefor a class with no registered label.colour: Advisory display RGB, as[r, g, b], falling back to[200, 200, 200]. It matches the PDF report's legend; a UI may override it.probability: The highest instance probability for this class on this frame, ornullwhen the model reported none.mask: Archive-relative path of this class's mask PNG. Absent when masks were not requested, and also when downscaling to the served width erased every pixel of the raster — which is what can leave a thinpolylineclass with vectors and no mask.instances: The class's raw drawable instances, un-collapsed, so per-instance identity and probability survive (a mask unions them into one plane). Absent when vectors were not requested.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
colour : tuple[int, int, int]
- static
instances : list[SegmentationInstance] | None
- static
label : str
- static
mask : str | None
- static
model_config
- static
name : str
- static
probability : float | None
SegmentationFrame
class SegmentationFrame(**data: Any):All segmentation output for one served frame.
Arguments
index: The frame's index, matching itsframe_NNN.webpmember.filename: That member's name, so a consumer need not rebuild it fromindex.width: The served frame's width in px. A mask PNG is this wide, so it composites onto the frame with no client-side scaling.height: The served frame's height in px.source_width: The frame's native width in px, before scaling.source_height: The frame's native height in px.sources: One entry per model that produced output for this frame. Empty (not absent) for a frame nothing produced output for.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
filename : str
- static
height : int
- static
index : int
- static
model_config
- static
source_height : int
- static
source_width : int
- static
sources : list[SegmentationSource]
- static
width : int
SegmentationInstance
class SegmentationInstance(**data: Any):One drawable instance inside a segmentations.json class entry.
Coordinates are in the model's own pixel space (mask_width/mask_height on
the parent source), not the served frame's — see scale_x/scale_y.
Only the geometry belonging to type is present: a key that does not apply
is omitted rather than served as null. probability is the exception —
it is always present, and is null when the model reported none.
Arguments
type: The instance's geometry. Apolygonand anellipsebound a region whose interior carries the class; apolylineis an open curve with no interior, used for retinal-layer boundaries, and a rasterised one is given a 3px stroke in model space and nothing wider. Instance types carrying no area (point, and anything unrecognised) are dropped before the index is built, so none reaches a consumer.probability: The model's confidence for this instance, ornullwhen it reported none. Per instance, not per class: the parent class entry carries its own.points: Flat[x0, y0, x1, y1, …], onpolygonandpolylineonly.cx: Ellipse centre x. Present onellipseonly, as are the four below.cy: Ellipse centre y.rx: Ellipse semi-axis x.ry: Ellipse semi-axis y.angle: Ellipse rotation, in degrees.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
angle : int | None
- static
cx : int | None
- static
cy : int | None
- static
model_config
- static
points : list[int] | None
- static
probability : float | None
- static
rx : int | None
- static
ry : int | None
- static
type : Literal['polygon', 'polyline', 'ellipse']
SegmentationSource
class SegmentationSource(**data: Any):Every class one model produced for one frame.
Two models can produce output for the same frame, and both can carry the same
class name, so a legend is grouped by source rather than by class name alone.
Arguments
model_ref: The model that produced this output.model_version: That model's version.kind: Which pipeline recognised the output's shape.mask_width: Width of the model's own coordinate space, which everyinstancescoordinate is in.mask_height: Height of that space.dims_match_source: Whether that space matches the frame's native dims.falsemeans the model inferred at some other size, which a client may want to treat with suspicion.scale_x:width / mask_width, full precision — the factor mapping a vector's x onto the served frame. An SVG overlay needs no arithmetic at all:viewBox="0 0 {mask_width} {mask_height}"absorbs it, which is why the server never pre-scales a vector (scalingrx/ryindependently would deform a rotated ellipse).scale_y:height / mask_height, the same for y.classes: One entry per class this model produced on this frame.
Create a new model by parsing and validating input data from keyword arguments.
Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.
self is explicitly positional-only to allow self as a field name.
Variables
- static
classes : list[SegmentationClass]
- static
dims_match_source : bool
- static
kind : Literal['pathology', 'retinal_layers']
- static
mask_height : int
- static
mask_width : int
- static
model_config
- static
model_ref : str
- static
model_version : int
- static
scale_x : float
- static
scale_y : float