SDK

Exams

Encrypted exams and reports with the diagnos SDK: link an exam to a patient, write the report the web editor opens, version it.

On this page

English · Português (Brasil)

vault.exams works like vault.patients — versioned, sealed documents — with two differences that matter: every exam belongs to one patient, and its content is a clinical report the web editor opens.

Create#

from diagnos import Diagnos, ExamRecord

vault = Diagnos()
patient = vault.patients.create({"legal_name": "Jane Doe", "display_name": "Jane"}, security_group="sg_radiology")

exam = vault.exams.create(
    ExamRecord(title="Chest CT", modality="CT", exam_date="2026-09-01", report_html="<p>Unremarkable.</p>"),
    patient_id=patient.id,  # required, and the one field sent in clear
    security_group="sg_radiology",
)
print(exam.id, exam.patient_id, exam.version_id)

patient_id is required and travels in clear on purpose: it is how the vault links an exam to its patient — to route and authorize — without opening either. Everything else, the modality included, is sealed. Like any document, an exam belongs to exactly one security group.

The record#

Field Notes
title e.g. Chest CT
modality e.g. CT, MR, US — sealed, not metadata
exam_date a date, an aware datetime or an ISO string; stored and truncated like a patient's dates
report_lexical the web editor's state, as a JSON string — the source of truth for the report
report_html HTML derived from it, for readers that never open the editor
custom_attributes any JSON object

A dict works as well as an ExamRecord, with the same typo protection. patient_id or report inside the record are refused with a pointer to where they belong.

The report#

The web editor stores a report as Lexical state (report_lexical) and derives report_html from it. When you produce a report that people will open in the web app, write both: with only report_html, the editor opens an empty document. A reader that only displays reports can rely on report_html alone — the CLI does exactly that, printing it as plain text.

Whether a report is a draft or published (exam.report_status: draft, published, or None when never set) is clear metadata on the exam's index. The SDK reads it and never sets it.

Read#

exam = vault.exams.get(exam.id)  # the newest content; a newer web-editor draft wins
print(exam.record.title, exam.patient_id, exam.report_status, exam.from_draft)
print(exam.record.report_html)

committed = vault.exams.get(exam.id, include_draft=False)

Drafts, versions and the index behave exactly as for patients.

List#

for row in vault.exams.iter_all(security_group="sg_radiology"):
    summary = row.summary  # title, modality and date, decrypted locally
    print(row.id, row.index.meta.get("patient_id"), summary.title if summary else "—")

of_patient = [row for row in vault.exams.iter_all() if row.index.meta.get("patient_id") == patient.id]
print(len(of_patient), "exam(s) of", patient.id)

Lists filter by security_group and include_deleted. There is no server-side filter by patient; the patient id is in each row's clear meta, so filtering locally costs nothing but the walk.

Update#

current = vault.exams.get(exam.id)
signed_off = current.record.model_copy(update={"report_html": "<p>Unremarkable. No nodules.</p>"})
exam = vault.exams.update(exam.id, signed_off, expected_latest_version_id=current.index.latest_version_id)

Like patients: a complete new version every time, and expected_latest_version_id turns a concurrent save into a ConflictError instead of a silent overwrite — see Safe concurrent writes. Exams have no tags.

Archive, delete, restore#

vault.exams.archive(exam.id)  # a flag, no new version
vault.exams.unarchive(exam.id)
vault.exams.delete(exam.id)  # to the trash, never a hard delete
vault.exams.restore(exam.id)

Files of an exam#

Images, DICOM series and PDFs are not part of the record: they are files linked to the exam by exam_id, each under its own key.

drive = vault.drives.drive("sg_radiology")
drive.upload("scans/IM-0001.dcm", exam_id=exam.id)
print([drive.name_of(node) for node in drive.iter_all(exam_id=exam.id)])