REST API adapters

Construction of authenticated HTTP clients for the ACMAD API.

acmad_uploader.adapters.api.auth.create_authenticated_client(*, api_url, realm_url, client_id, persist_tokens=True, open_browser=True, verify_tls=True, timeout=30.0, login_url_callback=None)

Create an HTTP client authenticated by OIDC device flow.

Credentials are entered at the identity provider. Only OIDC tokens may be persisted, using the operating system keyring when requested.

Return type:

Client

Shared low-level HTTP/JSON plumbing for ACMAD REST API adapters.

The patient/diagnosis/gait-assessment adapters predate this and deliberately keep their own copies of this plumbing (see CLAUDE.md) to avoid touching already-tested code for DRY’s sake alone. New adapters should use this shared client instead of duplicating it again.

class acmad_uploader.adapters.api.client.JsonApiClient(client, error_type)

Bases: object

Translate HTTP/JSON mechanics into a caller-supplied error type.

request(method, url, **kwargs)

Send a request and translate transport/API errors to the port error.

Return type:

Response

json(response)

Decode JSON or raise the destination error.

Return type:

object

object(response, description)

Decode an API object response.

Return type:

Mapping[str, Any]

list_items(endpoint, *, params, description)

Return every item from the first page of a filtered list response.

Return type:

list[Mapping[str, Any]]

single_or_none(endpoint, *, params, description)

Return zero or one filtered object, rejecting ambiguous results.

Return type:

Mapping[str, Any] | None

single(endpoint, *, params, description)

Return exactly one filtered API object.

Return type:

Mapping[str, Any]

required_url(item, description)

Read a hyperlinked serializer URL.

Return type:

str

error(message)

Build an instance of the destination error type for a custom failure.

For callers that need custom client-side matching beyond what single/single_or_none support (e.g. filtering results by a field the API doesn’t expose as a server-side filter) and still want to raise the same caller-supplied error type on a bad match.

Return type:

Exception

static pk_from_url(url)

Return the primary key segment of a hyperlinked serializer URL.

Auto-generated filterset fields for foreign keys expect the related object’s primary key, not its hyperlinked URL.

Return type:

str

Shared payload building for PatientEvent metadata across event adapters.

acmad_uploader.adapters.api.patient_event.patient_event_details_payload(details)

Build the writable PatientEvent fields common to every event type.

Return type:

dict[str, Any]

Shared read-only resolution of an existing gait assessment’s URL.

Every slice hanging off GaitAssessment (FunctionAssessment, MocapData, PhysicalExamination, …) needs to resolve the same patient -> patient event -> gait assessment chain by natural key. Only AcmadGaitAssessmentRepository itself is responsible for creating that chain; this helper only looks it up, raising the caller’s error type if any link is missing (which means the gait assessment upload hasn’t run yet).

class acmad_uploader.adapters.api.gait_assessment_lookup.GaitAssessmentLookup(api)

Bases: object

Resolve a gait assessment’s URL from (site_patient_id, age_in_days).

resolve(*, site_patient_id, age_in_days)

Return the gait assessment URL, raising if any link is missing.

Return type:

str

Shared read-only resolution of an existing mocap session’s URL.

Any slice hanging off MocapData (currently only BiomechanicsArtifact) needs to resolve the same patient -> patient event -> gait assessment -> mocap session chain by natural key. Only AcmadMocapDataRepository itself is responsible for creating that chain; this helper only looks it up, raising the caller’s error type if any link is missing (which means the mocap session upload hasn’t run yet).

AcmadMocapDataRepository._find_existing already implements this same client-side matching, deliberately kept separate/duplicated here rather than refactored to share code, to avoid touching already-tested code for DRY’s sake alone (see CLAUDE.md).

class acmad_uploader.adapters.api.mocap_data_lookup.MocapDataLookup(api)

Bases: object

Resolve a mocap session’s URL from its full natural key.

MocapData has no database-level natural key at all (mocapdata/ only filters on gait_assessment/session_condition/ gait_aid server-side) — the left/right condition-detail pair is matched client-side over that filtered list.

resolve(*, site_patient_id, age_in_days, session_condition, gait_aid, left_condition_detail, right_condition_detail)

Return the mocap session URL, raising if any link is missing.

Return type:

str

Patient repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.patient.AcmadPatientRepository(client)

Bases: object

Idempotently create or update basic patient records over HTTP.

upsert(patient)

Upsert a patient located by its globally unique site identifier.

Return type:

PatientUpsertResult

Diagnosis repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.diagnosis.AcmadDiagnosisRepository(client)

Bases: object

Idempotently create or update diagnosis records over HTTP.

definition and latest_primary_diagnosis are never sent: the API resolves the matching diagnosis reference record from the submitted group/subgroup/detail itself, and recalculates which primary diagnosis is “latest” from the patient’s full diagnosis history after every write.

site is always resolved from the workbook’s own site_id rather than inferred from the patient: a patient merged across clinics via the de-identified hash can be associated with more than one site, but a diagnosis is owned by the specific clinic that produced it.

upsert(diagnosis)

Upsert a diagnosis located by patient, type and age at diagnosis.

Return type:

DiagnosisUpsertResult

Gait assessment repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.gait_assessment.AcmadGaitAssessmentRepository(client)

Bases: object

Resolve or create a patient event and its gait assessment over HTTP.

patientevent/ filters on patient, event_type and age_in_days; gaitassessment/ filters on patient_event (confirmed against acmadapi.views.patientevent/gaitassessment), so both lookups are single filtered GETs rather than a listing narrowed client-side.

A repeat run with an unchanged assessment performs no write once the event and gait assessment already exist: both are only looked up and reused, since neither resource has writable fields beyond its linkage.

site is resolved from the workbook’s own site_id rather than inferred from the patient: a patient merged across clinics via the de-identified hash can be associated with more than one site, so the event needs its own explicit site, not a guess at the patient’s site.

upsert(assessment)

Resolve the patient, event and gait assessment, creating as needed.

Return type:

GaitAssessmentUpsertResult

Function assessment repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.function_assessment.AcmadFunctionAssessmentRepository(client)

Bases: object

Idempotently create or update function assessment records over HTTP.

Matched by its owning gait_assessment, resolved from (site_patient_id, age_in_days) via the shared GaitAssessmentLookup — the gait assessment itself must already have been uploaded.

upsert(assessment)

Upsert a function assessment located by its owning gait assessment.

Return type:

FunctionAssessmentUpsertResult

Mocap session repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.mocap_data.AcmadMocapDataRepository(client)

Bases: object

Idempotently create or update mocap session records over HTTP.

mocapdata/ only filters on gait_assessment/session_condition/ gait_aid server-side; MocapData has no database-level natural key at all, so the left/right condition-detail match is done client-side over that filtered list (confirmed with the user that the full combination is unique per day/assessment in practice).

upsert(session)

Upsert a mocap session located by its condition-detail combination.

Return type:

MocapDataUpsertResult

Biomechanics artifact repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.biomechanics_artifact.AcmadBiomechanicsArtifactRepository(client)

Bases: object

Idempotently upload a biomechanics artifact file and its metadata.

Matched by (mocap_data, original_path) — the destination’s own natural key (a real unique_together), so no client-side dedup is needed here, unlike MocapData/SurgicalProcedure` before their own natural-key fixes. An unchanged file (same sha256 already stored) is skipped entirely rather than re-uploaded.

The three “derived” CSV roles (normalised_kinematics_csv/ normalised_kinetics_csv/spatiotemporal_csv) require an OpenSimProcessingRun — resolved (or created once) per mocap session and reused for all three, since the destination forbids changing processing_run on an existing derived artifact (BiomechanicsArtifactSerializer.validate). Every other role uploads with no processing run at all.

upsert(artifact)

Resolve the mocap session, then upload or skip the artifact file.

Return type:

BiomechanicsArtifactUpsertResult

Physical examination repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.physical_examination.AcmadPhysicalExaminationRepository(client)

Bases: object

Idempotently create or update a physical examination and its detail rows.

Resolves the owning GaitAssessment via the shared GaitAssessmentLookup, then upserts PhysicalExamination (matched by gait_assessment), followed by Anthropometrics (1:1, matched by physical_examination), followed by the five side-keyed detail models (AnthropometricSide/RangeOfMotion/Strength/Tone/ FootStructure, each matched by (physical_examination, side)) — the same “parent then children” idiom as Surgery/ SurgicalProcedure, just with a fixed left+right pair per detail model instead of a variable-length collection.

upsert(examination)

Resolve the gait assessment, then upsert the examination and its details.

Return type:

PhysicalExaminationUpsertResult

PROMs family repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.proms.AcmadPromsRepository(client)

Bases: object

Idempotently create or update a PROMs parent row and its questionnaires.

Resolves the owning GaitAssessment via the shared GaitAssessmentLookup, then upserts PROMs (matched by gait_assessment), followed by FAQ (1:1, matched by proms) and each dual-respondent questionnaire (matched by (proms, completed_by)) — the same “parent then children” idiom as Surgery/PhysicalExamination.

upsert(proms)

Resolve the gait assessment, then upsert PROMs and its questionnaires.

Return type:

PromsUpsertResult

Surgery/intervention episode repository backed by the ACMAD REST API.

class acmad_uploader.adapters.api.surgery.AcmadSurgeryRepository(client)

Bases: object

Idempotently create or update a surgery episode and its procedures.

Resolves patient -> patient event -> surgery directly (not via the shared GaitAssessmentLookup, since this is a surgery-type event, not an assessment-type one). hospital is resolved best-effort: unlike site (the uploading clinic, always a participating institution), the physical hospital where surgery was performed may not correspond to any institution in the system at all (e.g. “other overseas site”), so a lookup miss just leaves the field unset rather than erroring.

Each SurgicalProcedure is matched by (surgery, procedure_index), the destination’s own natural key for it — procedure_index is the numbered workbook slot the procedure was read from.

upsert(surgery)

Resolve the patient, event and surgery, then upsert its procedures.

Return type:

SurgeryUpsertResult