exlab_wizard.api.schemas#

msgspec.Struct types for the ExLab-Wizard cache files.

This module is the single source of truth for the on-disk JSON schemas documented in design spec §11.3 (creation.json), §11.4 (readme_fields.json), §11.4.1 (equipment.json), §11.4.2 (test_runs.json), and §13.4 (ingest.json). Every cache reader and writer in the codebase round-trips its bytes through these Struct types via msgspec.json.decode(blob, type=...) / msgspec.json.encode(obj); schema validation happens during decode in one pass.

Design choices that affect every Struct in this module:

  • frozen=False – a few fields on CreationJson (sync_status, validation_overrides) are mutated in place by CreationWriter after the initial write (Backend Spec §4.4.5; §11.3 “mutated in place” note).

  • omit_defaults=True – field values that equal their declared default are omitted from the encoded JSON. Keeps writes compact and matches the on-disk shape shown in spec §11.3 (no nullable-but-null fields, no empty arrays for unused sub-blocks).

  • forbid_unknown_fields=False – unknown fields are silently preserved on round-trip via the writer’s raw_extras mechanism in cache/creation_writer.py. This is required by §11.9.3 writer policy rule 2: a v0.7 writer mutating a file written by a v0.8 writer MUST NOT drop the v0.8 fields it doesn’t recognize.

The validation_overrides field is typed as list[dict[str, Any]] rather than list[OverrideEntry | TombstoneEntry] because msgspec struct unions require an explicit tag (tag or tag_field) and the discriminator the spec mandates – the boolean revoked flag – cannot be used as a tag (msgspec tags must be strings or ints). Helpers override_entry_to_dict, tombstone_entry_to_dict, and parse_validation_override_entry bridge the wire form (dict) and the typed form (Struct) for callers that want either representation.

Functions

override_entry_to_dict(entry)

Serialize an OverrideEntry to a wire-form dict.

parse_validation_override_entry(entry)

Inspect entry["revoked"] and entry["revokes"] to decide which Struct shape to convert into.

tombstone_entry_to_dict(entry)

Serialize a TombstoneEntry to a wire-form dict.

Classes

CreationJson(schema_version, created_at, ...)

Top-level creation.json payload at schema version 1.8.

EquipmentJson(schema_version, id, label, ...)

equipment.json at schema version 1.0.

IngestJson(schema_version, project_name, ...)

ingest.json orchestrator staging record at schema version 1.1.

LimsProjectBlock(uid, short_id, name_at_creation)

LIMS-side project identity captured at creation time.

OrchestratorBlock(enabled, host, label)

Orchestrator-mode metadata.

OverrideEntry(id, problem_class, operator, ...)

Operator-recorded validation override entry.

PathsBlock(local, nas)

Resolved on-disk paths captured at creation time.

PluginApplied(plugin, version, ...[, isolation])

Per-plugin invocation record written into plugins_applied.

PluginIsolation(duration_ms, exit_code, ...)

Plugin worker isolation telemetry.

ReadmeFieldsJson(schema_version, ...)

readme_fields.json at schema version 1.1.

TemplateBlock(name, version, source_path[, ...])

Template provenance captured at creation time.

TestRunsJson(schema_version, created_at, ...)

test_runs.json marker at schema version 1.0.

TombstoneEntry(id, revokes, operator, ...[, ...])

Tombstone entry that revokes a prior override by id.

class exlab_wizard.api.schemas.CreationJson(schema_version: str, created_at: str, created_by: str, level: ~exlab_wizard.constants.enums.CreationLevel, run_kind: ~exlab_wizard.constants.enums.RunKind, lims_project: ~exlab_wizard.api.schemas.LimsProjectBlock, template: ~exlab_wizard.api.schemas.TemplateBlock, variables: dict[str, ~typing.Any], paths: ~exlab_wizard.api.schemas.PathsBlock, plugins_applied: list[~exlab_wizard.api.schemas.PluginApplied] = <factory>, orchestrator: ~exlab_wizard.api.schemas.OrchestratorBlock | None = None, sync_status: ~exlab_wizard.constants.enums.SyncStatus = SyncStatus.PENDING, validation_overrides: list[dict[str, ~typing.Any]] = <factory>)[source]#

Bases: Struct

Top-level creation.json payload at schema version 1.8.

Reading: msgspec.json.decode(blob, type=CreationJson) validates every required field and rejects type errors in one pass. Unknown fields are silently ignored at the Struct boundary; the writer re-serializes them via cache/creation_writer.py’s extras pass so forward-compat is preserved.

Writing: every write goes through CacheWriter; direct msgspec.json.encode(payload) calls are reserved for tests and for the writer’s tempfile pass.

created_at: str#
created_by: str#
level: CreationLevel#
lims_project: LimsProjectBlock#
orchestrator: OrchestratorBlock | None#
paths: PathsBlock#
plugins_applied: list[PluginApplied]#
run_kind: RunKind#
schema_version: str#
sync_status: SyncStatus#
template: TemplateBlock#
validation_overrides: list[dict[str, Any]]#
variables: dict[str, Any]#
class exlab_wizard.api.schemas.EquipmentJson(schema_version: str, id: str, label: str, configured_local_root: str, configured_nas_root: str, first_seen_at: str, last_modified_at: str)[source]#

Bases: Struct

equipment.json at schema version 1.0. Spec §11.4.1.

configured_local_root: str#
configured_nas_root: str#
first_seen_at: str#
id: str#
label: str#
last_modified_at: str#
schema_version: str#
class exlab_wizard.api.schemas.IngestJson(schema_version: str, project_name: str, equipment_id: str, run_kind: RunKind, run_path: str, transport: OrchestratorTransportType, current_state: IngestState, history: list[dict[str, ~typing.Any]]=<factory>)[source]#

Bases: Struct

ingest.json orchestrator staging record at schema version 1.1. Spec §13.4.

Written by the orchestrator only (not by equipment workstations). The history list is append-only per §13: lifecycle transitions are recorded, never overwritten. current_state mirrors the most recent history entry’s state for fast read-without-walk access.

History entries are loose dicts because the optional fields per state (files_received / bytes_received on complete; nas_path / checksum_file on sync_verified) make a strict type a nuisance. The state-machine validation is performed by the writer.

current_state: IngestState#
equipment_id: str#
history: list[dict[str, Any]]#
project_name: str#
run_kind: RunKind#
run_path: str#
schema_version: str#
transport: OrchestratorTransportType#
class exlab_wizard.api.schemas.LimsProjectBlock(uid: str, short_id: str, name_at_creation: str, source: LIMSProjectSource = LIMSProjectSource.LIVE, cache_freshness_at_use: str | None = None)[source]#

Bases: Struct

LIMS-side project identity captured at creation time. Spec §11.3.

Required on project- and run-level creation.json files at schema version >= 1.5. source and cache_freshness_at_use were added in 1.8; on a 1.7 file they are absent and read as "live" / None per the migration policy in §11.9.2.

cache_freshness_at_use: str | None#
name_at_creation: str#
short_id: str#
source: LIMSProjectSource#
uid: str#
class exlab_wizard.api.schemas.OrchestratorBlock(enabled: bool, host: str, label: str)[source]#

Bases: Struct

Orchestrator-mode metadata. Spec §11.3 / §13.

Absent (not null) at the parent-struct level when the wizard is running in single-equipment mode – CreationJson.orchestrator is None in that case and omit_defaults=True keeps the field out of the encoded JSON entirely.

enabled: bool#
host: str#
label: str#
class exlab_wizard.api.schemas.OverrideEntry(id: str, problem_class: str, operator: str, recorded_at: str, reason: str, revoked: bool = False, expires_at: str | None = None)[source]#

Bases: Struct

Operator-recorded validation override entry. Spec §11.3.

revoked is False on every override entry. The default is kept for ergonomic construction; the writer helper override_entry_to_dict ensures the field is always present on the wire (the spec requires it on every entry).

expires_at: str | None#
id: str#
operator: str#
problem_class: str#
reason: str#
recorded_at: str#
revoked: bool#
class exlab_wizard.api.schemas.PathsBlock(local: str, nas: str)[source]#

Bases: Struct

Resolved on-disk paths captured at creation time. Spec §11.3.

local: str#
nas: str#
class exlab_wizard.api.schemas.PluginApplied(plugin: str, version: str, files_affected: list[str], status: PluginStatus, isolation: PluginIsolation | None = None)[source]#

Bases: Struct

Per-plugin invocation record written into plugins_applied.

Spec §6.2.4 / §11.3. isolation was added in schema version 1.3; older readers ignore it and older writers treat its absence as a no-op.

files_affected: list[str]#
isolation: PluginIsolation | None#
plugin: str#
status: PluginStatus#
version: str#
class exlab_wizard.api.schemas.PluginIsolation(duration_ms: int, exit_code: int, peak_memory_mb: int)[source]#

Bases: Struct

Plugin worker isolation telemetry. Spec §6.2.4 / §11.3 (added 1.3).

duration_ms: int#
exit_code: int#
peak_memory_mb: int#
class exlab_wizard.api.schemas.ReadmeFieldsJson(schema_version: str, generated_at: str, core_fields: dict[str, str], system_fields: dict[str, ~typing.Any], template_fields: dict[str, ~typing.Any] = <factory>, config_fields: dict[str, ~typing.Any] = <factory>, custom_fields: list[dict[str, str]] = <factory>)[source]#

Bases: Struct

readme_fields.json at schema version 1.1. Spec §11.4.

config_fields: dict[str, Any]#
core_fields: dict[str, str]#
custom_fields: list[dict[str, str]]#
generated_at: str#
schema_version: str#
system_fields: dict[str, Any]#
template_fields: dict[str, Any]#
class exlab_wizard.api.schemas.TemplateBlock(name: str, version: str, source_path: str, run_scope: RunScope | None = None)[source]#

Bases: Struct

Template provenance captured at creation time. Spec §11.3.

name: str#
run_scope: RunScope | None#
source_path: str#
version: str#
class exlab_wizard.api.schemas.TestRunsJson(schema_version: str, created_at: str, project: str, equipment: str, run_kind: RunKind = RunKind.TEST)[source]#

Bases: Struct

test_runs.json marker at schema version 1.0. Spec §11.4.2.

Filename retained from v0.5 for backward compatibility even though the parent folder was renamed to TestRuns/ in v0.6.

created_at: str#
equipment: str#
project: str#
run_kind: RunKind#
schema_version: str#
class exlab_wizard.api.schemas.TombstoneEntry(id: str, revokes: str, operator: str, recorded_at: str, reason: str, revoked: bool = True)[source]#

Bases: Struct

Tombstone entry that revokes a prior override by id. Spec §11.3.

revoked is True on every tombstone. The default is kept for ergonomic construction; the writer helper tombstone_entry_to_dict ensures the field is always present on the wire.

id: str#
operator: str#
reason: str#
recorded_at: str#
revoked: bool#
revokes: str#
exlab_wizard.api.schemas.override_entry_to_dict(entry)[source]#

Serialize an OverrideEntry to a wire-form dict.

Uses msgspec.structs.asdict so that revoked (which equals its default of False on every override) is included. Spec §11.3 requires the field on every entry.

Parameters:

entry (OverrideEntry)

Return type:

dict[str, Any]

exlab_wizard.api.schemas.parse_validation_override_entry(entry)[source]#

Inspect entry["revoked"] and entry["revokes"] to decide which Struct shape to convert into.

The spec says tombstones carry revoked: True AND a revokes pointer; overrides carry revoked: False AND a problem_class. Use revoked first, revokes as a tiebreaker for old files (pre-1.6) where revokes may have been the only discriminator.

Parameters:

entry (dict[str, Any])

Return type:

OverrideEntry | TombstoneEntry

exlab_wizard.api.schemas.tombstone_entry_to_dict(entry)[source]#

Serialize a TombstoneEntry to a wire-form dict. Mirrors override_entry_to_dict; revoked is always emitted.

Parameters:

entry (TombstoneEntry)

Return type:

dict[str, Any]