Study metadata in bulk¶
Set study metadata (age, sex, BMI, subject key, session index) and lights markers across many studies in one call — the SDK counterpart of the platform's CSV import. Both endpoints return per-item results (207 semantics): the batch never fails as a whole because one row is wrong.
from yousleep_common.models import (
BatchPatchStudiesMetadataRequest,
BatchSetStudyLightsRequest,
StudyLightsAssignment,
StudyMetadataPatch,
)
response = await client.batch.patch_studies_metadata(
project.id,
BatchPatchStudiesMetadataRequest(
items=[
StudyMetadataPatch(study="night-01", age=42, sex="female"),
StudyMetadataPatch(study="night-02", age=None), # explicit clear
]
),
)
for result in response.results:
if result.status == "error":
print(result.study, result.error.code, result.error.message)
Field presence is the PATCH semantics¶
An absent field leaves the study unchanged; an explicit null
clears it. The SDK's wrappers serialize with exclude_unset, so
constructing StudyMetadataPatch(study="s", age=None) means clear age
while StudyMetadataPatch(study="s") means touch nothing. If you build
request JSON yourself, you must preserve that distinction — serializing
default Nones as null would clear every field you did not mention.
Addressing rows¶
Rows address studies by exact name or id within the project. A name that several studies of the project share comes back as a per-row conflict; address those studies by id.
Subject key and session index¶
A study belongs to a person. Rows in one project sharing a subject_key
form that subject's series — for per-subject trends, paired designs and
erasure — and the key is compared case- and whitespace-insensitively, so
P01, p01 and P 01 are one subject. Nothing is inferred: a study has no
subject until a key is set, and a study with none stands alone.
session_index declares the study's position in its series (1 = first) and
is unique within the subject. Leave it blank and the position is derived from
the order of the subject's recording starts; a night whose recording start is
unknown cannot be placed that way and needs a declared index. A subject whose
studies are only partly numbered is left out of session-indexed figures and
named in a warning, so number all of a subject's nights or none of them.
Both fields are also accepted by client.batch.create_studies, so a night
can be filed under its subject when it is created rather than patched in
afterwards.
Reading them back, one subject at a time or a whole project in subject order:
nights = await client.studies.list(project_id, subject_key="P01")
by_subject = await client.studies.list(project_id, order_by="subject")
subject_key matches the way grouping does, so "p 01" finds the same
studies as "P01". order_by="subject" puts each person's studies
together and in series order — declared visit numbers first, unnumbered
nights after them, and studies with no subject last — which is what a
paginated listing needs to be read person by person.
await client.batch.patch_studies_metadata(
project_id,
BatchPatchStudiesMetadataRequest(
items=[
{"study": "night-1", "subject_key": "P01", "session_index": 1},
{"study": "night-2", "subject_key": "P01", "session_index": 2},
{"study": "night-3", "subject_key": "P01", "session_index": 3},
]
),
)
Both fields follow the PATCH rule above: omit to leave alone, null to
clear. Clearing a key detaches the study; nothing else changes.
A subject column is the subject key
A header spelled subject maps to subject_key, not to the study
key. To import a sheet that holds study names under subject, map that
column to the study in the wizard's mapping step. A sheet with no study
column fails to resolve on every row and writes nothing.
Lights markers¶
Each marker is given in exactly one spelling:
await client.batch.set_study_lights(
project.id,
BatchSetStudyLightsRequest(
items=[
# Wall-clock time at the recording site, resolved per study with
# overnight wrap. Only valid while the study's recordings span
# <= 24 h — longer spans error per row toward the other spellings.
StudyLightsAssignment(study="night-01", lights_off={"clock": "23:15"}),
# Absolute datetime, read in the recording's own wall-clock frame
# (a timezone suffix is ignored, not converted).
StudyLightsAssignment(
study="night-02", lights_on={"datetime": "2026-03-02T07:00"}
),
# Milliseconds from the study's earliest recording start. Legal
# even before any recording exists (upload-later workflows).
StudyLightsAssignment(study="night-03", lights_off={"elapsed_ms": 600000}),
]
),
)
Existing markers win by default: a row targeting a marker that is already
set — including an explicit null delete — errors per item unless
replace_existing=True is passed. This protects manual edits made in the
viewer from being silently overwritten by a re-imported sheet.
The column vocabulary¶
Clients building a mapping UI (CSV headers → fields) should fetch the recognised columns from the server rather than hardcoding them:
Each entry carries the canonical key, accepted header aliases, and unit
notes. The same vocabulary ships in this package as
yousleep_common.models.STUDY_IMPORT_COLUMNS for offline use.