SDK
Patients
Create, read, update, archive and delete encrypted patient records with the diagnos SDK, including drafts, tags and safe concurrent writes.
English · Português (Brasil)
vault.patients creates, reads, lists and changes patient records. Everything clinical is sealed in your process
before it leaves; what the vault stores is a versioned, encrypted document it can route but never read. The model
behind it — versions, drafts, the sealed summary — is in Concepts.
Create#
from datetime import date
from diagnos import Diagnos
vault = Diagnos()
patient = vault.patients.create(
{"legal_name": "Jane Doe", "display_name": "Jane", "birth_date": date(1990, 1, 31)},
security_group="sg_oncology", # exactly one group per document
tags=["diabetes"], # list and search labels, sealed inside the summary
specialist_ids=["specialist_123"], # clear metadata the vault filters by
)
print(patient.id, patient.version_id, patient.tags)
The record can be a dict or a PatientRecord; either way it is validated before anything is encrypted. tags
and specialist_ids are arguments, not record fields, because they live in different places: tags are sealed with
the summary, specialist ids stay in clear metadata so the vault can filter by them.
The record#
PatientRecord mirrors the web app's patient data field for field, so what you write is what the web app reads:
| Field | Notes |
|---|---|
legal_name, display_name |
required |
external_id |
your system's id for this patient — plain text, sealed with the rest |
birth_date |
a date, an aware datetime or an ISO string — see Dates |
biological_sex |
MALE, FEMALE, INTERSEX, UNDEFINED |
gender_identity |
CIS_MALE, CIS_FEMALE, TRANS_MALE, TRANS_FEMALE, NON_BINARY, AGENDER, FLUID, OTHER, PREFER_NOT_TO_SAY |
race_identity |
WHITE, BLACK, BROWN, YELLOW, INDIGENOUS, NOT_DECLARED |
email, phone |
free text |
address |
postal_code, street, number, complement, district, city, state, country — all optional |
internal_notes |
a list of strings |
custom_attributes |
any JSON object |
identifiers |
identity documents, sealed by the vault — see Identity documents |
Every field is sealed. Types and defaults are in the SDK reference.
Typos are refused, unknown fields are kept#
The vault never sees plaintext, so nothing downstream would ever notice a misspelled field — it would be stored under a key nobody reads. The record is the last check, and it handles two opposite failures:
- a key that looks like a typo of a real field (
birthdate) is refused, naming the field it resembles; so is a key that belongs elsewhere (tagsinside the record); - any other unknown key is kept as it came, so a field the web app added after this SDK was released survives a read-modify-write instead of being silently deleted.
import pydantic
try:
vault.patients.create(
{"legal_name": "Jane Doe", "display_name": "Jane", "birthdate": "1990-01-31"}, security_group="sg_oncology"
)
except pydantic.ValidationError as error: # raised before anything is encrypted or sent
print(error.errors()[0]["msg"])
Note
A bad record raises pydantic.ValidationError (a ValueError) locally. diagnos.ValidationError is different:
it is the vault refusing a request — see Errors.
Dates and time precision#
birth_date accepts a date, an aware datetime or an ISO 8601 string, and is stored the way the web app stores
it: a UTC instant, YYYY-MM-DDTHH:MM:SS.sssZ. A date alone means midnight UTC; a time without a UTC offset is
refused, because it would mean a different instant on every machine.
A workspace can also fix an anonymization precision — month, day, hour, minute or second — that the
web app truncates every date to before encrypting. The external API does not expose that setting, so set
DIAGNOS_TIME_PRECISION to your workspace's value and the SDK applies the same truncation on every write:
from datetime import date
from diagnos import to_iso_instant, truncate_timestamp
print(to_iso_instant(date(1990, 1, 31))) # 1990-01-31T00:00:00.000Z
print(truncate_timestamp("1990-01-31T15:42:10Z", "month")) # 1990-01-01T00:00:00.000Z
Identity documents#
identifiers (CPF, passport…) are sealed by the vault's sensitive-data route, and opening one is audited — the
external API cannot seal a new one. The SDK carries existing values through a read-modify-write untouched and
refuses a plain-text value. For an id from another system, use external_id.
Read#
same = vault.patients.get(patient.id) # the newest content: a newer web-editor draft wins
print(same.record.legal_name, same.from_draft, same.version_id)
committed = vault.patients.get(patient.id, include_draft=False) # committed versions only
pinned = vault.patients.get(patient.id, version_id=patient.version_id) # one exact version
get() downloads and decrypts one version (or the draft). A Patient carries the decrypted record, its summary
and tags, where the content came from (version_id, or draft_rev when from_draft), and the index the vault
keeps — security_group_id, versions, updated_at and the flags.
List#
for row in vault.patients.iter_all(security_group="sg_oncology"):
summary = row.summary # decrypted locally from the sealed summary: no version downloaded
print(row.id, summary.display_name if summary else "—", summary.tags if summary else [])
page = vault.patients.list(limit=20) # one page; follow page.next_cursor for the next
Rows are PatientListItems: the index plus the decrypted summary. summary is None for a patient whose group
this session does not hold, instead of failing the page. include_deleted=True adds patients in the trash. Paging
is described in Concepts.
Update#
An update writes a complete new version — there are no partial updates. Read, change, write:
current = vault.patients.get(patient.id)
renamed = current.record.model_copy(update={"display_name": "Jane R."})
patient = vault.patients.update(
patient.id,
renamed,
expected_latest_version_id=current.index.latest_version_id, # refuse if someone saved meanwhile
)
model_copy keeps every other field — including ones this SDK does not model. tags=None (the default) keeps the
current tags; pass a list to replace them. specialist_ids works the same way.
Safe concurrent writes#
Without expected_latest_version_id, the last writer wins silently. With it, the vault refuses a write when another
version was committed since your read, and you decide what to do:
from diagnos import ConflictError
stale = patient.index.latest_version_id
vault.patients.update(patient.id, patient.record, tags=["diabetes", "follow-up"]) # someone else saves
try:
vault.patients.update(patient.id, renamed, expected_latest_version_id=stale)
except ConflictError as error:
print(error.code) # DocumentVersionMismatch: read again, merge, write again
Archive, delete, restore#
vault.patients.archive(patient.id) # a flag, no new version
vault.patients.unarchive(patient.id)
vault.patients.delete(patient.id) # to the trash; the encrypted history stays
vault.patients.restore(patient.id)
Each returns the updated DocumentIndex. Delete is never a hard delete: the patient disappears from lists until
restore, or until listed with include_deleted=True.
What is not here yet#
- The patient's
filestream — the web editor's rich document — is not exposed; the SDK reads and writes the structured record. - Drafts are read, never written.
Both, and the rest of the known limits, are tracked in COMPATIBILITY.md.